diff --git a/.eslintrc.js b/.eslintrc.js index e47c4e438e6..4b9d86e4be3 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,7 +15,7 @@ module.exports = { 'plugin:storybook/recommended', ], - plugins: ['react', 'react-hooks', 'json', 'jest', 'testing-library'], + plugins: ['react', 'react-hooks', 'json', 'testing-library'], rules: { camelcase: 'off', @@ -107,31 +107,16 @@ module.exports = { '**/fixtures/**.@(js|ts|tsx)', 'scripts/*.@(js|ts|tsx)', ], - env: { - jest: true, - }, - extends: ['plugin:jest/recommended'], rules: { - 'jest/expect-expect': 'off', - 'jest/no-standalone-expect': 'off', - 'jest/no-disabled-tests': 'error', - 'jest/consistent-test-it': ['error', { fn: 'it' }], '@typescript-eslint/consistent-type-assertions': 'off', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/no-confusing-void-expression': 'warn', 'node/handle-callback-err': 'off', - // TODO(mc, 2021-01-29): fix these and remove warning overrides - 'jest/no-deprecated-functions': 'warn', - 'jest/valid-title': 'warn', - 'jest/no-conditional-expect': 'warn', - 'jest/no-alias-methods': 'warn', - 'jest/valid-describe-callback': 'warn', }, }, { files: ['**/__tests__/**test.tsx'], - env: { jest: true }, extends: ['plugin:testing-library/react'], rules: { 'testing-library/no-manual-cleanup': 'off', diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 198a4a0df63..3c426ab4e14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -291,7 +291,7 @@ JavaScript dependencies are installed by [yarn][]. When calling yarn, you should A development dependency is any dependency that is used only to help manage the project. Examples of development dependencies would be: - Build tools (webpack, babel) -- Testing/linting/checking tools (jest, typescript, eslint) +- Testing/linting/checking tools (vitest, typescript, eslint) - Libraries used only in support scripts (aws, express) To add a development dependency: diff --git a/DEV_SETUP.md b/DEV_SETUP.md index cd618d8bdbd..238f2c7fda3 100644 --- a/DEV_SETUP.md +++ b/DEV_SETUP.md @@ -82,7 +82,7 @@ Close and re-open your terminal to confirm `nvs` is installed. nvs --version ``` -Now we can use nvs to install Node.js v18 and switch on `auto` mode, which will make sure Node.js v18 is used any time we're in the `opentrons` project directory. +Now we can use `nvs` to install the currently required Node.js version set in `.nvmrc`. The `auto` command selects the correct version of Node.js any time we're in the `opentrons` project directory. Without `auto`, we would have to manually run `use` or `install` each time we work on the project. ```shell nvs add 18 diff --git a/api-client/src/maintenance_runs/createMaintenanceRunAction.ts b/api-client/src/maintenance_runs/createMaintenanceRunAction.ts deleted file mode 100644 index 27c0a5bb47d..00000000000 --- a/api-client/src/maintenance_runs/createMaintenanceRunAction.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { POST, request } from '../request' - -import type { ResponsePromise } from '../request' -import type { HostConfig } from '../types' -import type { MaintenanceRunAction, MaintenanceRunActionType } from './types' - -export interface CreateMaintenanceRunActionData { - actionType: MaintenanceRunActionType -} - -export function createMaintenanceRunAction( - config: HostConfig, - maintenanceRunId: string, - data: CreateMaintenanceRunActionData -): ResponsePromise { - return request< - MaintenanceRunAction, - { data: CreateMaintenanceRunActionData } - >(POST, `/maintenance_runs/${maintenanceRunId}/actions`, { data }, config) -} diff --git a/api-client/src/maintenance_runs/index.ts b/api-client/src/maintenance_runs/index.ts index 2dd20325652..1f48025cd4d 100644 --- a/api-client/src/maintenance_runs/index.ts +++ b/api-client/src/maintenance_runs/index.ts @@ -2,7 +2,6 @@ export { getMaintenanceRun } from './getMaintenanceRun' export { deleteMaintenanceRun } from './deleteMaintenanceRun' export { createMaintenanceRun } from './createMaintenanceRun' export { createMaintenanceCommand } from './createMaintenanceCommand' -export { createMaintenanceRunAction } from './createMaintenanceRunAction' export { createMaintenanceRunLabwareDefinition } from './createMaintenanceRunLabwareDefinition' export { getCurrentMaintenanceRun } from './getCurrentMaintenanceRun' diff --git a/api-client/src/maintenance_runs/types.ts b/api-client/src/maintenance_runs/types.ts index cda8f8fa0b5..6696e3ba072 100644 --- a/api-client/src/maintenance_runs/types.ts +++ b/api-client/src/maintenance_runs/types.ts @@ -4,37 +4,19 @@ import type { LoadedModule, LoadedPipette, } from '@opentrons/shared-data' -import type { RunCommandSummary, LabwareOffsetCreateData } from '../runs' - -export const ENGINE_STATUS_IDLE = 'idle' as const -export const ENGINE_STATUS_RUNNING = 'running' as const -export const ENGINE_STATUS_PAUSE_REQUESTED = 'pause-requested' as const -export const ENGINE_STATUS_PAUSED = 'paused' -export const ENGINE_STATUS_STOP_REQUESTED = 'stop-requested' as const -export const ENGINE_STATUS_STOPPED = 'stopped' as const -export const ENGINE_STATUS_FAILED = 'failed' as const -export const ENGINE_STATUS_FINISHING = 'finishing' as const -export const ENGINE_STATUS_SUCCEEDED = 'succeeded' as const -export const ENGINE_STATUS_BLOCKED_BY_OPEN_DOOR = 'blocked-by-open-door' as const - -export type EngineStatus = - | typeof ENGINE_STATUS_IDLE - | typeof ENGINE_STATUS_RUNNING - | typeof ENGINE_STATUS_PAUSE_REQUESTED - | typeof ENGINE_STATUS_PAUSED - | typeof ENGINE_STATUS_STOP_REQUESTED - | typeof ENGINE_STATUS_STOPPED - | typeof ENGINE_STATUS_FAILED - | typeof ENGINE_STATUS_FINISHING - | typeof ENGINE_STATUS_SUCCEEDED - | typeof ENGINE_STATUS_BLOCKED_BY_OPEN_DOOR +import type { + RunCommandSummary, + LabwareOffsetCreateData, + RunStatus, + RunAction, +} from '../runs' export interface MaintenanceRunData { id: string createdAt: string - status: EngineStatus + status: RunStatus current: boolean - actions: MaintenanceRunAction[] + actions: RunAction[] errors: MaintenanceRunError[] pipettes: LoadedPipette[] modules: LoadedModule[] @@ -48,25 +30,6 @@ export interface MaintenanceRun { data: MaintenanceRunData } -export const MAINTENANCE_RUN_ACTION_TYPE_PLAY: 'play' = 'play' -export const MAINTENANCE_RUN_ACTION_TYPE_PAUSE: 'pause' = 'pause' -export const MAINTENANCE_RUN_ACTION_TYPE_STOP: 'stop' = 'stop' - -export type MaintenanceRunActionType = - | typeof MAINTENANCE_RUN_ACTION_TYPE_PLAY - | typeof MAINTENANCE_RUN_ACTION_TYPE_PAUSE - | typeof MAINTENANCE_RUN_ACTION_TYPE_STOP - -export interface MaintenanceRunAction { - id: string - createdAt: string - actionType: MaintenanceRunActionType -} - -export interface MaintenanceCreateRunActionData { - actionType: MaintenanceRunActionType -} - export interface MaintenanceCommandData { data: RunCommandSummary } diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 319cb568d3a..317b99433c9 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -18,6 +18,7 @@ export const RUN_STATUS_FAILED = 'failed' as const export const RUN_STATUS_FINISHING = 'finishing' as const export const RUN_STATUS_SUCCEEDED = 'succeeded' as const export const RUN_STATUS_BLOCKED_BY_OPEN_DOOR = 'blocked-by-open-door' as const +export const RUN_STATUS_AWAITING_RECOVERY = 'awaiting-recovery' as const export type RunStatus = | typeof RUN_STATUS_IDLE @@ -30,6 +31,7 @@ export type RunStatus = | typeof RUN_STATUS_FINISHING | typeof RUN_STATUS_SUCCEEDED | typeof RUN_STATUS_BLOCKED_BY_OPEN_DOOR + | typeof RUN_STATUS_AWAITING_RECOVERY export interface RunData { id: string diff --git a/api/docs/v2/robot_position.rst b/api/docs/v2/robot_position.rst index a7b014b187f..8b2ed762e71 100644 --- a/api/docs/v2/robot_position.rst +++ b/api/docs/v2/robot_position.rst @@ -219,6 +219,9 @@ Movement Speeds In addition to instructing the robot where to move a pipette, you can also control the speed at which it moves. Speed controls can be applied either to all pipette motions or to movement along a particular axis. +.. note:: + Like all mechanical systems, Opentrons robots have resonant frequencies that depend on their construction and current configuration. It's possible to set a speed that causes your robot to resonate, producing louder sounds than typical operation. This is safe, but if you find it annoying, increase or decrease the speed slightly. + .. _gantry_speed: Gantry Speed diff --git a/api/src/opentrons/cli/analyze.py b/api/src/opentrons/cli/analyze.py index 4ee9f6507af..b2b7d7747a8 100644 --- a/api/src/opentrons/cli/analyze.py +++ b/api/src/opentrons/cli/analyze.py @@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional, Sequence, Union from typing_extensions import Literal +from opentrons.protocol_engine.types import RunTimeParameter from opentrons.protocols.api_support.types import APIVersion from opentrons.protocol_reader import ( ProtocolReader, @@ -99,6 +100,9 @@ async def _analyze( ), metadata=protocol_source.metadata, robotType=protocol_source.robot_type, + # TODO(spp, 2024-03-12): update this once protocol reader/ runner can parse + # and report the runTimeParameters + runTimeParameters=[], commands=analysis.commands, errors=analysis.state_summary.errors, labware=analysis.state_summary.labware, @@ -156,6 +160,7 @@ class AnalyzeResults(BaseModel): # Fields that should match robot-server: robotType: RobotType + runTimeParameters: List[RunTimeParameter] commands: List[Command] labware: List[LoadedLabware] pipettes: List[LoadedPipette] diff --git a/api/src/opentrons/config/advanced_settings.py b/api/src/opentrons/config/advanced_settings.py index feebbe1cb48..f679c742d7e 100644 --- a/api/src/opentrons/config/advanced_settings.py +++ b/api/src/opentrons/config/advanced_settings.py @@ -219,6 +219,20 @@ class Setting(NamedTuple): description="When this setting is on, Flex will continue its activities regardless of pressure changes inside the pipette. Do not turn this setting on unless you are intentionally causing pressures over 8 kPa inside the pipette air channel.", robot_type=[RobotTypeEnum.FLEX], ), + SettingDefinition( + _id="enableErrorRecoveryExperiments", + title="Enable error recovery experiments", + description=( + "Do not enable." + " This is an Opentrons internal setting to experiment with" + " in-development error recovery features." + " This will interfere with your protocol runs," + " corrupt your robot's storage," + " bring misfortune and pestilence upon you and your livestock, etc." + ), + robot_type=[RobotTypeEnum.FLEX], + internal_only=True, + ), ] if ( @@ -668,6 +682,16 @@ def _migrate29to30(previous: SettingsMap) -> SettingsMap: return {k: v for k, v in previous.items() if "disableTipPresenceDetection" != k} +def _migrate30to31(previous: SettingsMap) -> SettingsMap: + """Migrate to version 31 of the feature flags file. + + - Adds the enableErrorRecoveryExperiments config element. + """ + newmap = {k: v for k, v in previous.items()} + newmap["enableErrorRecoveryExperiments"] = None + return newmap + + _MIGRATIONS = [ _migrate0to1, _migrate1to2, @@ -699,6 +723,7 @@ def _migrate29to30(previous: SettingsMap) -> SettingsMap: _migrate27to28, _migrate28to29, _migrate29to30, + _migrate30to31, ] """ List of all migrations to apply, indexed by (version - 1). See _migrate below diff --git a/api/src/opentrons/config/feature_flags.py b/api/src/opentrons/config/feature_flags.py index 583dae0b141..4a1161a2391 100644 --- a/api/src/opentrons/config/feature_flags.py +++ b/api/src/opentrons/config/feature_flags.py @@ -70,3 +70,9 @@ def require_estop() -> bool: return not advs.get_setting_with_env_overload( "estopNotRequired", RobotTypeEnum.FLEX ) + + +def enable_error_recovery_experiments() -> bool: + return advs.get_setting_with_env_overload( + "enableErrorRecoveryExperiments", RobotTypeEnum.FLEX + ) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 1bbe70712ce..6bf569bcd67 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -33,6 +33,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteNameType from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType +from opentrons.hardware_control.nozzle_manager import NozzleMap from . import deck_conflict from ..instrument import AbstractInstrument @@ -675,6 +676,9 @@ def get_active_channels(self) -> int: self._pipette_id ) + def get_nozzle_map(self) -> NozzleMap: + return self._engine_client.state.tips.get_pipette_nozzle_map(self._pipette_id) + def has_tip(self) -> bool: return ( self._engine_client.state.pipettes.get_attached_tip(self._pipette_id) @@ -709,14 +713,9 @@ def is_tip_tracking_available(self) -> bool: return True else: if self.get_channels() == 96: - # SINGLE configuration with H12 nozzle is technically supported by the - # current tip tracking implementation but we don't do any deck conflict - # checks for it, so we won't provide full support for it yet. - return ( - self.get_nozzle_configuration() == NozzleConfigurationType.COLUMN - and primary_nozzle == "A12" - ) + return True if self.get_channels() == 8: + # TODO: (cb, 03/06/24): Enable automatic tip tracking on the 8 channel pipettes once PAPI support exists return ( self.get_nozzle_configuration() == NozzleConfigurationType.SINGLE and primary_nozzle == "H1" diff --git a/api/src/opentrons/protocol_api/core/engine/labware.py b/api/src/opentrons/protocol_api/core/engine/labware.py index 5190831810c..9b48b309aa2 100644 --- a/api/src/opentrons/protocol_api/core/engine/labware.py +++ b/api/src/opentrons/protocol_api/core/engine/labware.py @@ -11,6 +11,7 @@ from opentrons.protocol_engine.errors import LabwareNotOnDeckError, ModuleNotOnDeckError from opentrons.protocol_engine.clients import SyncClient as ProtocolEngineClient from opentrons.types import DeckSlotName, Point +from opentrons.hardware_control.nozzle_manager import NozzleMap from ..labware import AbstractLabware, LabwareLoadParams from .well import WellCore @@ -122,7 +123,10 @@ def reset_tips(self) -> None: raise TypeError(f"{self.get_display_name()} is not a tip rack.") def get_next_tip( - self, num_tips: int, starting_tip: Optional[WellCore] + self, + num_tips: int, + starting_tip: Optional[WellCore], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: return self._engine_client.state.tips.get_next_tip( labware_id=self._labware_id, @@ -132,6 +136,7 @@ def get_next_tip( if starting_tip and starting_tip.labware_id == self._labware_id else None ), + nozzle_map=nozzle_map, ) def get_well_columns(self) -> List[List[str]]: diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 1864d308c4f..061e7d13960 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -9,6 +9,7 @@ from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleMap from ..disposal_locations import TrashBin, WasteChute from .well import WellCoreType @@ -218,6 +219,10 @@ def get_channels(self) -> int: def get_active_channels(self) -> int: ... + @abstractmethod + def get_nozzle_map(self) -> NozzleMap: + ... + @abstractmethod def has_tip(self) -> bool: ... diff --git a/api/src/opentrons/protocol_api/core/labware.py b/api/src/opentrons/protocol_api/core/labware.py index 4411155692f..ada1a7ff0ed 100644 --- a/api/src/opentrons/protocol_api/core/labware.py +++ b/api/src/opentrons/protocol_api/core/labware.py @@ -11,6 +11,7 @@ ) from opentrons.types import DeckSlotName, Point +from opentrons.hardware_control.nozzle_manager import NozzleMap from .well import WellCoreType @@ -110,7 +111,10 @@ def reset_tips(self) -> None: @abstractmethod def get_next_tip( - self, num_tips: int, starting_tip: Optional[WellCoreType] + self, + num_tips: int, + starting_tip: Optional[WellCoreType], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: """Get the name of the next available tip(s) in the rack, if available.""" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index db3ad39e6d9..57f129c32b3 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -18,6 +18,7 @@ ) from opentrons.protocols.geometry import planning from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleMap from ...disposal_locations import TrashBin, WasteChute from ..instrument import AbstractInstrument @@ -550,6 +551,10 @@ def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" + def get_nozzle_map(self) -> NozzleMap: + """This will never be called because it was added in API 2.18.""" + assert False, "get_nozzle_map only supported in API 2.18 & later" + def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py index 2749ef8949a..ece9be66f19 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_labware_core.py @@ -5,6 +5,7 @@ from opentrons.protocols.api_support.tip_tracker import TipTracker from opentrons.types import DeckSlotName, Location, Point +from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons_shared_data.labware.dev_types import LabwareParameters, LabwareDefinition from ..labware import AbstractLabware, LabwareLoadParams @@ -153,8 +154,15 @@ def reset_tips(self) -> None: well.set_has_tip(True) def get_next_tip( - self, num_tips: int, starting_tip: Optional[LegacyWellCore] + self, + num_tips: int, + starting_tip: Optional[LegacyWellCore], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: + if nozzle_map is not None: + raise ValueError( + "Nozzle Map cannot be provided to calls for next tip in legacy protocols." + ) next_well = self._tip_tracker.next_tip(num_tips, starting_tip) return next_well.get_name() if next_well else None diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index fb47da62c50..2ee61adf24e 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -23,6 +23,7 @@ from ...disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.hardware_control.nozzle_manager import NozzleMap from ..instrument import AbstractInstrument @@ -468,6 +469,10 @@ def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" + def get_nozzle_map(self) -> NozzleMap: + """This will never be called because it was added in API 2.18.""" + assert False, "get_nozzle_map only supported in API 2.18 & later" + def is_tip_tracking_available(self) -> bool: # Tip tracking is always available in legacy context return True diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 56c8dd4b5eb..9754def8e5b 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -27,6 +27,7 @@ requires_version, APIVersionError, ) +from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType from .core.common import InstrumentCore, ProtocolCore from .core.engine import ENGINE_CORE_API_VERSION @@ -56,6 +57,9 @@ _DROP_TIP_LOCATION_ALTERNATING_ADDED_IN = APIVersion(2, 15) """The version after which a drop-tip-into-trash procedure drops tips in different alternating locations within the trash well.""" _PARTIAL_NOZZLE_CONFIGURATION_ADDED_IN = APIVersion(2, 16) +"""The version after which a partial nozzle configuration became available for the 96 Channel Pipette.""" +_PARTIAL_NOZZLE_CONFIGURATION_AUTOMATIC_TIP_TRACKING_IN = APIVersion(2, 18) +"""The version after which automatic tip tracking supported partially configured nozzle layouts.""" class InstrumentContext(publisher.CommandPublisher): @@ -877,8 +881,31 @@ def pick_up_tip( # noqa: C901 if self._api_version >= _PARTIAL_NOZZLE_CONFIGURATION_ADDED_IN else self.channels ) + nozzle_map = ( + self._core.get_nozzle_map() + if self._api_version + >= _PARTIAL_NOZZLE_CONFIGURATION_AUTOMATIC_TIP_TRACKING_IN + else None + ) if location is None: + if ( + nozzle_map is not None + and nozzle_map.configuration != NozzleConfigurationType.FULL + and self.starting_tip is not None + ): + # Disallowing this avoids concerning the system with the direction + # in which self.starting_tip consumes tips. It would currently vary + # depending on the configuration layout of a pipette at a given + # time, which means that some combination of starting tip and partial + # configuraiton are incompatible under the current understanding of + # starting tip behavior. Replacing starting_tip with an undeprecated + # Labware.has_tip may solve this. + raise CommandPreconditionViolated( + "Automatic tip tracking is not available when using a partial pipette" + " nozzle configuration and InstrumentContext.starting_tip." + " Switch to a full configuration or set starting_tip to None." + ) if not self._core.is_tip_tracking_available(): raise CommandPreconditionViolated( "Automatic tip tracking is not available for the current pipette" @@ -886,11 +913,11 @@ def pick_up_tip( # noqa: C901 " that supports automatic tip tracking or specifying the exact tip" " to pick up." ) - tip_rack, well = labware.next_available_tip( starting_tip=self.starting_tip, tip_racks=self.tip_racks, channels=active_channels, + nozzle_map=nozzle_map, ) elif isinstance(location, labware.Well): @@ -902,6 +929,7 @@ def pick_up_tip( # noqa: C901 starting_tip=None, tip_racks=[location], channels=active_channels, + nozzle_map=nozzle_map, ) elif isinstance(location, types.Location): @@ -917,6 +945,7 @@ def pick_up_tip( # noqa: C901 starting_tip=None, tip_racks=[maybe_tip_rack], channels=active_channels, + nozzle_map=nozzle_map, ) else: raise TypeError( @@ -1323,6 +1352,12 @@ def transfer( # noqa: C901 if self._api_version >= _PARTIAL_NOZZLE_CONFIGURATION_ADDED_IN else self.channels ) + nozzle_map = ( + self._core.get_nozzle_map() + if self._api_version + >= _PARTIAL_NOZZLE_CONFIGURATION_AUTOMATIC_TIP_TRACKING_IN + else None + ) if blow_out and not blowout_location: if self.current_volume: @@ -1339,7 +1374,10 @@ def transfer( # noqa: C901 if new_tip != types.TransferTipPolicy.NEVER: tr, next_tip = labware.next_available_tip( - self.starting_tip, self.tip_racks, active_channels + self.starting_tip, + self.tip_racks, + active_channels, + nozzle_map=nozzle_map, ) max_volume = min(next_tip.max_volume, self.max_volume) else: diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 9333c75f60d..ecb4d06ac5b 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -20,6 +20,7 @@ from opentrons.types import Location, Point from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import requires_version, APIVersionError +from opentrons.hardware_control.nozzle_manager import NozzleMap # TODO(mc, 2022-09-02): re-exports provided for backwards compatibility # remove when their usage is no longer needed @@ -883,7 +884,11 @@ def tip_length(self, length: float) -> None: # TODO(mc, 2022-11-09): implementation detail; deprecate public method def next_tip( - self, num_tips: int = 1, starting_tip: Optional[Well] = None + self, + num_tips: int = 1, + starting_tip: Optional[Well] = None, + *, + nozzle_map: Optional[NozzleMap] = None, ) -> Optional[Well]: """ Find the next valid well for pick-up. @@ -904,6 +909,7 @@ def next_tip( well_name = self._core.get_next_tip( num_tips=num_tips, starting_tip=starting_tip._core if starting_tip else None, + nozzle_map=nozzle_map, ) return self._wells_by_name[well_name] if well_name is not None else None @@ -1061,7 +1067,11 @@ def split_tipracks(tip_racks: List[Labware]) -> Tuple[Labware, List[Labware]]: # TODO(mc, 2022-11-09): implementation detail, move to core def select_tiprack_from_list( - tip_racks: List[Labware], num_channels: int, starting_point: Optional[Well] = None + tip_racks: List[Labware], + num_channels: int, + starting_point: Optional[Well] = None, + *, + nozzle_map: Optional[NozzleMap] = None, ) -> Tuple[Labware, Well]: try: first, rest = split_tipracks(tip_racks) @@ -1074,14 +1084,16 @@ def select_tiprack_from_list( ) elif starting_point: first_well = starting_point + elif nozzle_map: + first_well = None else: first_well = first.wells()[0] - next_tip = first.next_tip(num_channels, first_well) + next_tip = first.next_tip(num_channels, first_well, nozzle_map=nozzle_map) if next_tip: return first, next_tip else: - return select_tiprack_from_list(rest, num_channels) + return select_tiprack_from_list(rest, num_channels, None, nozzle_map=nozzle_map) # TODO(mc, 2022-11-09): implementation detail, move to core @@ -1093,14 +1105,23 @@ def filter_tipracks_to_start( # TODO(mc, 2022-11-09): implementation detail, move to core def next_available_tip( - starting_tip: Optional[Well], tip_racks: List[Labware], channels: int + starting_tip: Optional[Well], + tip_racks: List[Labware], + channels: int, + *, + nozzle_map: Optional[NozzleMap] = None, ) -> Tuple[Labware, Well]: start = starting_tip if start is None: - return select_tiprack_from_list(tip_racks, channels) + return select_tiprack_from_list( + tip_racks, channels, None, nozzle_map=nozzle_map + ) else: return select_tiprack_from_list( - filter_tipracks_to_start(start, tip_racks), channels, start + filter_tipracks_to_start(start, tip_racks), + channels, + start, + nozzle_map=nozzle_map, ) diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 07f2ae17f9c..eb62ee7f33a 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -13,6 +13,7 @@ ) from .protocol_engine import ProtocolEngine from .errors import ProtocolEngineError, ErrorOccurrence +from .notes import CommandNote from .commands import ( Command, CommandParams, @@ -20,7 +21,6 @@ CommandStatus, CommandType, CommandIntent, - CommandNote, ) from .state import State, StateView, StateSummary, CommandSlice, CurrentCommand, Config from .plugins import AbstractPlugin diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 97f0744a9a2..3dfe6eaf51f 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -28,7 +28,6 @@ BaseCommandCreate, CommandStatus, CommandIntent, - CommandNote, ) from .command_unions import ( @@ -333,7 +332,6 @@ "BaseCommandCreate", "CommandStatus", "CommandIntent", - "CommandNote", # command parameter hashing "hash_command_params", # command schema generation diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 35f0878612b..4dcb81dcc33 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler from ..state import StateView + from ..notes import CommandNoteAdder AspirateCommandType = Literal["aspirate"] @@ -48,12 +49,14 @@ def __init__( state_view: StateView, hardware_api: HardwareControlAPI, movement: MovementHandler, + command_note_adder: CommandNoteAdder, **kwargs: object, ) -> None: self._pipetting = pipetting self._state_view = state_view self._hardware_api = hardware_api self._movement = movement + self._command_note_adder = command_note_adder async def execute(self, params: AspirateParams) -> AspirateResult: """Move to and aspirate from the requested well. @@ -98,7 +101,10 @@ async def execute(self, params: AspirateParams) -> AspirateResult: ) volume = await self._pipetting.aspirate_in_place( - pipette_id=pipette_id, volume=params.volume, flow_rate=params.flowRate + pipette_id=pipette_id, + volume=params.volume, + flow_rate=params.flowRate, + command_note_adder=self._command_note_adder, ) return AspirateResult( diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index 4cdcd36297c..f59bccdd9f7 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: from ..execution import PipettingHandler from ..state import StateView - + from ..notes import CommandNoteAdder AspirateInPlaceCommandType = Literal["aspirateInPlace"] @@ -45,11 +45,13 @@ def __init__( pipetting: PipettingHandler, hardware_api: HardwareControlAPI, state_view: StateView, + command_note_adder: CommandNoteAdder, **kwargs: object, ) -> None: self._pipetting = pipetting self._state_view = state_view self._hardware_api = hardware_api + self._command_note_adder = command_note_adder async def execute(self, params: AspirateInPlaceParams) -> AspirateInPlaceResult: """Aspirate without moving the pipette. @@ -69,7 +71,10 @@ async def execute(self, params: AspirateInPlaceParams) -> AspirateInPlaceResult: " so the plunger can be reset in a known safe position." ) volume = await self._pipetting.aspirate_in_place( - pipette_id=params.pipetteId, volume=params.volume, flow_rate=params.flowRate + pipette_id=params.pipetteId, + volume=params.volume, + flow_rate=params.flowRate, + command_note_adder=self._command_note_adder, ) return AspirateInPlaceResult(volume=volume) diff --git a/api/src/opentrons/protocol_engine/commands/command.py b/api/src/opentrons/protocol_engine/commands/command.py index 1bf72e12352..5c2ab46b06f 100644 --- a/api/src/opentrons/protocol_engine/commands/command.py +++ b/api/src/opentrons/protocol_engine/commands/command.py @@ -12,8 +12,6 @@ Optional, TypeVar, Tuple, - Union, - Literal, List, ) @@ -23,6 +21,7 @@ from opentrons.hardware_control import HardwareControlAPI from ..errors import ErrorOccurrence +from ..notes import CommandNote, CommandNoteAdder # Work around type-only circular dependencies. if TYPE_CHECKING: @@ -36,29 +35,6 @@ CommandPrivateResultT = TypeVar("CommandPrivateResultT") -NoteKind = Union[Literal["warning", "information"], str] - - -class CommandNote(BaseModel): - """A note about a command's execution or dispatch.""" - - noteKind: NoteKind = Field( - ..., - description="The kind of note this is. Only the literal possibilities should be" - " relied upon programmatically.", - ) - shortMessage: str = Field( - ..., - description="The accompanying human-readable short message (suitable for display in a single line)", - ) - longMessage: str = Field( - ..., - description="A longer message that may contain newlines and formatting characters describing the note.", - ) - source: str = Field( - ..., description="An identifier for the party that created the note" - ) - class CommandStatus(str, Enum): """Command execution status.""" @@ -215,6 +191,7 @@ def __init__( run_control: execution.RunControlHandler, rail_lights: execution.RailLightsHandler, status_bar: execution.StatusBarHandler, + command_note_adder: CommandNoteAdder, ) -> None: """Initialize the command implementation with execution handlers.""" pass @@ -256,6 +233,7 @@ def __init__( run_control: execution.RunControlHandler, rail_lights: execution.RailLightsHandler, status_bar: execution.StatusBarHandler, + command_note_adder: CommandNoteAdder, ) -> None: """Initialize the command implementation with execution handlers.""" pass diff --git a/api/src/opentrons/protocol_engine/execution/command_executor.py b/api/src/opentrons/protocol_engine/execution/command_executor.py index 7334d96e170..105d2af3994 100644 --- a/api/src/opentrons/protocol_engine/execution/command_executor.py +++ b/api/src/opentrons/protocol_engine/execution/command_executor.py @@ -1,7 +1,7 @@ """Command side-effect execution logic container.""" import asyncio from logging import getLogger -from typing import Optional +from typing import Optional, List, Dict, Any, Protocol from opentrons.hardware_control import HardwareControlAPI @@ -18,10 +18,12 @@ AbstractCommandImpl, CommandResult, CommandPrivateResult, + Command, ) from ..actions import ActionDispatcher, UpdateCommandAction, FailCommandAction from ..errors import RunStoppedError from ..errors.exceptions import EStopActivatedError as PE_EStopActivatedError +from ..notes import CommandNote, CommandNoteTracker from .equipment import EquipmentHandler from .movement import MovementHandler from .gantry_mover import GantryMover @@ -36,6 +38,29 @@ log = getLogger(__name__) +class CommandNoteTrackerProvider(Protocol): + """The correct shape for a function that provides a CommandNoteTracker. + + This function will be called by the executor once for each call to execute(). + It is mostly useful for testing harnesses. + """ + + def __call__(self) -> CommandNoteTracker: + """Provide a new CommandNoteTracker.""" + ... + + +class _NoteTracker(CommandNoteTracker): + def __init__(self) -> None: + self._notes: List[CommandNote] = [] + + def __call__(self, note: CommandNote) -> None: + self._notes.append(note) + + def get_notes(self) -> List[CommandNote]: + return self._notes + + class CommandExecutor: """CommandExecutor container class. @@ -58,6 +83,7 @@ def __init__( rail_lights: RailLightsHandler, status_bar: StatusBarHandler, model_utils: Optional[ModelUtils] = None, + command_note_tracker_provider: Optional[CommandNoteTrackerProvider] = None, ) -> None: """Initialize the CommandExecutor with access to its dependencies.""" self._hardware_api = hardware_api @@ -73,6 +99,9 @@ def __init__( self._rail_lights = rail_lights self._model_utils = model_utils or ModelUtils() self._status_bar = status_bar + self._command_note_tracker_provider = ( + command_note_tracker_provider or _NoteTracker + ) async def execute(self, command_id: str) -> None: """Run a given command's execution procedure. @@ -82,6 +111,7 @@ async def execute(self, command_id: str) -> None: command itself will be looked up from state. """ command = self._state_store.commands.get(command_id=command_id) + note_tracker = self._command_note_tracker_provider() command_impl = command._ImplementationCls( state_view=self._state_store, hardware_api=self._hardware_api, @@ -94,6 +124,7 @@ async def execute(self, command_id: str) -> None: run_control=self._run_control, rail_lights=self._rail_lights, status_bar=self._status_bar, + command_note_adder=note_tracker, ) started_at = self._model_utils.get_timestamp() @@ -128,6 +159,17 @@ async def execute(self, command_id: str) -> None: error = PE_EStopActivatedError(message=str(error), wrapping=[error]) elif not isinstance(error, EnumeratedError): error = PythonException(error) + notes_update = _append_notes_if_notes( + running_command, note_tracker.get_notes() + ) + + if notes_update: + command_with_new_notes = running_command.copy(update=notes_update) + self._action_dispatcher.dispatch( + UpdateCommandAction( + command=command_with_new_notes, private_result=None + ) + ) self._action_dispatcher.dispatch( FailCommandAction( @@ -138,15 +180,25 @@ async def execute(self, command_id: str) -> None: ) ) else: - completed_command = running_command.copy( - update={ - "result": result, - "status": CommandStatus.SUCCEEDED, - "completedAt": self._model_utils.get_timestamp(), - } - ) + update = { + "result": result, + "status": CommandStatus.SUCCEEDED, + "completedAt": self._model_utils.get_timestamp(), + **_append_notes_if_notes(running_command, note_tracker.get_notes()), + } + completed_command = running_command.copy(update=update) self._action_dispatcher.dispatch( UpdateCommandAction( command=completed_command, private_result=private_result ), ) + + +def _append_notes_if_notes( + running_command: Command, notes: List[CommandNote] +) -> Dict[str, Any]: + if not notes: + return {} + if running_command.notes is None: + return {"notes": notes} + return {"notes": running_command.notes + notes} diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index 7305a4c09da..7abfb158539 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -6,6 +6,7 @@ from opentrons.hardware_control import HardwareControlAPI from ..state import StateView, HardwarePipette +from ..notes import CommandNoteAdder, CommandNote from ..errors.exceptions import ( TipNotAttachedError, InvalidAspirateVolumeError, @@ -39,6 +40,7 @@ async def aspirate_in_place( pipette_id: str, volume: float, flow_rate: float, + command_note_adder: CommandNoteAdder, ) -> float: """Set flow-rate and aspirate.""" @@ -88,11 +90,15 @@ async def aspirate_in_place( pipette_id: str, volume: float, flow_rate: float, + command_note_adder: CommandNoteAdder, ) -> float: """Set flow-rate and aspirate.""" # get mount and config data from state and hardware controller adjusted_volume = _validate_aspirate_volume( - state_view=self._state_view, pipette_id=pipette_id, aspirate_volume=volume + state_view=self._state_view, + pipette_id=pipette_id, + aspirate_volume=volume, + command_note_adder=command_note_adder, ) hw_pipette = self._state_view.pipettes.get_hardware_pipette( pipette_id=pipette_id, @@ -199,11 +205,15 @@ async def aspirate_in_place( pipette_id: str, volume: float, flow_rate: float, + command_note_adder: CommandNoteAdder, ) -> float: """Virtually aspirate (no-op).""" self._validate_tip_attached(pipette_id=pipette_id, command_name="aspirate") return _validate_aspirate_volume( - state_view=self._state_view, pipette_id=pipette_id, aspirate_volume=volume + state_view=self._state_view, + pipette_id=pipette_id, + aspirate_volume=volume, + command_note_adder=command_note_adder, ) async def dispense_in_place( @@ -252,7 +262,10 @@ def create_pipetting_handler( def _validate_aspirate_volume( - state_view: StateView, pipette_id: str, aspirate_volume: float + state_view: StateView, + pipette_id: str, + aspirate_volume: float, + command_note_adder: CommandNoteAdder, ) -> float: """Get whether the given volume is valid to aspirate right now. @@ -285,7 +298,21 @@ def _validate_aspirate_volume( ), ) else: - return min(aspirate_volume, available_volume) + volume_to_aspirate = min(aspirate_volume, available_volume) + if volume_to_aspirate < aspirate_volume: + command_note_adder( + CommandNote( + noteKind="warning", + shortMessage=f"Aspirate clamped to {available_volume} µL", + longMessage=( + f"Command requested to aspirate {aspirate_volume} µL but only" + f" {available_volume} µL were available in the pipette. This is" + " probably a floating point artifact." + ), + source="execution", + ) + ) + return volume_to_aspirate def _validate_dispense_volume( diff --git a/api/src/opentrons/protocol_engine/notes/__init__.py b/api/src/opentrons/protocol_engine/notes/__init__.py new file mode 100644 index 00000000000..f5b1d8c1a2a --- /dev/null +++ b/api/src/opentrons/protocol_engine/notes/__init__.py @@ -0,0 +1,5 @@ +"""Protocol engine notes module.""" + +from .notes import NoteKind, CommandNote, CommandNoteAdder, CommandNoteTracker + +__all__ = ["NoteKind", "CommandNote", "CommandNoteAdder", "CommandNoteTracker"] diff --git a/api/src/opentrons/protocol_engine/notes/notes.py b/api/src/opentrons/protocol_engine/notes/notes.py new file mode 100644 index 00000000000..cf381aa4a68 --- /dev/null +++ b/api/src/opentrons/protocol_engine/notes/notes.py @@ -0,0 +1,42 @@ +"""Definitions of data and interface shapes for notes.""" +from typing import Union, Literal, Protocol, List +from pydantic import BaseModel, Field + +NoteKind = Union[Literal["warning", "information"], str] + + +class CommandNote(BaseModel): + """A note about a command's execution or dispatch.""" + + noteKind: NoteKind = Field( + ..., + description="The kind of note this is. Only the literal possibilities should be" + " relied upon programmatically.", + ) + shortMessage: str = Field( + ..., + description="The accompanying human-readable short message (suitable for display in a single line)", + ) + longMessage: str = Field( + ..., + description="A longer message that may contain newlines and formatting characters describing the note.", + ) + source: str = Field( + ..., description="An identifier for the party that created the note" + ) + + +class CommandNoteAdder(Protocol): + """The shape of a function that something can use to add a command note.""" + + def __call__(self, note: CommandNote) -> None: + """When called, this function should add the passed Note to some list.""" + ... + + +class CommandNoteTracker(CommandNoteAdder, Protocol): + """The shape of a class that can track notes.""" + + def get_notes(self) -> List[CommandNote]: + """When called, should return all notes previously added with __call__.""" + ... diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 3c408828337..9155a6da678 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -241,7 +241,7 @@ def estop(self, maintenance_run: bool) -> None: if self._state_store.commands.get_is_stopped(): return current_id = ( - self._state_store.commands.state.running_command_id + self._state_store.commands.get_running_command_id() or self._state_store.commands.state.queued_command_ids.head(None) ) diff --git a/api/src/opentrons/protocol_engine/state/commands.py b/api/src/opentrons/protocol_engine/state/commands.py index 1c47986c62b..6a93197ee4d 100644 --- a/api/src/opentrons/protocol_engine/state/commands.py +++ b/api/src/opentrons/protocol_engine/state/commands.py @@ -271,45 +271,30 @@ def handle_action(self, action: Action) -> None: # noqa: C901 error=action.error, ) prev_entry = self._state.commands_by_id[action.command_id] - self._state.commands_by_id[action.command_id] = CommandEntry( - index=prev_entry.index, - # TODO(mc, 2022-06-06): add new "cancelled" status or similar - # and don't set `completedAt` in commands other than the - # specific one that failed - command=prev_entry.command.copy( - update={ - "error": error_occurrence, - "completedAt": action.failed_at, - "status": CommandStatus.FAILED, - } - ), + # TODO(mc, 2022-06-06): add new "cancelled" status or similar + self._update_to_failed( + command_id=action.command_id, + failed_at=action.failed_at, + error_occurrence=error_occurrence, ) self._state.failed_command = self._state.commands_by_id[action.command_id] + if prev_entry.command.intent == CommandIntent.SETUP: - other_command_ids_to_fail = [ - *[i for i in self._state.queued_setup_command_ids], - ] + other_command_ids_to_fail = self._state.queued_setup_command_ids + for id in other_command_ids_to_fail: + self._update_to_failed( + command_id=id, failed_at=action.failed_at, error_occurrence=None + ) self._state.queued_setup_command_ids.clear() else: - other_command_ids_to_fail = [ - *[i for i in self._state.queued_command_ids], - ] + other_command_ids_to_fail = self._state.queued_command_ids + for id in other_command_ids_to_fail: + self._update_to_failed( + command_id=id, failed_at=action.failed_at, error_occurrence=None + ) self._state.queued_command_ids.clear() - for command_id in other_command_ids_to_fail: - prev_entry = self._state.commands_by_id[command_id] - - self._state.commands_by_id[command_id] = CommandEntry( - index=prev_entry.index, - command=prev_entry.command.copy( - update={ - "completedAt": action.failed_at, - "status": CommandStatus.FAILED, - } - ), - ) - if self._state.running_command_id == action.command_id: self._state.running_command_id = None @@ -378,6 +363,24 @@ def handle_action(self, action: Action) -> None: # noqa: C901 elif action.door_state == DoorState.CLOSED: self._state.is_door_blocking = False + def _update_to_failed( + self, + command_id: str, + failed_at: datetime, + error_occurrence: Optional[ErrorOccurrence], + ) -> None: + prev_entry = self._state.commands_by_id[command_id] + updated_command = prev_entry.command.copy( + update={ + "completedAt": failed_at, + "status": CommandStatus.FAILED, + **({"error": error_occurrence} if error_occurrence else {}), + } + ) + self._state.commands_by_id[command_id] = CommandEntry( + index=prev_entry.index, command=updated_command + ) + @staticmethod def _map_run_exception_to_error_occurrence( error_id: str, created_at: datetime, exception: Exception @@ -516,6 +519,10 @@ def get_error(self) -> Optional[ErrorOccurrence]: else: return run_error or finish_error + def get_running_command_id(self) -> Optional[str]: + """Return the ID of the command that's currently running, if there is one.""" + return self._state.running_command_id + def get_current(self) -> Optional[CurrentCommand]: """Return the "current" command, if any. @@ -632,6 +639,9 @@ def get_all_commands_final(self) -> bool: ) if no_command_running and no_command_to_execute: + # TODO(mm, 2024-03-14): This is a slow O(n) scan. When a long run ends and + # we reach this loop, it can disrupt the robot server. + # https://opentrons.atlassian.net/browse/EXEC-55 for command_id in self._state.all_command_ids: command = self._state.commands_by_id[command_id].command if command.error and command.intent != CommandIntent.SETUP: @@ -691,6 +701,13 @@ def validate_action_allowed( "Setup commands are not allowed after run has started." ) + elif self.get_status() == EngineStatus.AWAITING_RECOVERY: + # While we're developing error recovery, we'll conservatively disallow + # all actions, to avoid putting the engine in weird undefined states. + # We'll allow specific actions here as we flesh things out and add support + # for them. + raise NotImplementedError() + return action def get_status(self) -> EngineStatus: diff --git a/api/src/opentrons/protocol_engine/state/tips.py b/api/src/opentrons/protocol_engine/state/tips.py index 0e68710ae28..67598c32bba 100644 --- a/api/src/opentrons/protocol_engine/state/tips.py +++ b/api/src/opentrons/protocol_engine/state/tips.py @@ -1,7 +1,7 @@ """Tip state tracking.""" from dataclasses import dataclass from enum import Enum -from typing import Dict, Optional, List +from typing import Dict, Optional, List, Union from .abstract_store import HasState, HandlesActions from ..actions import ( @@ -21,6 +21,8 @@ PipetteNozzleLayoutResultMixin, ) +from opentrons.hardware_control.nozzle_manager import NozzleMap + class TipRackWellState(Enum): """The state of a single tip in a tip rack's well.""" @@ -41,6 +43,7 @@ class TipState: channels_by_pipette_id: Dict[str, int] length_by_pipette_id: Dict[str, float] active_channels_by_pipette_id: Dict[str, int] + nozzle_map_by_pipette_id: Dict[str, NozzleMap] class TipStore(HasState[TipState], HandlesActions): @@ -56,6 +59,7 @@ def __init__(self) -> None: channels_by_pipette_id={}, length_by_pipette_id={}, active_channels_by_pipette_id={}, + nozzle_map_by_pipette_id={}, ) def handle_action(self, action: Action) -> None: @@ -66,6 +70,7 @@ def handle_action(self, action: Action) -> None: config = action.private_result.config self._state.channels_by_pipette_id[pipette_id] = config.channels self._state.active_channels_by_pipette_id[pipette_id] = config.channels + self._state.nozzle_map_by_pipette_id[pipette_id] = config.nozzle_map self._handle_command(action.command) if isinstance(action.private_result, PipetteNozzleLayoutResultMixin): @@ -75,6 +80,7 @@ def handle_action(self, action: Action) -> None: self._state.active_channels_by_pipette_id[ pipette_id ] = nozzle_map.tip_count + self._state.nozzle_map_by_pipette_id[pipette_id] = nozzle_map else: self._state.active_channels_by_pipette_id[ pipette_id @@ -118,24 +124,46 @@ def _handle_command(self, command: Command) -> None: pipette_id = command.params.pipetteId self._state.length_by_pipette_id.pop(pipette_id, None) - def _set_used_tips(self, pipette_id: str, well_name: str, labware_id: str) -> None: - pipette_channels = self._state.active_channels_by_pipette_id.get(pipette_id) + def _set_used_tips( # noqa: C901 + self, pipette_id: str, well_name: str, labware_id: str + ) -> None: columns = self._state.column_by_labware_id.get(labware_id, []) wells = self._state.tips_by_labware_id.get(labware_id, {}) - - if pipette_channels == len(wells): - for well_name in wells.keys(): - wells[well_name] = TipRackWellState.USED - - elif columns and pipette_channels == len(columns[0]): - for column in columns: - if well_name in column: - for well in column: + nozzle_map = self._state.nozzle_map_by_pipette_id[pipette_id] + + # TODO (cb, 02-28-2024): Transition from using partial nozzle map to full instrument map for the set used logic + num_nozzle_cols = len(nozzle_map.columns) + num_nozzle_rows = len(nozzle_map.rows) + + critical_column = 0 + critical_row = 0 + for column in columns: + if well_name in column: + critical_row = column.index(well_name) + critical_column = columns.index(column) + + for i in range(num_nozzle_cols): + for j in range(num_nozzle_rows): + if nozzle_map.starting_nozzle == "A1": + if (critical_column + i < len(columns)) and ( + critical_row + j < len(columns[critical_column]) + ): + well = columns[critical_column + i][critical_row + j] + wells[well] = TipRackWellState.USED + elif nozzle_map.starting_nozzle == "A12": + if (critical_column - i >= 0) and ( + critical_row + j < len(columns[critical_column]) + ): + well = columns[critical_column - i][critical_row + j] + wells[well] = TipRackWellState.USED + elif nozzle_map.starting_nozzle == "H1": + if (critical_column + i < len(columns)) and (critical_row - j >= 0): + well = columns[critical_column + i][critical_row - j] + wells[well] = TipRackWellState.USED + elif nozzle_map.starting_nozzle == "H12": + if (critical_column - i >= 0) and (critical_row - j >= 0): + well = columns[critical_column - i][critical_row - j] wells[well] = TipRackWellState.USED - break - - else: - wells[well_name] = TipRackWellState.USED class TipView(HasState[TipState]): @@ -151,50 +179,255 @@ def __init__(self, state: TipState) -> None: """ self._state = state - # TODO (spp, 2023-12-05): update this logic once we support partial nozzle configurations - # that require the tip tracking to move right to left or front to back; - # for example when using leftmost column config of 96-channel - # or backmost single nozzle configuration of an 8-channel. def get_next_tip( # noqa: C901 - self, labware_id: str, num_tips: int, starting_tip_name: Optional[str] + self, + labware_id: str, + num_tips: int, + starting_tip_name: Optional[str], + nozzle_map: Optional[NozzleMap], ) -> Optional[str]: - """Get the next available clean tip.""" + """Get the next available clean tip. Does not support use of a starting tip if the pipette used is in a partial configuration.""" wells = self._state.tips_by_labware_id.get(labware_id, {}) columns = self._state.column_by_labware_id.get(labware_id, []) - if columns and num_tips == len(columns[0]): # Get next tips for 8-channel - column_head = [column[0] for column in columns] - starting_column_index = 0 - - if starting_tip_name: - for idx, column in enumerate(columns): - if starting_tip_name in column: - if starting_tip_name not in column_head: - starting_column_index = idx + 1 + def _identify_tip_cluster( + active_columns: int, + active_rows: int, + critical_column: int, + critical_row: int, + entry_well: str, + ) -> Optional[List[str]]: + tip_cluster = [] + for i in range(active_columns): + if entry_well == "A1" or entry_well == "H1": + if critical_column - i >= 0: + column = columns[critical_column - i] + else: + return None + elif entry_well == "A12" or entry_well == "H12": + if critical_column + i < len(columns): + column = columns[critical_column + i] + else: + return None + else: + raise ValueError( + f"Invalid entry well {entry_well} for tip cluster identification." + ) + for j in range(active_rows): + if entry_well == "A1" or entry_well == "A12": + if critical_row - j >= 0: + well = column[critical_row - j] else: - starting_column_index = idx - - for column in columns[starting_column_index:]: - if not any(wells[well] == TipRackWellState.USED for well in column): - return column[0] + return None + elif entry_well == "H1" or entry_well == "H12": + if critical_row + j < len(column): + well = column[critical_row + j] + else: + return None + tip_cluster.append(well) - elif num_tips == len(wells.keys()): # Get next tips for 96 channel - if starting_tip_name and starting_tip_name != columns[0][0]: + if any(well not in [*wells] for well in tip_cluster): return None - if not any( - tip_state == TipRackWellState.USED for tip_state in wells.values() - ): - return next(iter(wells)) - - else: # Get next tips for single channel - if starting_tip_name is not None: - wells = _drop_wells_before_starting_tip(wells, starting_tip_name) - - for well_name, tip_state in wells.items(): - if tip_state == TipRackWellState.CLEAN: - return well_name + return tip_cluster + def _validate_tip_cluster( + active_columns: int, active_rows: int, tip_cluster: List[str] + ) -> Union[str, int, None]: + if not any(wells[well] == TipRackWellState.USED for well in tip_cluster): + return tip_cluster[0] + elif all(wells[well] == TipRackWellState.USED for well in tip_cluster): + return None + else: + # The tip cluster list is ordered: Each row from a column in order by columns + tip_cluster_final_column = [] + for i in range(active_rows): + tip_cluster_final_column.append( + tip_cluster[((active_columns * active_rows) - 1) - i] + ) + tip_cluster_final_row = [] + for i in range(active_columns): + tip_cluster_final_row.append( + tip_cluster[(active_rows - 1) + (i * active_rows)] + ) + if all( + wells[well] == TipRackWellState.USED + for well in tip_cluster_final_column + ): + return None + elif all( + wells[well] == TipRackWellState.USED + for well in tip_cluster_final_row + ): + return None + else: + # Tiprack has no valid tip selection, cannot progress + return -1 + + # Search through the tiprack beginning at A1 + def _cluster_search_A1(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = active_columns - 1 + critical_row = active_rows - 1 + + while critical_column <= len(columns): + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "A1" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row + active_rows < len(columns[0]): + critical_row = critical_row + active_rows + else: + critical_column = critical_column + 1 + critical_row = active_rows - 1 + return None + + # Search through the tiprack beginning at A12 + def _cluster_search_A12(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = len(columns) - active_columns + critical_row = active_rows - 1 + + while critical_column >= 0: + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "A12" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row + active_rows < len(columns[0]): + critical_row = critical_row + active_rows + else: + critical_column = critical_column - 1 + critical_row = active_rows - 1 + return None + + # Search through the tiprack beginning at H1 + def _cluster_search_H1(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = active_columns - 1 + critical_row = len(columns[critical_column]) - active_rows + + while critical_column <= len(columns): # change to max size of labware + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "H1" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row - active_rows >= 0: + critical_row = critical_row - active_rows + else: + critical_column = critical_column + 1 + critical_row = len(columns[critical_column]) - active_rows + return None + + # Search through the tiprack beginning at H12 + def _cluster_search_H12(active_columns: int, active_rows: int) -> Optional[str]: + critical_column = len(columns) - active_columns + critical_row = len(columns[critical_column]) - active_rows + + while critical_column >= 0: + tip_cluster = _identify_tip_cluster( + active_columns, active_rows, critical_column, critical_row, "H12" + ) + if tip_cluster is not None: + result = _validate_tip_cluster( + active_columns, active_rows, tip_cluster + ) + if isinstance(result, str): + return result + elif isinstance(result, int) and result == -1: + return None + if critical_row - active_rows >= 0: + critical_row = critical_row - active_rows + else: + critical_column = critical_column - 1 + critical_row = len(columns[critical_column]) - active_rows + return None + + if starting_tip_name is None and nozzle_map is not None and columns: + num_channels = len(nozzle_map.full_instrument_map_store) + num_nozzle_cols = len(nozzle_map.columns) + num_nozzle_rows = len(nozzle_map.rows) + # Each pipette's cluster search is determined by the point of entry for a given pipette/configuration: + # - Single channel pipettes always search a tiprack top to bottom, left to right + # - Eight channel pipettes will begin at the top if the primary nozzle is H1 and at the bottom if + # it is A1. The eight channel will always progress across the columns left to right. + # - 96 Channel pipettes will begin in the corner opposite their primary/starting nozzle (if starting nozzle = A1, enter tiprack at H12) + # The 96 channel will then progress towards the opposite corner, either going up or down, left or right depending on configuration. + + if num_channels == 1: + return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) + elif num_channels == 8: + if nozzle_map.starting_nozzle == "A1": + return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "H1": + return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) + elif num_channels == 96: + if nozzle_map.starting_nozzle == "A1": + return _cluster_search_H12(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "A12": + return _cluster_search_H1(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "H1": + return _cluster_search_A12(num_nozzle_cols, num_nozzle_rows) + elif nozzle_map.starting_nozzle == "H12": + return _cluster_search_A1(num_nozzle_cols, num_nozzle_rows) + else: + raise ValueError( + f"Nozzle {nozzle_map.starting_nozzle} is an invalid starting tip for automatic tip pickup." + ) + else: + raise RuntimeError( + "Invalid number of channels for automatic tip tracking." + ) + else: + if columns and num_tips == len(columns[0]): # Get next tips for 8-channel + column_head = [column[0] for column in columns] + starting_column_index = 0 + + if starting_tip_name: + for idx, column in enumerate(columns): + if starting_tip_name in column: + if starting_tip_name not in column_head: + starting_column_index = idx + 1 + else: + starting_column_index = idx + + for column in columns[starting_column_index:]: + if not any(wells[well] == TipRackWellState.USED for well in column): + return column[0] + + elif num_tips == len(wells.keys()): # Get next tips for 96 channel + if starting_tip_name and starting_tip_name != columns[0][0]: + return None + + if not any( + tip_state == TipRackWellState.USED for tip_state in wells.values() + ): + return next(iter(wells)) + + else: # Get next tips for single channel + if starting_tip_name is not None: + wells = _drop_wells_before_starting_tip(wells, starting_tip_name) + + for well_name, tip_state in wells.items(): + if tip_state == TipRackWellState.CLEAN: + return well_name return None def get_pipette_channels(self, pipette_id: str) -> int: @@ -205,6 +438,10 @@ def get_pipette_active_channels(self, pipette_id: str) -> int: """Get the number of channels being used in the given pipette's configuration.""" return self._state.active_channels_by_pipette_id[pipette_id] + def get_pipette_nozzle_map(self, pipette_id: str) -> NozzleMap: + """Get the current nozzle map the given pipette's configuration.""" + return self._state.nozzle_map_by_pipette_id[pipette_id] + def has_clean_tip(self, labware_id: str, well_name: str) -> bool: """Get whether a well in a labware has a clean tip. diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 656f2263efc..d5b126542d4 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -35,6 +35,14 @@ class EngineStatus(str, Enum): FAILED = "failed" SUCCEEDED = "succeeded" + AWAITING_RECOVERY = "awaiting-recovery" + """The engine is waiting for external input to recover from a nonfatal error. + + New fixup commands may be enqueued, which will run immediately. + The run can't be paused in this state, but it can be canceled, or resumed from the + next protocol command if recovery is complete. + """ + class DeckSlotLocation(BaseModel): """The location of something placed in a single deck slot.""" @@ -748,7 +756,7 @@ class PostRunHardwareState(Enum): DISENGAGE_IN_PLACE = "disengageInPlace" -NOZZLE_NAME_REGEX = "[A-Z][0-100]" +NOZZLE_NAME_REGEX = r"[A-Z]\d{1,2}" PRIMARY_NOZZLE_LITERAL = Literal["A1", "H1", "A12", "H12"] @@ -837,3 +845,67 @@ def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": HwTipStateType.PRESENT: TipPresenceStatus.PRESENT, HwTipStateType.ABSENT: TipPresenceStatus.ABSENT, }[state] + + +class RTPBase(BaseModel): + """Parameters defined in a protocol.""" + + displayName: str = Field(..., description="Display string for the parameter.") + variableName: str = Field(..., description="Python variable name of the parameter.") + description: str = Field(..., description="Detailed description of the parameter.") + suffix: Optional[str] = Field( + None, + description="Units (like mL, mm/sec, etc) or a custom suffix for the parameter.", + ) + + +class IntParameter(RTPBase): + """An integer parameter defined in a protocol.""" + + min: int = Field( + ..., description="Minimum value that the integer param is allowed to have." + ) + max: int = Field( + ..., description="Maximum value that the integer param is allowed to have." + ) + default: int = Field( + ..., + description="Default value of the parameter, to be used when there is no client-specified value.", + ) + + +class FloatParameter(RTPBase): + """A float parameter defined in a protocol.""" + + min: float = Field( + ..., description="Minimum value that the float param is allowed to have." + ) + max: float = Field( + ..., description="Maximum value that the float param is allowed to have." + ) + default: float = Field( + ..., + description="Default value of the parameter, to be used when there is no client-specified value.", + ) + + +class EnumChoice(BaseModel): + """Components of choices used in RTP Enum Parameters.""" + + displayName: str = Field(..., description="Display string for the param's choice.") + value: str = Field(..., description="Enum value of the param's choice.") + + +class EnumParameter(RTPBase): + """A string enum defined in a protocol.""" + + choices: List[EnumChoice] = Field( + ..., description="List of valid choices for this parameter." + ) + default: str = Field( + ..., + description="Default value of the parameter, to be used when there is no client-specified value.", + ) + + +RunTimeParameter = Union[IntParameter, FloatParameter, EnumParameter] diff --git a/api/tests/opentrons/config/test_advanced_settings_migration.py b/api/tests/opentrons/config/test_advanced_settings_migration.py index 1070654e14d..4e88e28f262 100644 --- a/api/tests/opentrons/config/test_advanced_settings_migration.py +++ b/api/tests/opentrons/config/test_advanced_settings_migration.py @@ -8,7 +8,7 @@ @pytest.fixture def migrated_file_version() -> int: - return 30 + return 31 # make sure to set a boolean value in default_file_settings only if @@ -29,6 +29,7 @@ def default_file_settings() -> Dict[str, Any]: "disableStatusBar": None, "disableOverpressureDetection": None, "estopNotRequired": None, + "enableErrorRecoveryExperiments": None, } @@ -366,6 +367,18 @@ def v30_config(v29_config: Dict[str, Any]) -> Dict[str, Any]: return r +@pytest.fixture +def v31_config(v30_config: Dict[str, Any]) -> Dict[str, Any]: + r = v30_config.copy() + r.update( + { + "_version": 31, + "enableErrorRecoveryExperiments": None, + } + ) + return r + + @pytest.fixture( scope="session", params=[ @@ -401,6 +414,7 @@ def v30_config(v29_config: Dict[str, Any]) -> Dict[str, Any]: lazy_fixture("v28_config"), lazy_fixture("v29_config"), lazy_fixture("v30_config"), + lazy_fixture("v31_config"), ], ) def old_settings(request: SubRequest) -> Dict[str, Any]: @@ -492,4 +506,5 @@ def test_ensures_config() -> None: "disableStatusBar": None, "estopNotRequired": None, "disableOverpressureDetection": None, + "enableErrorRecoveryExperiments": None, } diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 0b5a0f26a47..3b296067a0d 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -1139,11 +1139,11 @@ def test_configure_nozzle_layout( argvalues=[ (96, NozzleConfigurationType.FULL, "A1", True), (96, NozzleConfigurationType.FULL, None, True), - (96, NozzleConfigurationType.ROW, "A1", False), - (96, NozzleConfigurationType.COLUMN, "A1", False), + (96, NozzleConfigurationType.ROW, "A1", True), + (96, NozzleConfigurationType.COLUMN, "A1", True), (96, NozzleConfigurationType.COLUMN, "A12", True), - (96, NozzleConfigurationType.SINGLE, "H12", False), - (96, NozzleConfigurationType.SINGLE, "A1", False), + (96, NozzleConfigurationType.SINGLE, "H12", True), + (96, NozzleConfigurationType.SINGLE, "A1", True), (8, NozzleConfigurationType.FULL, "A1", True), (8, NozzleConfigurationType.FULL, None, True), (8, NozzleConfigurationType.SINGLE, "H1", True), diff --git a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py index 5f84df6f62c..37d4511cce0 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_labware_core.py @@ -249,13 +249,16 @@ def test_get_next_tip( labware_id="cool-labware", num_tips=8, starting_tip_name="B1", + nozzle_map=None, ) ).then_return("A2") starting_tip = WellCore( name="B1", labware_id="cool-labware", engine_client=mock_engine_client ) - result = subject.get_next_tip(num_tips=8, starting_tip=starting_tip) + result = subject.get_next_tip( + num_tips=8, starting_tip=starting_tip, nozzle_map=None + ) assert result == "A2" diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 239d61c9d95..38ab8f5b54b 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -1,4 +1,5 @@ """Tests for the InstrumentContext public interface.""" +from collections import OrderedDict import inspect import pytest @@ -29,6 +30,8 @@ from opentrons.protocol_api.core.legacy.legacy_instrument_core import ( LegacyInstrumentCore, ) + +from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout from opentrons.types import Location, Mount, Point @@ -505,8 +508,25 @@ def test_blow_out_raises_no_location( subject.blow_out(location=None) +MOCK_MAP = NozzleMap.build( + physical_nozzles=OrderedDict({"A1": Point(0, 0, 0)}), + physical_rows=OrderedDict({"A": ["A1"]}), + physical_columns=OrderedDict({"1": ["A1"]}), + starting_nozzle="A1", + back_left_nozzle="A1", + front_right_nozzle="A1", +) + + +@pytest.mark.parametrize( + argnames=["api_version", "mock_map"], + argvalues=[(APIVersion(2, 18), MOCK_MAP), (APIVersion(2, 17), None)], +) def test_pick_up_tip_from_labware( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_map: Optional[NozzleMap], ) -> None: """It should pick up the next tip from a given labware.""" mock_tip_rack = decoy.mock(cls=Labware) @@ -514,11 +534,13 @@ def test_pick_up_tip_from_labware( top_location = Location(point=Point(1, 2, 3), labware=mock_well) decoy.when(mock_instrument_core.get_active_channels()).then_return(123) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) decoy.when( labware.next_available_tip( starting_tip=None, tip_racks=[mock_tip_rack], channels=123, + nozzle_map=mock_map, ) ).then_return((mock_tip_rack, mock_well)) decoy.when(mock_well.top()).then_return(top_location) @@ -558,8 +580,15 @@ def test_pick_up_tip_from_well_location( ) +@pytest.mark.parametrize( + argnames=["api_version", "mock_map"], + argvalues=[(APIVersion(2, 18), MOCK_MAP), (APIVersion(2, 17), None)], +) def test_pick_up_tip_from_labware_location( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_map: Optional[NozzleMap], ) -> None: """It should pick up the next tip from a given labware-based Location.""" mock_tip_rack = decoy.mock(cls=Labware) @@ -568,11 +597,13 @@ def test_pick_up_tip_from_labware_location( top_location = Location(point=Point(1, 2, 3), labware=mock_well) decoy.when(mock_instrument_core.get_active_channels()).then_return(123) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) decoy.when( labware.next_available_tip( starting_tip=None, tip_racks=[mock_tip_rack], channels=123, + nozzle_map=mock_map, ) ).then_return((mock_tip_rack, mock_well)) decoy.when(mock_well.top()).then_return(top_location) @@ -591,10 +622,17 @@ def test_pick_up_tip_from_labware_location( ) +@pytest.mark.parametrize( + argnames=["api_version", "mock_map"], + argvalues=[(APIVersion(2, 18), MOCK_MAP), (APIVersion(2, 17), None)], +) def test_pick_up_from_associated_tip_racks( - decoy: Decoy, mock_instrument_core: InstrumentCore, subject: InstrumentContext + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_map: Optional[NozzleMap], ) -> None: - """It should pick up from it associated tip racks.""" + """It should pick up from its associated tip racks.""" mock_tip_rack_1 = decoy.mock(cls=Labware) mock_tip_rack_2 = decoy.mock(cls=Labware) mock_starting_tip = decoy.mock(cls=Well) @@ -603,11 +641,13 @@ def test_pick_up_from_associated_tip_racks( decoy.when(mock_instrument_core.is_tip_tracking_available()).then_return(True) decoy.when(mock_instrument_core.get_active_channels()).then_return(123) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) decoy.when( labware.next_available_tip( starting_tip=mock_starting_tip, tip_racks=[mock_tip_rack_1, mock_tip_rack_2], channels=123, + nozzle_map=mock_map, ) ).then_return((mock_tip_rack_2, mock_well)) decoy.when(mock_well.top()).then_return(top_location) diff --git a/api/tests/opentrons/protocol_api_old/test_labware.py b/api/tests/opentrons/protocol_api_old/test_labware.py index c72c8a87346..8f6f1da267b 100644 --- a/api/tests/opentrons/protocol_api_old/test_labware.py +++ b/api/tests/opentrons/protocol_api_old/test_labware.py @@ -544,7 +544,10 @@ def test_tiprack_list(): core_map=None, # type: ignore[arg-type] ) - assert labware.select_tiprack_from_list([tiprack], 1) == (tiprack, tiprack["A1"]) + assert labware.select_tiprack_from_list([tiprack], 1) == ( + tiprack, + tiprack["A1"], + ) assert labware.select_tiprack_from_list([tiprack], 1, tiprack.wells()[1]) == ( tiprack, diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 178f118cc50..f625c19f93f 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -19,6 +19,7 @@ ) from opentrons.protocol_engine.types import CurrentWell, LoadedPipette from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.notes import CommandNoteAdder @pytest.fixture @@ -27,6 +28,7 @@ def subject( hardware_api: HardwareControlAPI, movement: MovementHandler, pipetting: PipettingHandler, + mock_command_note_adder: CommandNoteAdder, ) -> AspirateImplementation: """Get the implementation subject.""" return AspirateImplementation( @@ -34,6 +36,7 @@ def subject( state_view=state_view, movement=movement, hardware_api=hardware_api, + command_note_adder=mock_command_note_adder, ) @@ -44,6 +47,7 @@ async def test_aspirate_implementation_no_prep( movement: MovementHandler, pipetting: PipettingHandler, subject: AspirateImplementation, + mock_command_note_adder: CommandNoteAdder, ) -> None: """An Aspirate should have an execution implementation without preparing to aspirate.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) @@ -70,7 +74,12 @@ async def test_aspirate_implementation_no_prep( ).then_return(Point(x=1, y=2, z=3)) decoy.when( - await pipetting.aspirate_in_place(pipette_id="abc", volume=50, flow_rate=1.23), + await pipetting.aspirate_in_place( + pipette_id="abc", + volume=50, + flow_rate=1.23, + command_note_adder=mock_command_note_adder, + ), ).then_return(50) result = await subject.execute(data) @@ -84,6 +93,7 @@ async def test_aspirate_implementation_with_prep( hardware_api: HardwareControlAPI, movement: MovementHandler, pipetting: PipettingHandler, + mock_command_note_adder: CommandNoteAdder, subject: AspirateImplementation, ) -> None: """An Aspirate should have an execution implementation with preparing to aspirate.""" @@ -120,7 +130,12 @@ async def test_aspirate_implementation_with_prep( ).then_return(Point(x=1, y=2, z=3)) decoy.when( - await pipetting.aspirate_in_place(pipette_id="abc", volume=50, flow_rate=1.23), + await pipetting.aspirate_in_place( + pipette_id="abc", + volume=50, + flow_rate=1.23, + command_note_adder=mock_command_note_adder, + ), ).then_return(50) result = await subject.execute(data) @@ -139,7 +154,10 @@ async def test_aspirate_implementation_with_prep( async def test_aspirate_raises_volume_error( - decoy: Decoy, pipetting: PipettingHandler, subject: AspirateImplementation + decoy: Decoy, + pipetting: PipettingHandler, + mock_command_note_adder: CommandNoteAdder, + subject: AspirateImplementation, ) -> None: """Should raise an assertion error for volume larger than working volume.""" location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) @@ -156,7 +174,12 @@ async def test_aspirate_raises_volume_error( decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) decoy.when( - await pipetting.aspirate_in_place(pipette_id="abc", volume=50, flow_rate=1.23) + await pipetting.aspirate_in_place( + pipette_id="abc", + volume=50, + flow_rate=1.23, + command_note_adder=mock_command_note_adder, + ) ).then_raise(AssertionError("blah blah")) with pytest.raises(AssertionError): diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 26a39b9001f..3d09c029bcd 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -11,6 +11,7 @@ AspirateInPlaceImplementation, ) from opentrons.protocol_engine.errors.exceptions import PipetteNotReadyToAspirateError +from opentrons.protocol_engine.notes import CommandNoteAdder from opentrons.protocol_engine.state import ( StateStore, @@ -40,12 +41,14 @@ def subject( pipetting: PipettingHandler, state_store: StateStore, hardware_api: HardwareAPI, + mock_command_note_adder: CommandNoteAdder, ) -> AspirateInPlaceImplementation: """Get the impelementation subject.""" return AspirateInPlaceImplementation( pipetting=pipetting, hardware_api=hardware_api, state_view=state_store, + command_note_adder=mock_command_note_adder, ) @@ -54,6 +57,7 @@ async def test_aspirate_in_place_implementation( pipetting: PipettingHandler, state_store: StateStore, hardware_api: HardwareAPI, + mock_command_note_adder: CommandNoteAdder, subject: AspirateInPlaceImplementation, ) -> None: """It should aspirate in place.""" @@ -71,7 +75,10 @@ async def test_aspirate_in_place_implementation( decoy.when( await pipetting.aspirate_in_place( - pipette_id="pipette-id-abc", volume=123, flow_rate=1.234 + pipette_id="pipette-id-abc", + volume=123, + flow_rate=1.234, + command_note_adder=mock_command_note_adder, ) ).then_return(123) @@ -110,7 +117,10 @@ async def test_handle_aspirate_in_place_request_not_ready_to_aspirate( async def test_aspirate_raises_volume_error( - decoy: Decoy, pipetting: PipettingHandler, subject: AspirateInPlaceImplementation + decoy: Decoy, + pipetting: PipettingHandler, + subject: AspirateInPlaceImplementation, + mock_command_note_adder: CommandNoteAdder, ) -> None: """Should raise an assertion error for volume larger than working volume.""" data = AspirateInPlaceParams( @@ -122,7 +132,12 @@ async def test_aspirate_raises_volume_error( decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) decoy.when( - await pipetting.aspirate_in_place(pipette_id="abc", volume=50, flow_rate=1.23) + await pipetting.aspirate_in_place( + pipette_id="abc", + volume=50, + flow_rate=1.23, + command_note_adder=mock_command_note_adder, + ) ).then_raise(AssertionError("blah blah")) with pytest.raises(AssertionError): diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index d703a964078..dfd59089c2d 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -21,6 +21,7 @@ from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI from opentrons.hardware_control.api import API from opentrons.hardware_control.protocols.types import FlexRobotType, OT2RobotType +from opentrons.protocol_engine.notes import CommandNoteAdder if TYPE_CHECKING: from opentrons.hardware_control.ot3api import OT3API @@ -230,3 +231,9 @@ def supported_tip_fixture() -> pipette_definition.SupportedTipsDefinition: dispense=pipette_definition.ulPerMMDefinition(default={"1": [(0, 0, 0)]}), defaultPushOutVolume=3, ) + + +@pytest.fixture +def mock_command_note_adder(decoy: Decoy) -> CommandNoteAdder: + """Get a command note adder.""" + return decoy.mock(cls=CommandNoteAdder) diff --git a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py index 50c54eceacf..961bfa3ac54 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_command_executor.py +++ b/api/tests/opentrons/protocol_engine/execution/test_command_executor.py @@ -40,8 +40,12 @@ RailLightsHandler, StatusBarHandler, ) +from opentrons.protocol_engine.execution.command_executor import ( + CommandNoteTrackerProvider, +) from opentrons_shared_data.errors.exceptions import EStopActivatedError, PythonException +from opentrons.protocol_engine.notes import CommandNoteTracker, CommandNote @pytest.fixture @@ -122,6 +126,33 @@ def status_bar(decoy: Decoy) -> StatusBarHandler: return decoy.mock(cls=StatusBarHandler) +@pytest.fixture +def command_note_tracker_provider(decoy: Decoy) -> CommandNoteTrackerProvider: + """Get a mock tracker provider.""" + return decoy.mock(cls=CommandNoteTrackerProvider) + + +def get_next_tracker( + decoy: Decoy, provider: CommandNoteTrackerProvider +) -> CommandNoteTracker: + """Get the next tracker provided by a provider, in code without being a fixture. + + This is useful for testing the execution of multiple commands, each of which will get + a different tracker instance. + """ + new_tracker = decoy.mock(cls=CommandNoteTracker) + decoy.when(provider()).then_return(new_tracker) + return new_tracker + + +@pytest.fixture +def command_note_tracker( + decoy: Decoy, command_note_tracker_provider: CommandNoteTrackerProvider +) -> CommandNoteTracker: + """Get the tracker that the provider will provide.""" + return get_next_tracker(decoy, command_note_tracker_provider) + + @pytest.fixture def subject( hardware_api: HardwareControlAPI, @@ -137,6 +168,7 @@ def subject( rail_lights: RailLightsHandler, status_bar: StatusBarHandler, model_utils: ModelUtils, + command_note_tracker_provider: CommandNoteTrackerProvider, ) -> CommandExecutor: """Get a CommandExecutor test subject with its dependencies mocked out.""" return CommandExecutor( @@ -153,6 +185,7 @@ def subject( model_utils=model_utils, rail_lights=rail_lights, status_bar=status_bar, + command_note_tracker_provider=command_note_tracker_provider, ) @@ -184,6 +217,7 @@ async def test_execute( rail_lights: RailLightsHandler, status_bar: StatusBarHandler, model_utils: ModelUtils, + command_note_tracker: CommandNoteTracker, subject: CommandExecutor, ) -> None: """It should be able to execute a command.""" @@ -256,6 +290,7 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: run_control=run_control, rail_lights=rail_lights, status_bar=status_bar, + command_note_adder=command_note_tracker, ) ).then_return( command_impl # type: ignore[arg-type] @@ -321,6 +356,7 @@ async def test_execute_raises_protocol_engine_error( status_bar: StatusBarHandler, model_utils: ModelUtils, subject: CommandExecutor, + command_note_tracker: CommandNoteTracker, command_error: Exception, expected_error: Any, unexpected_error: bool, @@ -380,6 +416,7 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: run_control=run_control, rail_lights=rail_lights, status_bar=status_bar, + command_note_adder=command_note_tracker, ) ).then_return( command_impl # type: ignore[arg-type] @@ -408,3 +445,246 @@ def _ImplementationCls(self) -> Type[_TestCommandImpl]: ) ), ) + + +async def test_executor_forwards_notes_on_command_success( + decoy: Decoy, + hardware_api: HardwareControlAPI, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + equipment: EquipmentHandler, + movement: MovementHandler, + mock_gantry_mover: GantryMover, + labware_movement: LabwareMovementHandler, + pipetting: PipettingHandler, + mock_tip_handler: TipHandler, + run_control: RunControlHandler, + rail_lights: RailLightsHandler, + status_bar: StatusBarHandler, + model_utils: ModelUtils, + command_note_tracker: CommandNoteTracker, + subject: CommandExecutor, +) -> None: + """It should be able to add notes during OK execution to command updates.""" + TestCommandImplCls = decoy.mock(func=_TestCommandImpl) + command_impl = decoy.mock(cls=_TestCommandImpl) + + class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): + commandType: str = "testCommand" + params: _TestCommandParams + result: Optional[_TestCommandResult] + + @property + def _ImplementationCls(self) -> Type[_TestCommandImpl]: + return TestCommandImplCls + + command_params = _TestCommandParams() + command_result = _TestCommandResult() + + queued_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + status=CommandStatus.QUEUED, + params=command_params, + ), + ) + + command_notes = [ + CommandNote( + noteKind="warning", + shortMessage="hello", + longMessage="test command note", + source="test", + ) + ] + + running_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + startedAt=datetime(year=2022, month=2, day=2), + status=CommandStatus.RUNNING, + params=command_params, + ), + ) + + completed_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + startedAt=datetime(year=2022, month=2, day=2), + completedAt=datetime(year=2023, month=3, day=3), + status=CommandStatus.SUCCEEDED, + params=command_params, + result=command_result, + notes=command_notes, + ), + ) + + decoy.when(state_store.commands.get(command_id="command-id")).then_return( + queued_command + ) + + decoy.when( + queued_command._ImplementationCls( + state_view=state_store, + hardware_api=hardware_api, + equipment=equipment, + movement=movement, + gantry_mover=mock_gantry_mover, + labware_movement=labware_movement, + pipetting=pipetting, + tip_handler=mock_tip_handler, + run_control=run_control, + rail_lights=rail_lights, + status_bar=status_bar, + command_note_adder=command_note_tracker, + ) + ).then_return( + command_impl # type: ignore[arg-type] + ) + + decoy.when(await command_impl.execute(command_params)).then_return(command_result) + + decoy.when(model_utils.get_timestamp()).then_return( + datetime(year=2022, month=2, day=2), + datetime(year=2023, month=3, day=3), + ) + decoy.when(command_note_tracker.get_notes()).then_return(command_notes) + + await subject.execute("command-id") + + decoy.verify( + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=running_command) + ), + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=completed_command) + ), + ) + + +async def test_executor_forwards_notes_on_command_failure( + decoy: Decoy, + hardware_api: HardwareControlAPI, + state_store: StateStore, + action_dispatcher: ActionDispatcher, + equipment: EquipmentHandler, + movement: MovementHandler, + mock_gantry_mover: GantryMover, + labware_movement: LabwareMovementHandler, + pipetting: PipettingHandler, + mock_tip_handler: TipHandler, + run_control: RunControlHandler, + rail_lights: RailLightsHandler, + status_bar: StatusBarHandler, + model_utils: ModelUtils, + subject: CommandExecutor, + command_note_tracker: CommandNoteTracker, +) -> None: + """It should handle an error occuring during execution.""" + TestCommandImplCls = decoy.mock(func=_TestCommandImpl) + command_impl = decoy.mock(cls=_TestCommandImpl) + + class _TestCommand(BaseCommand[_TestCommandParams, _TestCommandResult]): + commandType: str = "testCommand" + params: _TestCommandParams + result: Optional[_TestCommandResult] + + @property + def _ImplementationCls(self) -> Type[_TestCommandImpl]: + return TestCommandImplCls + + command_params = _TestCommandParams() + command_notes = [ + CommandNote( + noteKind="warning", + shortMessage="hello", + longMessage="test command note", + source="test", + ) + ] + + queued_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + status=CommandStatus.QUEUED, + params=command_params, + ), + ) + + running_command = cast( + Command, + _TestCommand( + id="command-id", + key="command-key", + createdAt=datetime(year=2021, month=1, day=1), + startedAt=datetime(year=2022, month=2, day=2), + status=CommandStatus.RUNNING, + params=command_params, + ), + ) + running_command_with_notes = running_command.copy(update={"notes": command_notes}) + + decoy.when(state_store.commands.get(command_id="command-id")).then_return( + queued_command + ) + + decoy.when( + queued_command._ImplementationCls( + state_view=state_store, + hardware_api=hardware_api, + equipment=equipment, + movement=movement, + gantry_mover=mock_gantry_mover, + labware_movement=labware_movement, + pipetting=pipetting, + tip_handler=mock_tip_handler, + run_control=run_control, + rail_lights=rail_lights, + status_bar=status_bar, + command_note_adder=command_note_tracker, + ) + ).then_return( + command_impl # type: ignore[arg-type] + ) + + decoy.when(await command_impl.execute(command_params)).then_raise( + RuntimeError("oh no") + ) + + decoy.when(model_utils.generate_id()).then_return("error-id") + decoy.when(model_utils.get_timestamp()).then_return( + datetime(year=2022, month=2, day=2), + datetime(year=2023, month=3, day=3), + ) + decoy.when(command_note_tracker.get_notes()).then_return(command_notes) + + await subject.execute("command-id") + + decoy.verify( + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=running_command) + ), + action_dispatcher.dispatch( + UpdateCommandAction(private_result=None, command=running_command_with_notes) + ), + action_dispatcher.dispatch( + FailCommandAction( + command_id="command-id", + error_id="error-id", + failed_at=datetime(year=2023, month=3, day=3), + error=matchers.ErrorMatching(PythonException, match="oh no"), + ) + ), + ) diff --git a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py index bcb61324ad0..b087084abff 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_pipetting_handler.py @@ -21,6 +21,8 @@ InvalidPushOutVolumeError, InvalidDispenseVolumeError, ) +from opentrons.protocol_engine.notes import CommandNoteAdder, CommandNote +from ..note_utils import CommandNoteMatcher @pytest.fixture @@ -217,6 +219,7 @@ async def test_hw_aspirate_in_place( mock_state_view: StateView, mock_hardware_api: HardwareAPI, hardware_subject: HardwarePipettingHandler, + mock_command_note_adder: CommandNoteAdder, ) -> None: """Should set flow_rate and call hardware_api aspirate.""" decoy.when(mock_state_view.pipettes.get_working_volume("pipette-id")).then_return( @@ -247,7 +250,10 @@ async def test_hw_aspirate_in_place( ) result = await hardware_subject.aspirate_in_place( - pipette_id="pipette-id", volume=25, flow_rate=2.5 + pipette_id="pipette-id", + volume=25, + flow_rate=2.5, + command_note_adder=mock_command_note_adder, ) assert result == 25 @@ -324,7 +330,7 @@ def test_virtual_get_is_ready_to_aspirate( async def test_virtual_aspirate_in_place( - mock_state_view: StateView, decoy: Decoy + mock_state_view: StateView, decoy: Decoy, mock_command_note_adder: CommandNoteAdder ) -> None: """Should return the volume.""" decoy.when( @@ -342,7 +348,10 @@ async def test_virtual_aspirate_in_place( ) result = await subject.aspirate_in_place( - pipette_id="pipette-id", volume=2, flow_rate=5 + pipette_id="pipette-id", + volume=2, + flow_rate=5, + command_note_adder=mock_command_note_adder, ) assert result == 2 @@ -408,7 +417,7 @@ async def test_virtual_dispense_in_place_raises_no_tip( async def test_virtual_aspirate_validate_tip_attached( - mock_state_view: StateView, decoy: Decoy + mock_state_view: StateView, decoy: Decoy, mock_command_note_adder: CommandNoteAdder ) -> None: """Should raise an error that a tip is not attached.""" subject = VirtualPipettingHandler(state_view=mock_state_view) @@ -420,7 +429,12 @@ async def test_virtual_aspirate_validate_tip_attached( with pytest.raises( TipNotAttachedError, match="Cannot perform aspirate without a tip attached" ): - await subject.aspirate_in_place("pipette-id", volume=20, flow_rate=1) + await subject.aspirate_in_place( + "pipette-id", + volume=20, + flow_rate=1, + command_note_adder=mock_command_note_adder, + ) async def test_virtual_dispense_validate_tip_attached( @@ -446,6 +460,7 @@ async def test_aspirate_volume_validation( mock_state_view: StateView, mock_hardware_api: HardwareAPI, hardware_subject: HardwarePipettingHandler, + mock_command_note_adder: CommandNoteAdder, ) -> None: """It should validate the input volume, possibly adjusting it for rounding error. @@ -490,13 +505,30 @@ async def test_aspirate_volume_validation( for subject in [virtual_subject, hardware_subject]: assert ( await subject.aspirate_in_place( - pipette_id="pipette-id", volume=ok_volume, flow_rate=1 + pipette_id="pipette-id", + volume=ok_volume, + flow_rate=1, + command_note_adder=mock_command_note_adder, ) == expected_adjusted_volume ) + decoy.verify( + mock_command_note_adder( + cast( + CommandNote, + CommandNoteMatcher( + matching_noteKind_regex="warning", + matching_shortMessage_regex="Aspirate clamped to 1 µL", + ), + ) + ) + ) with pytest.raises(InvalidAspirateVolumeError): await subject.aspirate_in_place( - pipette_id="pipette-id", volume=not_ok_volume, flow_rate=1 + pipette_id="pipette-id", + volume=not_ok_volume, + flow_rate=1, + command_note_adder=mock_command_note_adder, ) diff --git a/api/tests/opentrons/protocol_engine/note_utils.py b/api/tests/opentrons/protocol_engine/note_utils.py new file mode 100644 index 00000000000..0ca3af9ccca --- /dev/null +++ b/api/tests/opentrons/protocol_engine/note_utils.py @@ -0,0 +1,63 @@ +"""Test utilities for dealing with notes.""" +import re +from typing import Optional +from opentrons.protocol_engine.notes import CommandNote + + +class CommandNoteMatcher: + """Decoy matcher for notes instances.""" + + def __init__( + self, + matching_noteKind_regex: Optional[str] = None, + matching_shortMessage_regex: Optional[str] = None, + matching_longMessage_regex: Optional[str] = None, + matching_source_regex: Optional[str] = None, + ) -> None: + """Build a CommandNoteMatcher. All provided arguments are checked with re.search.""" + self._matching_noteKind_regex = ( + re.compile(matching_noteKind_regex) + if matching_noteKind_regex is not None + else None + ) + self._matching_shortMessage_regex = ( + re.compile(matching_shortMessage_regex) + if matching_shortMessage_regex is not None + else None + ) + self._matching_longMessage_regex = ( + re.compile(matching_longMessage_regex) + if matching_longMessage_regex is not None + else None + ) + self._matching_source_regex = ( + re.compile(matching_source_regex) + if matching_source_regex is not None + else None + ) + + def __eq__(self, other: object) -> bool: + """Called by Decoy. returns True on a match, False otherwise.""" + if not isinstance(other, CommandNote): + return False + if ( + self._matching_noteKind_regex is not None + and not self._matching_noteKind_regex.search(other.noteKind) + ): + return False + if ( + self._matching_shortMessage_regex is not None + and not self._matching_shortMessage_regex.search(other.shortMessage) + ): + return False + if ( + self._matching_longMessage_regex is not None + and not self._matching_longMessage_regex.search(other.longMessage) + ): + return False + if ( + self._matching_source_regex is not None + and not self._matching_source_regex.search(other.source) + ): + return False + return True diff --git a/api/tests/opentrons/protocol_engine/pipette_fixtures.py b/api/tests/opentrons/protocol_engine/pipette_fixtures.py index 26c2ed33448..70937beeb9f 100644 --- a/api/tests/opentrons/protocol_engine/pipette_fixtures.py +++ b/api/tests/opentrons/protocol_engine/pipette_fixtures.py @@ -331,7 +331,7 @@ def get_default_nozzle_map(pipette_type: PipetteNameType) -> NozzleMap: physical_columns=EIGHT_CHANNEL_COLS, starting_nozzle="A1", back_left_nozzle="A1", - front_right_nozzle="A1", + front_right_nozzle="H1", ) elif "96" in pipette_type.value: return NozzleMap.build( @@ -340,7 +340,7 @@ def get_default_nozzle_map(pipette_type: PipetteNameType) -> NozzleMap: physical_columns=NINETY_SIX_COLS, starting_nozzle="A1", back_left_nozzle="A1", - front_right_nozzle="A1", + front_right_nozzle="H12", ) else: return NozzleMap.build( diff --git a/api/tests/opentrons/protocol_engine/state/test_command_view.py b/api/tests/opentrons/protocol_engine/state/test_command_view.py index 82fb21dc1f1..034e1276063 100644 --- a/api/tests/opentrons/protocol_engine/state/test_command_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_command_view.py @@ -683,6 +683,15 @@ def test_get_okay_to_clear(subject: CommandView, expected_is_okay: bool) -> None assert subject.get_is_okay_to_clear() is expected_is_okay +def test_get_running_command_id() -> None: + """It should return the running command ID.""" + subject_with_running = get_command_view(running_command_id="command-id") + assert subject_with_running.get_running_command_id() == "command-id" + + subject_without_running = get_command_view(running_command_id=None) + assert subject_without_running.get_running_command_id() is None + + def test_get_current() -> None: """It should return the "current" command.""" subject = get_command_view( @@ -851,7 +860,7 @@ def test_get_slice_default_cursor_running() -> None: def test_get_slice_default_cursor_queued() -> None: - """It should select a cursor based on the next queued command, if present.""" + """It should select a cursor automatically.""" command_1 = create_succeeded_command(command_id="command-id-1") command_2 = create_succeeded_command(command_id="command-id-2") command_3 = create_succeeded_command(command_id="command-id-3") @@ -861,7 +870,7 @@ def test_get_slice_default_cursor_queued() -> None: subject = get_command_view( commands=[command_1, command_2, command_3, command_4, command_5], running_command_id=None, - queued_command_ids=["command-id-4", "command-id-4", "command-id-5"], + queued_command_ids=[command_4.id, command_5.id], ) result = subject.get_slice(cursor=None, length=2) diff --git a/api/tests/opentrons/protocol_engine/state/test_tip_state.py b/api/tests/opentrons/protocol_engine/state/test_tip_state.py index a164656aeca..3f4ff0cf860 100644 --- a/api/tests/opentrons/protocol_engine/state/test_tip_state.py +++ b/api/tests/opentrons/protocol_engine/state/test_tip_state.py @@ -115,17 +115,51 @@ def drop_tip_in_place_command() -> commands.DropTipInPlace: ], ) def test_get_next_tip_returns_none( - load_labware_command: commands.LoadLabware, subject: TipStore + load_labware_command: commands.LoadLabware, + subject: TipStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=96, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=1, starting_tip_name=None, + nozzle_map=None, ) assert result is None @@ -133,17 +167,59 @@ def test_get_next_tip_returns_none( @pytest.mark.parametrize("input_tip_amount", [1, 8, 96]) def test_get_next_tip_returns_first_tip( - load_labware_command: commands.LoadLabware, subject: TipStore, input_tip_amount: int + load_labware_command: commands.LoadLabware, + subject: TipStore, + input_tip_amount: int, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should start at the first tip in the labware.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + pipette_name_type = PipetteNameType.P1000_96 + if input_tip_amount == 1: + pipette_name_type = PipetteNameType.P300_SINGLE_GEN2 + elif input_tip_amount == 8: + pipette_name_type = PipetteNameType.P300_MULTI_GEN2 + else: + pipette_name_type = PipetteNameType.P1000_96 + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=input_tip_amount, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(pipette_name_type), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=input_tip_amount, starting_tip_name=None, + nozzle_map=None, ) assert result == "A1" @@ -155,16 +231,49 @@ def test_get_next_tip_used_starting_tip( subject: TipStore, input_tip_amount: int, result_well_name: str, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should start searching at the given starting tip.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=input_tip_amount, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=input_tip_amount, starting_tip_name="B1", + nozzle_map=None, ) assert result == result_well_name @@ -201,11 +310,29 @@ def test_get_next_tip_skips_picked_up_tip( load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] result=commands.LoadPipetteResult(pipetteId="pipette-id") ) + channels_num = input_tip_amount + if input_starting_tip is not None: + pipette_name_type = PipetteNameType.P1000_96 + if input_tip_amount == 1: + pipette_name_type = PipetteNameType.P300_SINGLE_GEN2 + elif input_tip_amount == 8: + pipette_name_type = PipetteNameType.P300_MULTI_GEN2 + else: + pipette_name_type = PipetteNameType.P1000_96 + else: + channels_num = get_next_tip_tips + pipette_name_type = PipetteNameType.P1000_96 + if get_next_tip_tips == 1: + pipette_name_type = PipetteNameType.P300_SINGLE_GEN2 + elif get_next_tip_tips == 8: + pipette_name_type = PipetteNameType.P300_MULTI_GEN2 + else: + pipette_name_type = PipetteNameType.P1000_96 load_pipette_private_result = commands.LoadPipettePrivateResult( pipette_id="pipette-id", serial_number="pipette-serial", config=LoadedStaticPipetteData( - channels=input_tip_amount, + channels=channels_num, max_volume=15, min_volume=3, model="gen a", @@ -219,9 +346,9 @@ def test_get_next_tip_skips_picked_up_tip( nominal_tip_overlap={}, nozzle_offset_z=1.23, home_position=4.56, - nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), - back_left_corner_offset=Point(x=1, y=2, z=3), - front_right_corner_offset=Point(x=4, y=5, z=6), + nozzle_map=get_default_nozzle_map(pipette_name_type), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), ), ) subject.handle_action( @@ -237,6 +364,7 @@ def test_get_next_tip_skips_picked_up_tip( labware_id="cool-labware", num_tips=get_next_tip_tips, starting_tip_name=input_starting_tip, + nozzle_map=load_pipette_private_result.config.nozzle_map, ) assert result == result_well_name @@ -245,16 +373,48 @@ def test_get_next_tip_skips_picked_up_tip( def test_get_next_tip_with_starting_tip( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) - + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=1, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), + back_left_corner_offset=Point(x=1, y=2, z=3), + front_right_corner_offset=Point(x=4, y=5, z=6), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=1, starting_tip_name="B2", + nozzle_map=load_pipette_private_result.config.nozzle_map, ) assert result == "B2" @@ -278,6 +438,7 @@ def test_get_next_tip_with_starting_tip( labware_id="cool-labware", num_tips=1, starting_tip_name="B2", + nozzle_map=load_pipette_private_result.config.nozzle_map, ) assert result == "C2" @@ -286,16 +447,49 @@ def test_get_next_tip_with_starting_tip( def test_get_next_tip_with_starting_tip_8_channel( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the starting tip, and then the following tip after that.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=8, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_MULTI_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=8, starting_tip_name="A2", + nozzle_map=None, ) assert result == "A2" @@ -319,6 +513,7 @@ def test_get_next_tip_with_starting_tip_8_channel( labware_id="cool-labware", num_tips=8, starting_tip_name="A2", + nozzle_map=None, ) assert result == "A3" @@ -327,16 +522,49 @@ def test_get_next_tip_with_starting_tip_8_channel( def test_get_next_tip_with_starting_tip_out_of_tips( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the starting tip of H12 and then None after that.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=1, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_SINGLE_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=1, starting_tip_name="H12", + nozzle_map=None, ) assert result == "H12" @@ -360,6 +588,7 @@ def test_get_next_tip_with_starting_tip_out_of_tips( labware_id="cool-labware", num_tips=1, starting_tip_name="H12", + nozzle_map=None, ) assert result is None @@ -368,16 +597,49 @@ def test_get_next_tip_with_starting_tip_out_of_tips( def test_get_next_tip_with_column_and_starting_tip( subject: TipStore, load_labware_command: commands.LoadLabware, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should return the first tip in a column, taking starting tip into account.""" subject.handle_action( actions.UpdateCommandAction(private_result=None, command=load_labware_command) ) + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=8, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P300_MULTI_GEN2), + back_left_corner_offset=Point(0, 0, 0), + front_right_corner_offset=Point(0, 0, 0), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) result = TipView(subject.state).get_next_tip( labware_id="cool-labware", num_tips=8, starting_tip_name="D1", + nozzle_map=None, ) assert result == "A2" @@ -400,7 +662,7 @@ def test_reset_tips( pipette_id="pipette-id", serial_number="pipette-serial", config=LoadedStaticPipetteData( - channels=8, + channels=1, max_volume=15, min_volume=3, model="gen a", @@ -435,6 +697,7 @@ def test_reset_tips( labware_id="cool-labware", num_tips=1, starting_tip_name=None, + nozzle_map=None, ) assert result == "A1" @@ -759,5 +1022,117 @@ def test_next_tip_uses_active_channels( labware_id="cool-labware", num_tips=5, starting_tip_name=None, + nozzle_map=None, ) assert result == "A2" + + +def test_next_tip_automatic_tip_tracking_with_partial_configurations( + subject: TipStore, + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, + load_labware_command: commands.LoadLabware, + pick_up_tip_command: commands.PickUpTip, +) -> None: + """Test tip tracking logic using multiple pipette configurations.""" + # Load labware + subject.handle_action( + actions.UpdateCommandAction(private_result=None, command=load_labware_command) + ) + + # Load pipette + load_pipette_command = commands.LoadPipette.construct( # type: ignore[call-arg] + result=commands.LoadPipetteResult(pipetteId="pipette-id") + ) + load_pipette_private_result = commands.LoadPipettePrivateResult( + pipette_id="pipette-id", + serial_number="pipette-serial", + config=LoadedStaticPipetteData( + channels=96, + max_volume=15, + min_volume=3, + model="gen a", + display_name="display name", + flow_rates=FlowRates( + default_aspirate={}, + default_dispense={}, + default_blow_out={}, + ), + tip_configuration_lookup_table={15: supported_tip_fixture}, + nominal_tip_overlap={}, + nozzle_offset_z=1.23, + home_position=4.56, + nozzle_map=get_default_nozzle_map(PipetteNameType.P1000_96), + back_left_corner_offset=Point(x=1, y=2, z=3), + front_right_corner_offset=Point(x=4, y=5, z=6), + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=load_pipette_private_result, command=load_pipette_command + ) + ) + + def _assert_and_pickup(well: str, nozzle_map: NozzleMap) -> None: + result = TipView(subject.state).get_next_tip( + labware_id="cool-labware", + num_tips=0, + starting_tip_name=None, + nozzle_map=nozzle_map, + ) + assert result == well + + pick_up_tip = commands.PickUpTip.construct( # type: ignore[call-arg] + params=commands.PickUpTipParams.construct( + pipetteId="pipette-id", + labwareId="cool-labware", + wellName=result, + ), + result=commands.PickUpTipResult.construct( + position=DeckPoint(x=0, y=0, z=0), tipLength=1.23 + ), + ) + + subject.handle_action( + actions.UpdateCommandAction(private_result=None, command=pick_up_tip) + ) + + # Configure nozzle for partial configurations + configure_nozzle_layout_cmd = commands.ConfigureNozzleLayout.construct( # type: ignore[call-arg] + result=commands.ConfigureNozzleLayoutResult() + ) + + def _reconfigure_nozzle_layout(start: str, back_l: str, front_r: str) -> NozzleMap: + + configure_nozzle_private_result = commands.ConfigureNozzleLayoutPrivateResult( + pipette_id="pipette-id", + nozzle_map=NozzleMap.build( + physical_nozzles=NINETY_SIX_MAP, + physical_rows=NINETY_SIX_ROWS, + physical_columns=NINETY_SIX_COLS, + starting_nozzle=start, + back_left_nozzle=back_l, + front_right_nozzle=front_r, + ), + ) + subject.handle_action( + actions.UpdateCommandAction( + private_result=configure_nozzle_private_result, + command=configure_nozzle_layout_cmd, + ) + ) + return configure_nozzle_private_result.nozzle_map + + map = _reconfigure_nozzle_layout("A1", "A1", "H10") + _assert_and_pickup("A3", map) + map = _reconfigure_nozzle_layout("A1", "A1", "F2") + _assert_and_pickup("C1", map) + + # Configure to single tip pickups + map = _reconfigure_nozzle_layout("H12", "H12", "H12") + _assert_and_pickup("A1", map) + map = _reconfigure_nozzle_layout("H1", "H1", "H1") + _assert_and_pickup("A2", map) + map = _reconfigure_nozzle_layout("A12", "A12", "A12") + _assert_and_pickup("B1", map) + map = _reconfigure_nozzle_layout("A1", "A1", "A1") + _assert_and_pickup("B2", map) diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index 59772c868ed..1508373152d 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -749,7 +749,7 @@ async def test_estop_during_command( decoy.when(model_utils.get_timestamp()).then_return(timestamp) decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(state_store.commands.get_is_stopped()).then_return(False) - decoy.when(state_store.commands.state.running_command_id).then_return(command_id) + decoy.when(state_store.commands.get_running_command_id()).then_return(command_id) decoy.when(state_store.commands.state.queued_command_ids).then_return( fake_command_set ) @@ -793,7 +793,7 @@ async def test_estop_without_command( decoy.when(model_utils.get_timestamp()).then_return(timestamp) decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(state_store.commands.get_is_stopped()).then_return(False) - decoy.when(state_store.commands.state.running_command_id).then_return(None) + decoy.when(state_store.commands.get_running_command_id()).then_return(None) expected_stop = StopAction(from_estop=True) expected_hardware_stop = HardwareStoppedAction( diff --git a/app-shell-odd/src/__mocks__/log.ts b/app-shell-odd/src/__mocks__/log.ts index eb498dd5963..7b3cdc8dcfe 100644 --- a/app-shell-odd/src/__mocks__/log.ts +++ b/app-shell-odd/src/__mocks__/log.ts @@ -1,4 +1,3 @@ // mock logger // NOTE: importing mock to avoid copy-paste -// eslint-disable-next-line jest/no-mocks-import export * from '@opentrons/app/src/__mocks__/logger' diff --git a/app-shell-odd/src/notify.ts b/app-shell-odd/src/notify.ts index be9af060346..f88280369a0 100644 --- a/app-shell-odd/src/notify.ts +++ b/app-shell-odd/src/notify.ts @@ -1,13 +1,19 @@ /* eslint-disable @typescript-eslint/no-dynamic-delete */ import mqtt from 'mqtt' +import isEqual from 'lodash/isEqual' import { createLogger } from './log' import type { BrowserWindow } from 'electron' -import type { NotifyTopic } from '@opentrons/app/src/redux/shell/types' +import type { + NotifyTopic, + NotifyResponseData, + NotifyRefetchData, + NotifyUnsubscribeData, + NotifyNetworkError, +} from '@opentrons/app/src/redux/shell/types' import type { Action, Dispatch } from './types' -// TODO(jh, 2024-01-22): refactor the ODD connection store to manage a single client only. // TODO(jh, 2024-03-01): after refactoring notify connectivity and subscription logic, uncomment logs. // Manages MQTT broker connections via a connection store, establishing a connection to the broker only if a connection does not @@ -123,7 +129,7 @@ function subscribe(notifyParams: NotifyParams): Promise { log.warn( `Failed to connect to ${hostname} - ${error.name}: ${error.message} ` ) - let failureMessage: string = FAILURE_STATUSES.ECONNFAILED + let failureMessage: NotifyNetworkError = FAILURE_STATUSES.ECONNFAILED if (connectionStore[hostname]?.client == null) { unreachableHosts.add(hostname) if ( @@ -184,7 +190,6 @@ function subscribe(notifyParams: NotifyParams): Promise { function subscribeCb(error: Error, result: mqtt.ISubscriptionGrant[]): void { const { subscriptions } = connectionStore[hostname] if (error != null) { - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) sendToBrowserDeserialized({ browserWindow, hostname, @@ -197,7 +202,6 @@ function subscribe(notifyParams: NotifyParams): Promise { } }, RENDER_TIMEOUT) } else { - // log.info(`Successfully subscribed on ${hostname} to topic: ${topic}`) if (subscriptions[topic] > 0) { subscriptions[topic] += 1 } else { @@ -227,7 +231,6 @@ function subscribe(notifyParams: NotifyParams): Promise { counter++ if (counter === MAX_RETRIES) { clearInterval(intervalId) - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) reject(new Error('Maximum subscription retries exceeded.')) } }, CHECK_CONNECTION_INTERVAL) @@ -252,24 +255,15 @@ function unsubscribe(notifyParams: NotifyParams): Promise { if (isLastSubscription) { client?.unsubscribe(topic, {}, (error, result) => { - if (error != null) { - // log.warn( - // `Failed to unsubscribe on ${hostname} from topic: ${topic}` - // ) - } else { - // log.info( - // `Successfully unsubscribed on ${hostname} from topic: ${topic}` - // ) + if (error == null) { handleDecrementSubscriptionCount(hostname, topic) + } else { + log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) } }) } else { subscriptions[topic] -= 1 } - } else { - // log.info( - // `Attempted to unsubscribe from unconnected hostname: ${hostname}` - // ) } }, RENDER_TIMEOUT) }) @@ -344,12 +338,21 @@ function establishListeners({ client.on( 'message', (topic: NotifyTopic, message: Buffer, packet: mqtt.IPublishPacket) => { - sendToBrowserDeserialized({ - browserWindow, - hostname, - topic, - message: message.toString(), - }) + deserialize(message.toString()) + .then(deserializedMessage => { + log.debug('Received notification data from main via IPC', { + hostname, + topic, + }) + + browserWindow.webContents.send( + 'notify', + hostname, + topic, + deserializedMessage + ) + }) + .catch(error => log.debug(`${error.message}`)) } ) @@ -410,7 +413,7 @@ interface SendToBrowserParams { browserWindow: BrowserWindow hostname: string topic: NotifyTopic - message: string + message: NotifyResponseData } function sendToBrowserDeserialized({ @@ -419,18 +422,34 @@ function sendToBrowserDeserialized({ topic, message, }: SendToBrowserParams): void { - let deserializedMessage: string | Object + browserWindow.webContents.send('notify', hostname, topic, message) +} - try { - deserializedMessage = JSON.parse(message) - } catch { - deserializedMessage = message - } +const VALID_MODELS: [NotifyRefetchData, NotifyUnsubscribeData] = [ + { refetchUsingHTTP: true }, + { unsubscribe: true }, +] + +function deserialize(message: string): Promise { + return new Promise((resolve, reject) => { + let deserializedMessage: NotifyResponseData | Record + const error = new Error( + `Unexpected data received from notify broker: ${message}` + ) - // log.info('Received notification data from main via IPC', { - // hostname, - // topic, - // }) + try { + deserializedMessage = JSON.parse(message) + } catch { + reject(error) + } - browserWindow.webContents.send('notify', hostname, topic, deserializedMessage) + const isValidNotifyResponse = VALID_MODELS.some(model => + isEqual(model, deserializedMessage) + ) + if (!isValidNotifyResponse) { + reject(error) + } else { + resolve(JSON.parse(message)) + } + }) } diff --git a/app-shell/src/notify.ts b/app-shell/src/notify.ts index 95dfcbdcac3..3de2281a385 100644 --- a/app-shell/src/notify.ts +++ b/app-shell/src/notify.ts @@ -1,10 +1,17 @@ /* eslint-disable @typescript-eslint/no-dynamic-delete */ import mqtt from 'mqtt' +import isEqual from 'lodash/isEqual' import { createLogger } from './log' import type { BrowserWindow } from 'electron' -import type { NotifyTopic } from '@opentrons/app/src/redux/shell/types' +import type { + NotifyTopic, + NotifyResponseData, + NotifyRefetchData, + NotifyUnsubscribeData, + NotifyNetworkError, +} from '@opentrons/app/src/redux/shell/types' import type { Action, Dispatch } from './types' // TODO(jh, 2024-03-01): after refactoring notify connectivity and subscription logic, uncomment logs. @@ -120,7 +127,7 @@ function subscribe(notifyParams: NotifyParams): Promise { log.warn( `Failed to connect to ${hostname} - ${error.name}: ${error.message} ` ) - let failureMessage: string = FAILURE_STATUSES.ECONNFAILED + let failureMessage: NotifyNetworkError = FAILURE_STATUSES.ECONNFAILED if (connectionStore[hostname]?.client == null) { unreachableHosts.add(hostname) if ( @@ -181,7 +188,6 @@ function subscribe(notifyParams: NotifyParams): Promise { function subscribeCb(error: Error, result: mqtt.ISubscriptionGrant[]): void { const { subscriptions } = connectionStore[hostname] if (error != null) { - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) sendToBrowserDeserialized({ browserWindow, hostname, @@ -194,7 +200,6 @@ function subscribe(notifyParams: NotifyParams): Promise { } }, RENDER_TIMEOUT) } else { - // log.info(`Successfully subscribed on ${hostname} to topic: ${topic}`) if (subscriptions[topic] > 0) { subscriptions[topic] += 1 } else { @@ -224,7 +229,6 @@ function subscribe(notifyParams: NotifyParams): Promise { counter++ if (counter === MAX_RETRIES) { clearInterval(intervalId) - // log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) reject(new Error('Maximum subscription retries exceeded.')) } }, CHECK_CONNECTION_INTERVAL) @@ -249,24 +253,15 @@ function unsubscribe(notifyParams: NotifyParams): Promise { if (isLastSubscription) { client?.unsubscribe(topic, {}, (error, result) => { - if (error != null) { - // log.warn( - // `Failed to unsubscribe on ${hostname} from topic: ${topic}` - // ) - } else { - // log.info( - // `Successfully unsubscribed on ${hostname} from topic: ${topic}` - // ) + if (error == null) { handleDecrementSubscriptionCount(hostname, topic) + } else { + log.warn(`Failed to subscribe on ${hostname} to topic: ${topic}`) } }) } else { subscriptions[topic] -= 1 } - } else { - // log.info( - // `Attempted to unsubscribe from unconnected hostname: ${hostname}` - // ) } }, RENDER_TIMEOUT) }) @@ -341,12 +336,21 @@ function establishListeners({ client.on( 'message', (topic: NotifyTopic, message: Buffer, packet: mqtt.IPublishPacket) => { - sendToBrowserDeserialized({ - browserWindow, - hostname, - topic, - message: message.toString(), - }) + deserialize(message.toString()) + .then(deserializedMessage => { + log.debug('Received notification data from main via IPC', { + hostname, + topic, + }) + + browserWindow.webContents.send( + 'notify', + hostname, + topic, + deserializedMessage + ) + }) + .catch(error => log.debug(`${error.message}`)) } ) @@ -407,7 +411,7 @@ interface SendToBrowserParams { browserWindow: BrowserWindow hostname: string topic: NotifyTopic - message: string + message: NotifyResponseData } function sendToBrowserDeserialized({ @@ -416,18 +420,34 @@ function sendToBrowserDeserialized({ topic, message, }: SendToBrowserParams): void { - let deserializedMessage: string | Object + browserWindow.webContents.send('notify', hostname, topic, message) +} - try { - deserializedMessage = JSON.parse(message) - } catch { - deserializedMessage = message - } +const VALID_MODELS: [NotifyRefetchData, NotifyUnsubscribeData] = [ + { refetchUsingHTTP: true }, + { unsubscribe: true }, +] + +function deserialize(message: string): Promise { + return new Promise((resolve, reject) => { + let deserializedMessage: NotifyResponseData | Record + const error = new Error( + `Unexpected data received from notify broker: ${message}` + ) - // log.info('Received notification data from main via IPC', { - // hostname, - // topic, - // }) + try { + deserializedMessage = JSON.parse(message) + } catch { + reject(error) + } - browserWindow.webContents.send('notify', hostname, topic, deserializedMessage) + const isValidNotifyResponse = VALID_MODELS.some(model => + isEqual(model, deserializedMessage) + ) + if (!isValidNotifyResponse) { + reject(error) + } else { + resolve(JSON.parse(message)) + } + }) } diff --git a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx index d1a7307b77c..ba9b852923e 100644 --- a/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx +++ b/app/src/App/__tests__/OnDeviceDisplayApp.test.tsx @@ -27,7 +27,7 @@ import { getIsShellReady } from '../../redux/shell' import { getLocalRobot } from '../../redux/discovery' import { mockConnectedRobot } from '../../redux/discovery/__fixtures__' import { useCurrentRunRoute, useProtocolReceiptToast } from '../hooks' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import type { OnDeviceDisplaySettings } from '../../redux/config/schema-types' @@ -51,7 +51,7 @@ vi.mock('../../pages/DeckConfiguration') vi.mock('../../redux/config') vi.mock('../../redux/shell') vi.mock('../../redux/discovery') -vi.mock('../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../resources/maintenance_runs') vi.mock('../hooks') const mockSettings = { diff --git a/app/src/App/hooks.ts b/app/src/App/hooks.ts index cbc40b396eb..a7db8ed203f 100644 --- a/app/src/App/hooks.ts +++ b/app/src/App/hooks.ts @@ -22,8 +22,7 @@ import { import { checkShellUpdate } from '../redux/shell' import { useToaster } from '../organisms/ToasterOven' -import { useNotifyAllRunsQuery } from '../resources/runs/useNotifyAllRunsQuery' -import { useNotifyRunQuery } from '../resources/runs/useNotifyRunQuery' +import { useNotifyAllRunsQuery, useNotifyRunQuery } from '../resources/runs' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data' import type { Dispatch } from '../redux/types' diff --git a/app/src/DesignTokens/Colors/Colors.stories.tsx b/app/src/DesignTokens/Colors/Colors.stories.tsx index b1a1bef3c15..cb35bbea9ec 100644 --- a/app/src/DesignTokens/Colors/Colors.stories.tsx +++ b/app/src/DesignTokens/Colors/Colors.stories.tsx @@ -74,7 +74,7 @@ const Template: Story = args => { gridGap={SPACING.spacing4} width="20rem" height="6rem" - borderRadius={BORDERS.borderRadiusSize2} + borderRadius={BORDERS.borderRadius8} onClick={() => handleClick(color[0])} style={{ cursor: 'pointer' }} border={`2px solid ${COLORS.black90}`} diff --git a/app/src/assets/labware/__tests__/findLabware.test.ts b/app/src/assets/labware/__tests__/findLabware.test.ts index d97a67f74cc..1d9c6ccad78 100644 --- a/app/src/assets/labware/__tests__/findLabware.test.ts +++ b/app/src/assets/labware/__tests__/findLabware.test.ts @@ -123,7 +123,6 @@ describe('findLabwareDefWithCustom', () => { SPECS.forEach(spec => { // TODO(mc, 2021-05-19): these tests are failing due to bug in code under test // see: https://github.com/Opentrons/opentrons/issues/7823 - // eslint-disable-next-line jest/no-disabled-tests it.skip(`should ${spec.should}`, () => { expect( findLabwareDefWithCustom( diff --git a/app/src/assets/localization/en/app_settings.json b/app/src/assets/localization/en/app_settings.json index b8d7bdc4c2c..1c228f79f95 100644 --- a/app/src/assets/localization/en/app_settings.json +++ b/app/src/assets/localization/en/app_settings.json @@ -1,5 +1,7 @@ { "__dev_internal__protocolStats": "Protocol Stats", + "__dev_internal__enableRunTimeParameters": "Enable Run Time Parameters", + "__dev_internal__enableRunNotes": "Display Notes During a Protocol Run", "add_folder_button": "Add labware source folder", "add_ip_button": "Add", "add_ip_error": "Enter an IP Address or Hostname", diff --git a/app/src/assets/localization/en/protocol_details.json b/app/src/assets/localization/en/protocol_details.json index 229aa43cc90..b5d6afe072f 100644 --- a/app/src/assets/localization/en/protocol_details.json +++ b/app/src/assets/localization/en/protocol_details.json @@ -1,5 +1,7 @@ { "author": "author", + "both_mounts": "Both Mounts", + "choices": "{{count}} choices", "choose_robot_to_run": "Choose Robot to Run\n{{protocol_name}}", "clear_and_proceed_to_setup": "Clear and proceed to setup", "connect_modules_to_see_controls": "Connect modules to see controls", @@ -7,6 +9,7 @@ "connection_status": "connection status", "creation_method": "creation method", "deck_view": "Deck View", + "default_value": "Default Value", "delete_protocol_perm": "{{name}} and its run history will be permanently deleted.", "delete_protocol": "Delete Protocol", "delete_this_protocol": "Delete this protocol?", @@ -20,18 +23,26 @@ "labware": "labware", "last_analyzed": "last analyzed", "last_updated": "last updated", - "both_mounts": "Both Mounts", "left_mount": "left mount", + "left_right": "Left, Right", "liquid_name": "liquid name", "liquids_not_in_protocol": "no liquids are specified for this protocol", "liquids": "liquids", + "listed_values_are_view_only": "Listed values are view-only", "location": "location", "modules": "modules", + "name": "Name", + "num_choices": "{{num}} choices", "no_available_robots_found": "No available robots found", + "no_parameters": "No parameters specified in this protocol", "no_summary": "no summary specified for this protocol.", "not_connected": "not connected", "not_in_protocol": "no {{section}} is specified for this protocol", + "off": "Off", + "on_off": "On, off", + "on": "On", "org_or_author": "org/author", + "parameters": "Parameters", "pipette_aspirate_count_description": "individual aspirate commands per pipette.", "pipette_aspirate_count": "{{pipette}} aspirate count", "pipette_dispense_count_description": "individual dispense commands per pipette.", @@ -44,6 +55,7 @@ "protocol_outdated_app_analysis": "This protocol's analysis is out of date. It may produce different results if you run it now.", "python_api_version": "Python API {{version}}", "quantity": "Quantity", + "range": "Range", "read_less": "read less", "read_more": "read more", "right_mount": "right mount", @@ -56,6 +68,7 @@ "sending": "Sending", "show_in_folder": "Show in folder", "slot": "Slot {{slotName}}", + "start_setup_customize_values": "Start setup to customize values", "start_setup": "Start setup", "successfully_sent": "Successfully sent", "total_volume": "total volume", diff --git a/app/src/assets/localization/en/shared.json b/app/src/assets/localization/en/shared.json index fe3c9177c5c..8c8bed0a5af 100644 --- a/app/src/assets/localization/en/shared.json +++ b/app/src/assets/localization/en/shared.json @@ -6,6 +6,7 @@ "before_you_begin": "Before you begin", "browse": "browse", "cancel": "cancel", + "change_robot": "Change robot", "clear_data": "clear data", "close_robot_door": "Close the robot door before starting the run.", "close": "close", @@ -13,8 +14,10 @@ "confirm_placement": "Confirm placement", "confirm_position": "Confirm position", "confirm_terminate": "This will immediately stop the activity begun on a computer. You, or another user, may lose progress or see an error in the Opentrons App.", + "confirm_values": "Confirm values", "confirm": "Confirm", "continue_activity": "Continue activity", + "continue_to_param": "Continue to parameters", "continue": "continue", "delete": "Delete", "did_pipette_pick_up_tip": "Did pipette pick up tip successfully?", diff --git a/app/src/atoms/Banner/Banner.stories.tsx b/app/src/atoms/Banner/Banner.stories.tsx index bba4f00aaa1..3356b0edb98 100644 --- a/app/src/atoms/Banner/Banner.stories.tsx +++ b/app/src/atoms/Banner/Banner.stories.tsx @@ -10,7 +10,7 @@ export default { } as Meta const Template: Story> = args => ( - + {'Banner component'} ) export const Primary = Template.bind({}) diff --git a/app/src/atoms/Banner/index.tsx b/app/src/atoms/Banner/index.tsx index 8b875572253..a74fcf829ba 100644 --- a/app/src/atoms/Banner/index.tsx +++ b/app/src/atoms/Banner/index.tsx @@ -11,7 +11,6 @@ import { IconProps, JUSTIFY_SPACE_BETWEEN, RESPONSIVENESS, - SIZE_1, SPACING, TYPOGRAPHY, } from '@opentrons/components' @@ -91,7 +90,7 @@ export function Banner(props: BannerProps): JSX.Element { const bannerProps = BANNER_PROPS_BY_TYPE[type] const iconProps = { ...(icon ?? bannerProps.icon), - size: size ?? SIZE_1, + size: size ?? '1rem', marginRight: iconMarginRight ?? SPACING.spacing8, marginLeft: iconMarginLeft ?? '0rem', color: BANNER_PROPS_BY_TYPE[type].color, @@ -105,7 +104,7 @@ export function Banner(props: BannerProps): JSX.Element { font-size: 1.25rem; font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; background-color: ${COLORS.yellow35}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius12}; line-height: 1.5rem; } ` @@ -143,7 +142,7 @@ export function Banner(props: BannerProps): JSX.Element { ) : null} {(isCloseActionLoading ?? false) && ( - + )} ) diff --git a/app/src/atoms/Chip/__tests__/Chip.test.tsx b/app/src/atoms/Chip/__tests__/Chip.test.tsx index e231752d1c4..611a6e76ce0 100644 --- a/app/src/atoms/Chip/__tests__/Chip.test.tsx +++ b/app/src/atoms/Chip/__tests__/Chip.test.tsx @@ -36,7 +36,7 @@ describe('Chip', () => { const chip = screen.getByTestId('Chip_success') const chipText = screen.getByText('mockSuccess') expect(chip).toHaveStyle(`background-color: ${COLORS.green35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) const icon = screen.getByLabelText('icon_mockSuccess') expect(icon).toHaveStyle(`color: ${COLORS.green60}`) @@ -52,7 +52,7 @@ describe('Chip', () => { const chip = screen.getByTestId('Chip_success') const chipText = screen.getByText('mockSuccess') expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.green60}`) const icon = screen.getByLabelText('icon_mockSuccess') expect(icon).toHaveStyle(`color: ${COLORS.green60}`) @@ -67,7 +67,7 @@ describe('Chip', () => { const chip = screen.getByTestId('Chip_warning') const chipText = screen.getByText('mockWarning') expect(chip).toHaveStyle(`background-color: ${COLORS.yellow35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) const icon = screen.getByLabelText('icon_mockWarning') expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) @@ -83,7 +83,7 @@ describe('Chip', () => { const chip = screen.getByTestId('Chip_warning') const chipText = screen.getByText('mockWarning') expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.yellow60}`) const icon = screen.getByLabelText('icon_mockWarning') expect(icon).toHaveStyle(`color: ${COLORS.yellow60}`) @@ -100,7 +100,7 @@ describe('Chip', () => { expect(chip).toHaveStyle( `background-color: ${COLORS.black90}${COLORS.opacity20HexCode}` ) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) const icon = screen.getByLabelText('icon_mockNeutral') expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) @@ -116,7 +116,7 @@ describe('Chip', () => { const chip = screen.getByTestId('Chip_neutral') const chipText = screen.getByText('mockNeutral') expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.grey60}`) const icon = screen.getByLabelText('icon_mockNeutral') expect(icon).toHaveStyle(`color: ${COLORS.grey60}`) @@ -131,7 +131,7 @@ describe('Chip', () => { const chip = screen.getByTestId('Chip_error') const chipText = screen.getByText('mockError') expect(chip).toHaveStyle(`background-color: ${COLORS.red35}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) const icon = screen.getByLabelText('icon_mockError') expect(icon).toHaveStyle(`color: ${COLORS.red60}`) @@ -147,7 +147,7 @@ describe('Chip', () => { const chip = screen.getByTestId('Chip_error') const chipText = screen.getByText('mockError') expect(chip).toHaveStyle(`background-color: ${COLORS.transparent}`) - expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(chip).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(chipText).toHaveStyle(`color: ${COLORS.red60}`) const icon = screen.getByLabelText('icon_mockError') expect(icon).toHaveStyle(`color: ${COLORS.red60}`) diff --git a/app/src/atoms/Chip/index.tsx b/app/src/atoms/Chip/index.tsx index 85a090e509c..18283d4e401 100644 --- a/app/src/atoms/Chip/index.tsx +++ b/app/src/atoms/Chip/index.tsx @@ -40,12 +40,12 @@ const CHIP_PROPS_BY_TYPE: Record< > = { basic: { backgroundColor: `${COLORS.black90}${COLORS.opacity20HexCode}`, - borderRadius: BORDERS.borderRadiusSize1, + borderRadius: BORDERS.borderRadius4, textColor: COLORS.grey60, }, error: { backgroundColor: COLORS.red35, - borderRadius: BORDERS.borderRadiusSize5, + borderRadius: BORDERS.borderRadius40, iconColor: COLORS.red60, textColor: COLORS.red60, }, @@ -57,20 +57,20 @@ const CHIP_PROPS_BY_TYPE: Record< }, neutral: { backgroundColor: `${COLORS.black90}${COLORS.opacity20HexCode}`, - borderRadius: BORDERS.borderRadiusSize5, + borderRadius: BORDERS.borderRadius40, iconColor: COLORS.grey60, textColor: COLORS.grey60, }, success: { backgroundColor: COLORS.green35, - borderRadius: BORDERS.borderRadiusSize5, + borderRadius: BORDERS.borderRadius40, iconColor: COLORS.green60, iconName: 'ot-check', textColor: COLORS.green60, }, warning: { backgroundColor: COLORS.yellow35, - borderRadius: BORDERS.borderRadiusSize5, + borderRadius: BORDERS.borderRadius40, iconColor: COLORS.yellow60, textColor: COLORS.yellow60, }, diff --git a/app/src/atoms/InlineNotification/index.tsx b/app/src/atoms/InlineNotification/index.tsx index a92492d7c46..59bbbef4cb3 100644 --- a/app/src/atoms/InlineNotification/index.tsx +++ b/app/src/atoms/InlineNotification/index.tsx @@ -1,16 +1,16 @@ import * as React from 'react' import { - Icon, - JUSTIFY_SPACE_BETWEEN, - IconProps, - Flex, - DIRECTION_ROW, ALIGN_CENTER, + BORDERS, + Btn, COLORS, + DIRECTION_ROW, + Flex, + Icon, + IconProps, + JUSTIFY_SPACE_BETWEEN, SPACING, TYPOGRAPHY, - BORDERS, - Btn, } from '@opentrons/components' import { StyledText } from '../text' @@ -46,8 +46,8 @@ const INLINE_NOTIFICATION_PROPS_BY_TYPE: Record< }, neutral: { icon: { name: 'information' }, - backgroundColor: COLORS.grey30, - color: COLORS.grey60, + backgroundColor: COLORS.blue30, + color: COLORS.blue60, }, success: { icon: { name: 'ot-check' }, @@ -72,7 +72,7 @@ export function InlineNotification( {fullHeading} - {message && fullmessage} + {message != null && fullmessage} {onCloseClick && ( diff --git a/app/src/atoms/InputField/index.tsx b/app/src/atoms/InputField/index.tsx index 7ffb71119d2..d67710557f6 100644 --- a/app/src/atoms/InputField/index.tsx +++ b/app/src/atoms/InputField/index.tsx @@ -7,12 +7,11 @@ import { COLOR_WARNING_DARK, COLORS, DIRECTION_COLUMN, - DISPLAY_INLINE_BLOCK, Flex, RESPONSIVENESS, SPACING, - TEXT_ALIGN_RIGHT, TYPOGRAPHY, + TEXT_ALIGN_RIGHT, } from '@opentrons/components' export const INPUT_TYPE_NUMBER = 'number' as const @@ -36,6 +35,8 @@ export interface InputFieldProps { value?: string | number | null /** if included, InputField will use error style and display error instead of caption */ error?: string | null + /** optional title */ + title?: string | null /** optional caption. hidden when `error` is given */ caption?: string | null /** appears to the right of the caption. Used for character limits, eg '0/45' */ @@ -62,6 +63,12 @@ export interface InputFieldProps { /** if input type is number, these are the min and max values */ max?: number min?: number + /** horizontal text alignment for title, input, and (sub)captions */ + textAlign?: + | typeof TYPOGRAPHY.textAlignLeft + | typeof TYPOGRAPHY.textAlignCenter + /** small or medium input field height, relevant only */ + size?: 'medium' | 'small' } export function InputField(props: InputFieldProps): JSX.Element { @@ -80,20 +87,39 @@ export function InputField(props: InputFieldProps): JSX.Element { } function Input(props: InputFieldProps): JSX.Element { + const { + placeholder, + textAlign = TYPOGRAPHY.textAlignLeft, + size = 'small', + title, + ...inputProps + } = props const error = props.error != null const value = props.isIndeterminate ?? false ? '' : props.value ?? '' const placeHolder = props.isIndeterminate ?? false ? '-' : props.placeholder + const OUTER_CSS = css` + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + &:focus-within { + filter: ${error + ? 'none' + : `drop-shadow(0px 0px 10px ${COLORS.blue50})`}; + } + } + ` + const INPUT_FIELD = css` display: flex; background-color: ${COLORS.white}; - border-radius: ${SPACING.spacing4}; + border-radius: ${BORDERS.borderRadius4}; padding: ${SPACING.spacing8}; border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50}; font-size: ${TYPOGRAPHY.fontSizeP}; + width: 100%; + height: 2rem; - &:active { - border: 1px ${BORDERS.styleSolid} ${COLORS.grey50}; + &:active:enabled { + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; } & input { @@ -103,6 +129,7 @@ function Input(props: InputFieldProps): JSX.Element { flex: 1 1 auto; width: 100%; height: ${SPACING.spacing16}; + text-align: ${textAlign}; } & input:focus { outline: none; @@ -110,12 +137,18 @@ function Input(props: InputFieldProps): JSX.Element { &:hover { border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60}; + } + + &:focus-visible { + border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60}; outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; outline-offset: 3px; } - &:focus { - border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey60}; + + &:focus-within { + border: 1px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.blue50}; } + &:disabled { border: 1px ${BORDERS.styleSolid} ${COLORS.grey30}; } @@ -124,6 +157,29 @@ function Input(props: InputFieldProps): JSX.Element { -webkit-appearance: none; margin: 0; } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: ${size === 'small' ? '4.25rem' : '5rem'}; + box-shadow: ${error ? BORDERS.shadowBig : 'none'}; + font-size: ${TYPOGRAPHY.fontSize28}; + padding: ${SPACING.spacing16} ${SPACING.spacing24}; + border: 2px ${BORDERS.styleSolid} ${error ? COLORS.red50 : COLORS.grey50}; + + &:focus-within { + box-shadow: none; + border: ${error ? '2px' : '3px'} ${BORDERS.styleSolid} + ${error ? COLORS.red50 : COLORS.blue50}; + } + + & input { + color: ${COLORS.black90}; + flex: 1 1 auto; + width: 100%; + height: 100%; + font-size: ${TYPOGRAPHY.fontSize28}; + line-height: ${TYPOGRAPHY.lineHeight36}; + } + } ` const FORM_BOTTOM_SPACE_STYLE = css` @@ -133,6 +189,21 @@ function Input(props: InputFieldProps): JSX.Element { } ` + const TITLE_STYLE = css` + color: ${error ? COLORS.red50 : COLORS.black90}; + padding-bottom: ${SPACING.spacing8}; + font-size: ${TYPOGRAPHY.fontSizeLabel}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; + line-height: ${TYPOGRAPHY.lineHeight12}; + align-text: ${textAlign}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + font-size: ${TYPOGRAPHY.fontSize22}; + font-weight: ${TYPOGRAPHY.fontWeightRegular}; + line-height: ${TYPOGRAPHY.lineHeight28}; + justify-content: ${textAlign}; + } + ` + const ERROR_TEXT_STYLE = css` color: ${COLORS.red50}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { @@ -141,38 +212,57 @@ function Input(props: InputFieldProps): JSX.Element { } ` + const UNITS_STYLE = css` + color: ${props.disabled ? COLORS.grey40 : COLORS.grey50}; + font-size: ${TYPOGRAPHY.fontSizeLabel}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; + line-height: ${TYPOGRAPHY.lineHeight12}; + align-text: ${TEXT_ALIGN_RIGHT}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + color: ${props.disabled ? COLORS.grey40 : COLORS.grey50}; + font-size: ${TYPOGRAPHY.fontSize22}; + font-weight: ${TYPOGRAPHY.fontWeightRegular}; + line-height: ${TYPOGRAPHY.lineHeight28}; + justify-content: ${textAlign}; + } + ` + return ( - - - - {props.units != null && ( - - {props.units} - - )} - - - {props.caption} - {props.secondaryCaption != null ? ( - {props.secondaryCaption} - ) : null} - {props.error} + + {props.title != null ? ( + {props.title} + ) : null} + + + + {props.units != null ? ( + {props.units} + ) : null} + + + {props.caption != null ? ( + {props.caption} + ) : null} + {props.secondaryCaption != null ? ( + {props.secondaryCaption} + ) : null} + {props.error} + ) diff --git a/app/src/atoms/InstrumentContainer/index.tsx b/app/src/atoms/InstrumentContainer/index.tsx index 0effcea81bb..2536ea89647 100644 --- a/app/src/atoms/InstrumentContainer/index.tsx +++ b/app/src/atoms/InstrumentContainer/index.tsx @@ -17,7 +17,7 @@ export const InstrumentContainer = ( return ( { expect(listItem).toHaveStyle( `padding: ${SPACING.spacing16} ${SPACING.spacing24}` ) - expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadiusSize3}`) + expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius12}`) }) it('should render correct style - noActive', () => { props.type = 'noActive' @@ -39,7 +39,7 @@ describe('ListItem', () => { expect(listItem).toHaveStyle( `padding: ${SPACING.spacing16} ${SPACING.spacing24}` ) - expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadiusSize3}`) + expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius12}`) }) it('should render correct style - success', () => { props.type = 'success' @@ -50,7 +50,7 @@ describe('ListItem', () => { expect(listItem).toHaveStyle( `padding: ${SPACING.spacing16} ${SPACING.spacing24}` ) - expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadiusSize3}`) + expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius12}`) }) it('should render correct style - warning', () => { props.type = 'warning' @@ -61,6 +61,6 @@ describe('ListItem', () => { expect(listItem).toHaveStyle( `padding: ${SPACING.spacing16} ${SPACING.spacing24}` ) - expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadiusSize3}`) + expect(listItem).toHaveStyle(`borderRadius: ${BORDERS.borderRadius12}`) }) }) diff --git a/app/src/atoms/ListItem/index.tsx b/app/src/atoms/ListItem/index.tsx index 741ce9233c1..8df8ed82938 100644 --- a/app/src/atoms/ListItem/index.tsx +++ b/app/src/atoms/ListItem/index.tsx @@ -42,7 +42,7 @@ export function ListItem(props: ListItemProps): JSX.Element { height="max-content" padding={`${SPACING.spacing16} ${SPACING.spacing24}`} backgroundColor={listItemProps.backgroundColor} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} {...styleProps} > {children} diff --git a/app/src/atoms/MenuList/DropdownMenu.tsx b/app/src/atoms/MenuList/DropdownMenu.tsx index 5c1fb657cec..9693efa920a 100644 --- a/app/src/atoms/MenuList/DropdownMenu.tsx +++ b/app/src/atoms/MenuList/DropdownMenu.tsx @@ -26,52 +26,101 @@ export interface DropdownMenuProps { filterOptions: DropdownOption[] onClick: (value: string) => void currentOption: DropdownOption + width?: string + dropdownType?: 'rounded' | 'neutral' + title?: string } // TODO: (smb: 4/15/22) refactor this to use html select for accessibility export function DropdownMenu(props: DropdownMenuProps): JSX.Element { - const { filterOptions, onClick, currentOption } = props + const { + filterOptions, + onClick, + currentOption, + width = '9.125rem', + dropdownType = 'rounded', + title, + } = props const [showDropdownMenu, setShowDropdownMenu] = React.useState(false) - const toggleSetShowDropdownMenu = (): void => + const toggleSetShowDropdownMenu = (): void => { setShowDropdownMenu(!showDropdownMenu) + } const dropDownMenuWrapperRef = useOnClickOutside({ - onClickOutside: () => setShowDropdownMenu(false), + onClickOutside: () => { + setShowDropdownMenu(false) + }, }) + const DROPDOWN_STYLE = css` + flex-direction: ${DIRECTION_ROW}; + background-color: ${COLORS.white}; + cursor: pointer; + padding: ${SPACING.spacing8} ${SPACING.spacing12}; + border: 1px ${BORDERS.styleSolid} + ${showDropdownMenu ? COLORS.blue50 : COLORS.grey50}; + border-radius: ${dropdownType === 'rounded' + ? BORDERS.borderRadiusFull + : BORDERS.borderRadius4}; + align-items: ${ALIGN_CENTER}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + width: ${width}; + + &:hover { + border: 1px ${BORDERS.styleSolid} + ${showDropdownMenu ? COLORS.blue50 : COLORS.grey55}; + } + + &:active { + border: 1px ${BORDERS.styleSolid} ${COLORS.blue50}; + } + + &:focus-visible { + border: 1px ${BORDERS.styleSolid} ${COLORS.grey55}; + outline: 2px ${BORDERS.styleSolid} ${COLORS.blue50}; + outline-offset: 2px; + } + + &:disabled { + background-color: ${COLORS.transparent}; + color: ${COLORS.grey40}; + } + ` + return ( - <> + + {title !== null ? ( + + {title} + + ) : null} { + e.preventDefault() + toggleSetShowDropdownMenu() + }} + css={DROPDOWN_STYLE} + ref={dropDownMenuWrapperRef} > {currentOption.name} - + {showDropdownMenu ? ( + + ) : ( + + )} {showDropdownMenu && ( {filterOptions.map((option, index) => ( )} - + ) } diff --git a/app/src/atoms/MenuList/MenuItem.tsx b/app/src/atoms/MenuList/MenuItem.tsx index a91e64321e7..42a4efe2cb8 100644 --- a/app/src/atoms/MenuList/MenuItem.tsx +++ b/app/src/atoms/MenuList/MenuItem.tsx @@ -21,11 +21,7 @@ export const MenuItem = styled.button` ${SPACING.spacing12}; &:hover { - background-color: ${COLORS.grey10}; - } - - &:active { - background-color: ${COLORS.grey30}; + background-color: ${COLORS.blue10}; } &:disabled { diff --git a/app/src/atoms/MenuList/index.tsx b/app/src/atoms/MenuList/index.tsx index 4d06e8f5de5..1994e6b24d9 100644 --- a/app/src/atoms/MenuList/index.tsx +++ b/app/src/atoms/MenuList/index.tsx @@ -20,7 +20,7 @@ export const MenuList = (props: MenuListProps): JSX.Element | null => { const { children, isOnDevice = false, onClick = null } = props return isOnDevice && onClick != null ? ( diff --git a/app/src/atoms/SelectField/Select.tsx b/app/src/atoms/SelectField/Select.tsx index 4ac553344d8..92192c264bb 100644 --- a/app/src/atoms/SelectField/Select.tsx +++ b/app/src/atoms/SelectField/Select.tsx @@ -41,7 +41,7 @@ export function Select(props: SelectComponentProps): JSX.Element { clearIndicator: NO_STYLE_FN, control: (styles: CSSObjectWithLabel) => ({ ...styles, - borderRadius: BORDERS.radiusRoundEdge, + borderRadius: BORDERS.borderRadiusFull, border: BORDERS.lineBorder, width: props.width != null ? props.width : 'auto', height: SPACING.spacing16, diff --git a/app/src/atoms/Skeleton/index.tsx b/app/src/atoms/Skeleton/index.tsx index 69890ee621f..556a5653a77 100644 --- a/app/src/atoms/Skeleton/index.tsx +++ b/app/src/atoms/Skeleton/index.tsx @@ -12,7 +12,7 @@ interface SkeletonProps { export const Skeleton = (props: SkeletonProps): JSX.Element => { const { width, height, backgroundSize, borderRadius } = props const SKELETON_STYLE = css` - border-radius: ${borderRadius ?? BORDERS.radiusSoftCorners}; + border-radius: ${borderRadius ?? BORDERS.borderRadius8}; animation: shimmer 2s infinite linear; background: linear-gradient( to right, diff --git a/app/src/atoms/Slideout/MultiSlideout.stories.tsx b/app/src/atoms/Slideout/MultiSlideout.stories.tsx new file mode 100644 index 00000000000..abe53b8f1bf --- /dev/null +++ b/app/src/atoms/Slideout/MultiSlideout.stories.tsx @@ -0,0 +1,52 @@ +import * as React from 'react' +import { TYPOGRAPHY, PrimaryBtn, COLORS } from '@opentrons/components' +import { MultiSlideout } from './MultiSlideout' +import { StyledText } from '../text' + +import type { Story, Meta } from '@storybook/react' + +export default { + title: 'App/Atoms/MultiSlideout', + component: MultiSlideout, + argTypes: { onClick: { action: 'clicked' } }, +} as Meta + +const Template: Story> = args => { + const [firstPage, setFirstPage] = React.useState(false) + + const togglePage = (): void => { + setFirstPage(prevState => !prevState) + } + + const children = ( + <> + + {firstPage ? 'first page body' : 'second page body'} + + + + + {firstPage ? 'Go to Second Page' : 'Go to First Page'} + + + + ) + + return ( + + {children} + + ) +} + +export const Primary = Template.bind({}) +Primary.args = { + title: 'This is the slideout title with the max width', + isExpanded: 'true', + maxSteps: 2, +} diff --git a/app/src/atoms/Slideout/MultiSlideout.tsx b/app/src/atoms/Slideout/MultiSlideout.tsx new file mode 100644 index 00000000000..71ce02f6de6 --- /dev/null +++ b/app/src/atoms/Slideout/MultiSlideout.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import { Slideout } from './index' + +interface MultiSlideoutProps { + title: string | React.ReactElement + children: React.ReactNode + onCloseClick: () => void + currentStep: number + maxSteps: number + // isExpanded is for collapse and expand animation + isExpanded?: boolean + footer?: React.ReactNode +} + +export const MultiSlideout = (props: MultiSlideoutProps): JSX.Element => { + const { + isExpanded, + title, + onCloseClick, + children, + footer, + maxSteps, + currentStep, + } = props + + return ( + + {children} + + ) +} diff --git a/app/src/atoms/Slideout/index.tsx b/app/src/atoms/Slideout/index.tsx index 57d20e1de50..a3940f73727 100644 --- a/app/src/atoms/Slideout/index.tsx +++ b/app/src/atoms/Slideout/index.tsx @@ -19,14 +19,20 @@ import { import { Divider } from '../structure' import { StyledText } from '../text' +import { useTranslation } from 'react-i18next' +export interface MultiSlideoutSpecs { + currentStep: number + maxSteps: number +} export interface SlideoutProps { title: string | React.ReactElement children: React.ReactNode - onCloseClick: () => unknown + onCloseClick: () => void // isExpanded is for collapse and expand animation isExpanded?: boolean footer?: React.ReactNode + multiSlideoutSpecs?: MultiSlideoutSpecs } const SHARED_STYLE = css` @@ -108,10 +114,17 @@ const CLOSE_ICON_STYLE = css` ` export const Slideout = (props: SlideoutProps): JSX.Element => { - const { isExpanded, title, onCloseClick, children, footer } = props + const { + isExpanded, + title, + onCloseClick, + children, + footer, + multiSlideoutSpecs, + } = props + const { t } = useTranslation('shared') const slideOutRef = React.useRef(null) const [isReachedBottom, setIsReachedBottom] = React.useState(false) - const hasBeenExpanded = React.useRef(isExpanded ?? false) const handleScroll = (): void => { if (slideOutRef.current == null) return @@ -166,6 +179,19 @@ export const Slideout = (props: SlideoutProps): JSX.Element => { flexDirection={DIRECTION_COLUMN} justifyContent={JUSTIFY_SPACE_BETWEEN} > + {multiSlideoutSpecs === undefined ? null : ( + + {t('step', { + current: multiSlideoutSpecs.currentStep, + max: multiSlideoutSpecs.maxSteps, + })} + + )} {typeof title === 'string' ? ( { { const SUBMIT_INPUT_STYLE = css` background-color: ${COLORS.blue50}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8} ${SPACING.spacing16}; color: ${COLORS.white}; ${TYPOGRAPHY.pSemiBold} diff --git a/app/src/atoms/buttons/TabbedButton.tsx b/app/src/atoms/buttons/TabbedButton.tsx index 224f0f52e2a..6d4d8f7b967 100644 --- a/app/src/atoms/buttons/TabbedButton.tsx +++ b/app/src/atoms/buttons/TabbedButton.tsx @@ -45,7 +45,7 @@ interface TabbedButtonProps extends React.ComponentProps { export const TabbedButton = styled(Btn)` ${props => css` - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; box-shadow: none; font-size: ${TYPOGRAPHY.fontSize22}; font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; diff --git a/app/src/atoms/buttons/TertiaryButton.tsx b/app/src/atoms/buttons/TertiaryButton.tsx index a6ab30fb0ed..a44cdd3d61a 100644 --- a/app/src/atoms/buttons/TertiaryButton.tsx +++ b/app/src/atoms/buttons/TertiaryButton.tsx @@ -10,7 +10,7 @@ import { export const TertiaryButton = styled(NewPrimaryBtn)` background-color: ${COLORS.blue50}; - border-radius: ${BORDERS.radiusRoundEdge}; + border-radius: ${BORDERS.borderRadiusFull}; box-shadow: none; color: ${COLORS.white}; overflow: no-wrap; diff --git a/app/src/atoms/buttons/__tests__/FloatingActionButton.test.tsx b/app/src/atoms/buttons/__tests__/FloatingActionButton.test.tsx index d8f27ce0e0b..7e62b0f8662 100644 --- a/app/src/atoms/buttons/__tests__/FloatingActionButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/FloatingActionButton.test.tsx @@ -34,7 +34,7 @@ describe('FloatingActionButton', () => { expect(button).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSize28}`) expect(button).toHaveStyle(`font-weight: ${TYPOGRAPHY.fontWeightSemiBold}`) expect(button).toHaveStyle(`line-height: ${TYPOGRAPHY.lineHeight36}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize5}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadius40}`) expect(button).toHaveStyle( `text-transform: ${TYPOGRAPHY.textTransformNone}` ) diff --git a/app/src/atoms/buttons/__tests__/MediumButton.test.tsx b/app/src/atoms/buttons/__tests__/MediumButton.test.tsx index 456da8768b8..f4d23ea3a32 100644 --- a/app/src/atoms/buttons/__tests__/MediumButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/MediumButton.test.tsx @@ -93,7 +93,7 @@ describe('MediumButton', () => { } render(props) expect(screen.getByRole('button')).toHaveStyle( - `border-radius: ${BORDERS.borderRadiusSize5}` + `border-radius: ${BORDERS.borderRadius40}` ) }) }) diff --git a/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx b/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx index 116dc1c287d..978f46e3c08 100644 --- a/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/QuaternaryButton.test.tsx @@ -24,7 +24,7 @@ describe('QuaternaryButton', () => { render(props) const button = screen.getByText('secondary tertiary button') expect(button).toHaveStyle(`background-color: ${COLORS.white}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.radiusRoundEdge}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadiusFull}`) expect(button).toHaveStyle('box-shadow: 0 0 0') expect(button).toHaveStyle(`color: ${COLORS.blue50}`) expect(button).toHaveStyle( diff --git a/app/src/atoms/buttons/__tests__/SmallButton.test.tsx b/app/src/atoms/buttons/__tests__/SmallButton.test.tsx index b86a4939d74..2aa55acef6e 100644 --- a/app/src/atoms/buttons/__tests__/SmallButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/SmallButton.test.tsx @@ -28,7 +28,7 @@ describe('SmallButton', () => { `background-color: ${COLORS.blue60}` ) expect(screen.getByRole('button')).toHaveStyle( - `border-radius: ${BORDERS.borderRadiusSize4}` + `border-radius: ${BORDERS.borderRadius16}` ) }) it('renders the alert button', () => { @@ -82,7 +82,7 @@ describe('SmallButton', () => { } render(props) expect(screen.getByRole('button')).toHaveStyle( - `border-radius: ${BORDERS.borderRadiusSize5}` + `border-radius: ${BORDERS.borderRadius40}` ) }) it('renders an icon with start placement', () => { diff --git a/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx b/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx index 3a3d9a68435..40f61eeef13 100644 --- a/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/SubmitPrimaryButton.test.tsx @@ -29,7 +29,7 @@ describe('SubmitPrimaryButton', () => { render(props) const button = screen.getByText('submit primary button') expect(button).toHaveStyle(`background-color: ${COLORS.blue60}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.radiusSoftCorners}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) expect(button).toHaveStyle( `padding: ${SPACING.spacing8} ${SPACING.spacing16}` ) diff --git a/app/src/atoms/buttons/__tests__/TabbedButton.test.tsx b/app/src/atoms/buttons/__tests__/TabbedButton.test.tsx index c58596b2971..893b71ab904 100644 --- a/app/src/atoms/buttons/__tests__/TabbedButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/TabbedButton.test.tsx @@ -30,7 +30,7 @@ describe('Unselected TabbedButton', () => { expect(button).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSize22}`) expect(button).toHaveStyle(`font-weight: ${TYPOGRAPHY.fontWeightSemiBold}`) expect(button).toHaveStyle(`line-height: ${TYPOGRAPHY.lineHeight28}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize4}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadius16}`) expect(button).toHaveStyle( `text-transform: ${TYPOGRAPHY.textTransformNone}` ) @@ -75,7 +75,7 @@ describe('Selected TabbedButton', () => { expect(button).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSize22}`) expect(button).toHaveStyle(`font-weight: ${TYPOGRAPHY.fontWeightSemiBold}`) expect(button).toHaveStyle(`line-height: ${TYPOGRAPHY.lineHeight28}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadiusSize4}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadius16}`) expect(button).toHaveStyle( `text-transform: ${TYPOGRAPHY.textTransformNone}` ) diff --git a/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx b/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx index 488d5fa1aec..4c0b2b97a1e 100644 --- a/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx +++ b/app/src/atoms/buttons/__tests__/TertiaryButton.test.tsx @@ -29,7 +29,7 @@ describe('TertiaryButton', () => { expect(button).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSizeLabel}`) expect(button).toHaveStyle(`font-weight: ${TYPOGRAPHY.fontWeightSemiBold}`) expect(button).toHaveStyle(`line-height: ${TYPOGRAPHY.lineHeight12}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.radiusRoundEdge}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadiusFull}`) expect(button).toHaveStyle( `text-transform: ${TYPOGRAPHY.textTransformNone}` ) diff --git a/app/src/molecules/CardButton/index.tsx b/app/src/molecules/CardButton/index.tsx index ece8c803f8a..8a86d4de651 100644 --- a/app/src/molecules/CardButton/index.tsx +++ b/app/src/molecules/CardButton/index.tsx @@ -22,7 +22,7 @@ const CARD_BUTTON_STYLE = css` display: flex; flex-direction: ${DIRECTION_COLUMN}; align-items: ${ALIGN_CENTER}; - border-radius: ${BORDERS.borderRadiusSize5}; + border-radius: ${BORDERS.borderRadius40}; padding: ${SPACING.spacing32}; box-shadow: none; diff --git a/app/src/molecules/InfoMessage/index.tsx b/app/src/molecules/InfoMessage/index.tsx index dd576483c29..0d3c7174557 100644 --- a/app/src/molecules/InfoMessage/index.tsx +++ b/app/src/molecules/InfoMessage/index.tsx @@ -26,7 +26,7 @@ export function InfoMessage({ title, body }: InfoMessageProps): JSX.Element { backgroundColor={COLORS.blue30} flexDirection={DIRECTION_ROW} alignItems={body != null ? ALIGN_FLEX_START : ALIGN_CENTER} - borderRadius={BORDERS.radiusSoftCorners} + borderRadius={BORDERS.borderRadius4} gridGap={SPACING.spacing8} padding={SPACING.spacing16} data-testid={`InfoMessage_${title}`} diff --git a/app/src/molecules/InstrumentCard/MenuOverlay.tsx b/app/src/molecules/InstrumentCard/MenuOverlay.tsx index 6c1f5a37143..8578523f552 100644 --- a/app/src/molecules/InstrumentCard/MenuOverlay.tsx +++ b/app/src/molecules/InstrumentCard/MenuOverlay.tsx @@ -32,7 +32,7 @@ export function MenuOverlay(props: MenuOverlayProps): JSX.Element { return ( diff --git a/app/src/molecules/JogControls/StepSizeControl.tsx b/app/src/molecules/JogControls/StepSizeControl.tsx index b5f30c0a0cb..d53ae5de06d 100644 --- a/app/src/molecules/JogControls/StepSizeControl.tsx +++ b/app/src/molecules/JogControls/StepSizeControl.tsx @@ -174,7 +174,7 @@ export function TouchStepSizeControl(props: StepSizeControlProps): JSX.Element { flex="3" flexDirection={DIRECTION_COLUMN} border={`1px solid ${COLORS.grey50}`} - borderRadius={BORDERS.borderRadiusSize4} + borderRadius={BORDERS.borderRadius16} padding={SPACING.spacing16} gridGap={SPACING.spacing16} > diff --git a/app/src/molecules/JogControls/TouchControlButton.tsx b/app/src/molecules/JogControls/TouchControlButton.tsx index 10422172381..0ad85f1de7d 100644 --- a/app/src/molecules/JogControls/TouchControlButton.tsx +++ b/app/src/molecules/JogControls/TouchControlButton.tsx @@ -7,7 +7,7 @@ export const TouchControlButton = styled.button<{ selected: boolean }>` background-color: ${({ selected }) => selected ? COLORS.blue50 : COLORS.blue35}; cursor: default; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; box-shadow: none; padding: ${SPACING.spacing8} ${SPACING.spacing20}; diff --git a/app/src/molecules/LegacyModal/LegacyModalShell.tsx b/app/src/molecules/LegacyModal/LegacyModalShell.tsx index c97ab700582..9142ea14cbf 100644 --- a/app/src/molecules/LegacyModal/LegacyModalShell.tsx +++ b/app/src/molecules/LegacyModal/LegacyModalShell.tsx @@ -107,12 +107,12 @@ const ModalArea = styled.div< overflow-y: ${OVERFLOW_AUTO}; max-height: 100%; width: 100%; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; box-shadow: ${BORDERS.smallDropShadow}; height: ${({ isFullPage }) => (isFullPage ? '100%' : 'auto')}; background-color: ${COLORS.white}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; } ${styleProps}; ` diff --git a/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx b/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx index c1538b3fd46..536f8dc0b37 100644 --- a/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx +++ b/app/src/molecules/MiniCard/__tests__/MiniCard.test.tsx @@ -27,7 +27,7 @@ describe('MiniCard', () => { const miniCard = screen.getByText('mock mini card') expect(miniCard).toHaveStyle(`background-color: ${COLORS.grey10}`) expect(miniCard).toHaveStyle(`border: 1px solid ${COLORS.grey35}`) - expect(miniCard).toHaveStyle(`border-radius: ${BORDERS.radiusSoftCorners}`) + expect(miniCard).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) expect(miniCard).toHaveStyle(`padding: ${SPACING.spacing8}`) expect(miniCard).toHaveStyle(`width: 100%`) expect(miniCard).toHaveStyle(`cursor: pointer`) @@ -39,7 +39,7 @@ describe('MiniCard', () => { const miniCard = screen.getByText('mock mini card') expect(miniCard).toHaveStyle(`background-color: ${COLORS.blue10}`) expect(miniCard).toHaveStyle(`border: 1px solid ${COLORS.blue50}`) - expect(miniCard).toHaveStyle(`border-radius: ${BORDERS.radiusSoftCorners}`) + expect(miniCard).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) expect(miniCard).toHaveStyle(`padding: ${SPACING.spacing8}`) expect(miniCard).toHaveStyle(`width: 100%`) expect(miniCard).toHaveStyle(`cursor: pointer`) @@ -52,7 +52,7 @@ describe('MiniCard', () => { const miniCard = screen.getByText('mock mini card') expect(miniCard).toHaveStyle(`background-color: ${COLORS.red20}`) expect(miniCard).toHaveStyle(`border: 1px solid ${COLORS.red50}`) - expect(miniCard).toHaveStyle(`border-radius: ${BORDERS.radiusSoftCorners}`) + expect(miniCard).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) expect(miniCard).toHaveStyle(`padding: ${SPACING.spacing8}`) expect(miniCard).toHaveStyle(`width: 100%`) expect(miniCard).toHaveStyle(`cursor: pointer`) diff --git a/app/src/molecules/MiniCard/index.tsx b/app/src/molecules/MiniCard/index.tsx index 1b7dd584f6a..2ae0f6724ad 100644 --- a/app/src/molecules/MiniCard/index.tsx +++ b/app/src/molecules/MiniCard/index.tsx @@ -14,7 +14,7 @@ interface MiniCardProps extends StyleProps { const unselectedOptionStyles = css` background-color: ${COLORS.white}; border: 1px solid ${COLORS.grey30}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8}; width: 100%; cursor: pointer; diff --git a/app/src/molecules/Modal/Modal.stories.tsx b/app/src/molecules/Modal/Modal.stories.tsx index 7060d710fdb..e29a6197224 100644 --- a/app/src/molecules/Modal/Modal.stories.tsx +++ b/app/src/molecules/Modal/Modal.stories.tsx @@ -30,7 +30,7 @@ Default.args = { }, children: ( diff --git a/app/src/molecules/Modal/Modal.tsx b/app/src/molecules/Modal/Modal.tsx index 42c803049d7..f51293c015d 100644 --- a/app/src/molecules/Modal/Modal.tsx +++ b/app/src/molecules/Modal/Modal.tsx @@ -61,7 +61,7 @@ export function Modal(props: ModalProps): JSX.Element { width={modalWidth} height="max-content" maxHeight="36.875rem" - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} boxShadow={BORDERS.shadowSmall} margin={SPACING.spacing32} flexDirection={DIRECTION_COLUMN} @@ -80,8 +80,8 @@ export function Modal(props: ModalProps): JSX.Element { paddingTop={header != null ? '0rem' : SPACING.spacing32} borderRadius={ header != null - ? `0px 0px ${BORDERS.borderRadiusSize3} ${BORDERS.borderRadiusSize3}` - : BORDERS.borderRadiusSize3 + ? `0px 0px ${BORDERS.borderRadius12} ${BORDERS.borderRadius12}` + : BORDERS.borderRadius12 } maxHeight="30.625rem" {...styleProps} diff --git a/app/src/molecules/Modal/ModalHeader.tsx b/app/src/molecules/Modal/ModalHeader.tsx index b62e592d537..7d73adc3468 100644 --- a/app/src/molecules/Modal/ModalHeader.tsx +++ b/app/src/molecules/Modal/ModalHeader.tsx @@ -32,7 +32,7 @@ export function ModalHeader(props: ModalHeaderBaseProps): JSX.Element { flexDirection={DIRECTION_ROW} justifyContent={JUSTIFY_SPACE_BETWEEN} alignItems={ALIGN_CENTER} - borderRadius={`${BORDERS.borderRadiusSize3} ${BORDERS.borderRadiusSize3} 0px 0px`} + borderRadius={`${BORDERS.borderRadius12} ${BORDERS.borderRadius12} 0px 0px`} {...styleProps} > diff --git a/app/src/molecules/NavTab/index.tsx b/app/src/molecules/NavTab/index.tsx index 75dea82b9c1..97d6e4a9f12 100644 --- a/app/src/molecules/NavTab/index.tsx +++ b/app/src/molecules/NavTab/index.tsx @@ -1,9 +1,15 @@ import * as React from 'react' +import styled, { css } from 'styled-components' import { NavLink } from 'react-router-dom' -import styled from 'styled-components' import { BORDERS, COLORS, SPACING, TYPOGRAPHY } from '@opentrons/components' +export const TAB_BORDER_STYLE = css` + border-bottom-style: ${BORDERS.styleSolid}; + border-bottom-width: 2px; + border-bottom-color: ${COLORS.purple50}; +` + interface NavTabProps { to: string tabName: string @@ -17,7 +23,7 @@ const StyledNavLink = styled(NavLink)>` &.active { color: ${COLORS.black90}; - ${BORDERS.tabBorder} + ${TAB_BORDER_STYLE} } ` const DisabledNavLink = styled.span` diff --git a/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx b/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx index 9844e1cc416..00862359a37 100644 --- a/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx +++ b/app/src/molecules/PythonLabwareOffsetSnippet/index.tsx @@ -13,7 +13,7 @@ const JsonTextArea = styled.textarea` min-height: 28vh; width: 100%; background-color: ${COLORS.grey30}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; padding: ${SPACING.spacing8}; margin: ${SPACING.spacing16} 0; font-size: ${TYPOGRAPHY.fontSizeCaption}; diff --git a/app/src/molecules/ToggleGroup/useToggleGroup.tsx b/app/src/molecules/ToggleGroup/useToggleGroup.tsx index 841e471dd0c..0dd67d5ca58 100644 --- a/app/src/molecules/ToggleGroup/useToggleGroup.tsx +++ b/app/src/molecules/ToggleGroup/useToggleGroup.tsx @@ -10,7 +10,7 @@ import { import { useTrackEvent } from '../../redux/analytics' const BUTTON_GROUP_STYLES = css` - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; margin-top: -1px; width: fit-content; @@ -47,12 +47,12 @@ const BUTTON_GROUP_STYLES = css` } button:first-child { - border-radius: ${BORDERS.radiusSoftCorners} 0 0 ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4} 0 0 ${BORDERS.borderRadius8}; border-right: none; } button:last-child { - border-radius: 0 ${BORDERS.radiusSoftCorners} ${BORDERS.radiusSoftCorners} 0; + border-radius: 0 ${BORDERS.borderRadius4} ${BORDERS.borderRadius8} 0; border-left: none; } ` diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index d794cc4df76..68be10dc49c 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -24,7 +24,7 @@ const StyledLabel = styled.label` width: 100%; padding: ${SPACING.spacing32}; border: 2px dashed ${COLORS.grey30}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; text-align: center; background-color: ${COLORS.white}; diff --git a/app/src/molecules/WizardHeader/index.tsx b/app/src/molecules/WizardHeader/index.tsx index d1f28588988..867a1d2eda6 100644 --- a/app/src/molecules/WizardHeader/index.tsx +++ b/app/src/molecules/WizardHeader/index.tsx @@ -48,7 +48,7 @@ const EXIT_BUTTON_STYLE = css` const BOX_STYLE = css` background-color: ${COLORS.white} @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; } ` const HEADER_CONTAINER_STYLE = css` @@ -57,7 +57,7 @@ const HEADER_CONTAINER_STYLE = css` padding: ${SPACING.spacing16} ${SPACING.spacing32}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: 1.75rem ${SPACING.spacing32}; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; } ` const HEADER_TEXT_STYLE = css` diff --git a/app/src/molecules/WizardRequiredEquipmentList/index.tsx b/app/src/molecules/WizardRequiredEquipmentList/index.tsx index 94308b5dc0f..fbf775b7c45 100644 --- a/app/src/molecules/WizardRequiredEquipmentList/index.tsx +++ b/app/src/molecules/WizardRequiredEquipmentList/index.tsx @@ -53,7 +53,7 @@ export function WizardRequiredEquipmentList( {equipmentList.map((requiredEquipmentProps, index) => ( diff --git a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx b/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx index b442cef4b41..75c34f1f843 100644 --- a/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx +++ b/app/src/organisms/ApplyHistoricOffsets/hooks/__tests__/useOffsetCandidatesForAnalysis.test.tsx @@ -19,6 +19,8 @@ import type { OffsetCandidate } from '../useOffsetCandidatesForAnalysis' vi.mock('../useAllHistoricOffsets') vi.mock('../getLabwareLocationCombos') vi.mock('@opentrons/shared-data') +vi.mock('../../../../resources/runs') +vi.mock('../../../../resources/useNotifyService') const mockLabwareDef = fixtureTiprack300ul as LabwareDefinition2 diff --git a/app/src/organisms/CalibrationStatusCard/index.tsx b/app/src/organisms/CalibrationStatusCard/index.tsx index 29e15b64510..db2f2e36076 100644 --- a/app/src/organisms/CalibrationStatusCard/index.tsx +++ b/app/src/organisms/CalibrationStatusCard/index.tsx @@ -62,7 +62,7 @@ export function CalibrationStatusCard({ alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN} border={BORDERS.lineBorder} - borderRadius={BORDERS.radiusSoftCorners} + borderRadius={BORDERS.borderRadius4} padding={SPACING.spacing16} > { return filePath.split('/').reverse()[0] } @@ -360,7 +370,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { minHeight="11rem" padding={SPACING.spacing16} css={css` - ${BORDERS.cardOutlineBorder} + ${CARD_OUTLINE_BORDER_STYLE} &:hover { border-color: ${COLORS.grey30}; } diff --git a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx index 721a7fbad49..cb277f6fb2a 100644 --- a/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx +++ b/app/src/organisms/ChooseRobotSlideout/AvailableRobotOption.tsx @@ -23,7 +23,7 @@ import { appShellRequestor } from '../../redux/shell/remote' import OT2_PNG from '../../assets/images/OT2-R_HERO.png' import FLEX_PNG from '../../assets/images/FLEX.png' import { RobotBusyStatusAction } from '.' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' import type { IconName } from '@opentrons/components' import type { Runs } from '@opentrons/api-client' diff --git a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx index 47aa465c62d..524c59d4f06 100644 --- a/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotSlideout/__tests__/ChooseRobotSlideout.test.tsx @@ -18,6 +18,7 @@ import { mockReachableRobot, mockUnreachableRobot, } from '../../../redux/discovery/__fixtures__' +import { useFeatureFlag } from '../../../redux/config' import { getNetworkInterfaces } from '../../../redux/networking' import { ChooseRobotSlideout } from '..' import { useNotifyService } from '../../../resources/useNotifyService' @@ -26,7 +27,7 @@ vi.mock('../../../redux/discovery') vi.mock('../../../redux/robot-update') vi.mock('../../../redux/networking') vi.mock('../../../resources/useNotifyService') - +vi.mock('../../../redux/config') const render = (props: React.ComponentProps) => { return renderWithProviders( @@ -42,6 +43,7 @@ const mockSetSelectedRobot = vi.fn() describe('ChooseRobotSlideout', () => { beforeEach(() => { + vi.mocked(useFeatureFlag).mockReturnValue(true) vi.mocked(getConnectableRobots).mockReturnValue([mockConnectableRobot]) vi.mocked(getUnreachableRobots).mockReturnValue([mockUnreachableRobot]) vi.mocked(getReachableRobots).mockReturnValue([mockReachableRobot]) @@ -127,6 +129,32 @@ describe('ChooseRobotSlideout', () => { expect(vi.mocked(startDiscovery)).toHaveBeenCalled() expect(dispatch).toHaveBeenCalledWith({ type: 'mockStartDiscovery' }) }) + it('renders the multi slideout page 1', () => { + render({ + onCloseClick: vi.fn(), + isExpanded: true, + isSelectedRobotOnDifferentSoftwareVersion: false, + selectedRobot: null, + setSelectedRobot: mockSetSelectedRobot, + title: 'choose robot slideout title', + robotType: 'OT-2 Standard', + multiSlideout: { currentPage: 1 }, + }) + screen.getByText('Step 1 / 2') + }) + it('renders the multi slideout page 2', () => { + render({ + onCloseClick: vi.fn(), + isExpanded: true, + isSelectedRobotOnDifferentSoftwareVersion: false, + selectedRobot: null, + setSelectedRobot: mockSetSelectedRobot, + title: 'choose robot slideout title', + robotType: 'OT-2 Standard', + multiSlideout: { currentPage: 2 }, + }) + screen.getByText('Step 2 / 2') + }) it('defaults to first available robot and allows an available robot to be selected', () => { vi.mocked(getConnectableRobots).mockReturnValue([ { ...mockConnectableRobot, name: 'otherRobot', ip: 'otherIp' }, diff --git a/app/src/organisms/ChooseRobotSlideout/index.tsx b/app/src/organisms/ChooseRobotSlideout/index.tsx index f9c9c37730c..4e2e1ed694c 100644 --- a/app/src/organisms/ChooseRobotSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotSlideout/index.tsx @@ -35,6 +35,7 @@ import { } from '../../redux/discovery' import { Banner } from '../../atoms/Banner' import { Slideout } from '../../atoms/Slideout' +import { MultiSlideout } from '../../atoms/Slideout/MultiSlideout' import { StyledText } from '../../atoms/text' import { AvailableRobotOption } from './AvailableRobotOption' @@ -43,6 +44,17 @@ import type { SlideoutProps } from '../../atoms/Slideout' import type { UseCreateRun } from '../../organisms/ChooseRobotToRunProtocolSlideout/useCreateRunFromProtocol' import type { State, Dispatch } from '../../redux/types' import type { Robot } from '../../redux/discovery/types' +import { useFeatureFlag } from '../../redux/config' + +export const CARD_OUTLINE_BORDER_STYLE = css` + border-style: ${BORDERS.styleSolid}; + border-width: 1px; + border-color: ${COLORS.grey30}; + border-radius: ${BORDERS.borderRadius8}; + &:hover { + border-color: ${COLORS.grey55}; + } +` interface RobotIsBusyAction { type: 'robotIsBusy' @@ -90,6 +102,7 @@ interface ChooseRobotSlideoutProps isAnalysisError?: boolean isAnalysisStale?: boolean showIdleOnly?: boolean + multiSlideout?: { currentPage: number } } export function ChooseRobotSlideout( @@ -112,7 +125,9 @@ export function ChooseRobotSlideout( setSelectedRobot, robotType, showIdleOnly = false, + multiSlideout, } = props + const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') const dispatch = useDispatch() const isScanning = useSelector((state: State) => getScanning(state)) @@ -171,145 +186,157 @@ export function ChooseRobotSlideout( const unavailableCount = unhealthyReachableRobots.length + unreachableRobots.length - return ( - - - {isAnalysisError ? ( - {t('protocol_failed_app_analysis')} - ) : null} - {isAnalysisStale ? ( - {t('protocol_outdated_app_analysis')} - ) : null} - - {isScanning ? ( - - - {t('app_settings:searching')} - - - - ) : ( - dispatch(startDiscovery())} - textTransform={TYPOGRAPHY.textTransformCapitalize} - role="button" - css={TYPOGRAPHY.linkPSemiBold} + const pageOneBody = ( + + {isAnalysisError ? ( + {t('protocol_failed_app_analysis')} + ) : null} + {isAnalysisStale ? ( + {t('protocol_outdated_app_analysis')} + ) : null} + + {isScanning ? ( + + - {t('shared:refresh')} - - )} - - {!isScanning && healthyReachableRobots.length === 0 ? ( - - - - {t('no_available_robots_found')} + {t('app_settings:searching')} + ) : ( - healthyReachableRobots.map(robot => { - const isSelected = - selectedRobot != null && selectedRobot.ip === robot.ip - return ( - - { - if (!isCreatingRun) { - resetCreateRun?.() - setSelectedRobot(robot) - } - }} - isError={runCreationError != null} - isSelected={isSelected} - isSelectedRobotOnDifferentSoftwareVersion={ - isSelectedRobotOnDifferentSoftwareVersion - } - showIdleOnly={showIdleOnly} - registerRobotBusyStatus={registerRobotBusyStatus} - /> - {runCreationError != null && isSelected && ( - - {runCreationErrorCode === 409 ? ( - - ), - }} - /> - ) : ( - runCreationError - )} - - )} - - ) - }) - )} - {!isScanning && unavailableCount > 0 ? ( - dispatch(startDiscovery())} + textTransform={TYPOGRAPHY.textTransformCapitalize} + role="button" + css={TYPOGRAPHY.linkPSemiBold} > - - {showIdleOnly - ? t('unavailable_or_busy_robot_not_listed', { - count: unavailableCount + reducerBusyCount, - }) - : t('unavailable_robot_not_listed', { - count: unavailableCount, - })} - - - {t('view_unavailable_robots')} - - - ) : null} + {t('shared:refresh')} + + )} + {!isScanning && healthyReachableRobots.length === 0 ? ( + + + + {t('no_available_robots_found')} + + + ) : ( + healthyReachableRobots.map(robot => { + const isSelected = + selectedRobot != null && selectedRobot.ip === robot.ip + return ( + + { + if (!isCreatingRun) { + resetCreateRun?.() + setSelectedRobot(robot) + } + }} + isError={runCreationError != null} + isSelected={isSelected} + isSelectedRobotOnDifferentSoftwareVersion={ + isSelectedRobotOnDifferentSoftwareVersion + } + showIdleOnly={showIdleOnly} + registerRobotBusyStatus={registerRobotBusyStatus} + /> + {runCreationError != null && isSelected && ( + + {runCreationErrorCode === 409 ? ( + + ), + }} + /> + ) : ( + runCreationError + )} + + )} + + ) + }) + )} + {!isScanning && unavailableCount > 0 ? ( + + + {showIdleOnly + ? t('unavailable_or_busy_robot_not_listed', { + count: unavailableCount + reducerBusyCount, + }) + : t('unavailable_robot_not_listed', { + count: unavailableCount, + })} + + + {t('view_unavailable_robots')} + + + ) : null} + + ) + + const pageTwoBody = TODO + + return multiSlideout != null && enableRunTimeParametersFF ? ( + + {multiSlideout.currentPage === 1 ? pageOneBody : pageTwoBody} + + ) : ( + + {pageOneBody} ) } diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx index 70b54a106ce..3e9e437bbc4 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/__tests__/ChooseRobotToRunProtocolSlideout.test.tsx @@ -19,6 +19,7 @@ import { getUnreachableRobots, startDiscovery, } from '../../../redux/discovery' +import { useFeatureFlag } from '../../../redux/config' import { getRobotUpdateDisplayInfo } from '../../../redux/robot-update' import { mockConnectableRobot, @@ -44,6 +45,7 @@ vi.mock('../../../redux/config') vi.mock('../useCreateRunFromProtocol') vi.mock('../../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis') vi.mock('../../../resources/useNotifyService') +vi.mock('../../../redux/config') const render = ( props: React.ComponentProps @@ -70,6 +72,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { mockTrackCreateProtocolRunEvent = vi.fn( () => new Promise(resolve => resolve({})) ) + vi.mocked(useFeatureFlag).mockReturnValue(true) vi.mocked(getRobotUpdateDisplayInfo).mockReturnValue({ autoUpdateAction: '', autoUpdateDisabledReason: null, @@ -183,16 +186,17 @@ describe('ChooseRobotToRunProtocolSlideout', () => { showSlideout: true, }) const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) - expect(proceedButton).not.toBeDisabled() + const otherRobot = screen.getByText('otherRobot') fireEvent.click(otherRobot) // unselect default robot - expect(proceedButton).not.toBeDisabled() const mockRobot = screen.getByText('opentrons-robot-name') fireEvent.click(mockRobot) - expect(proceedButton).not.toBeDisabled() fireEvent.click(proceedButton) + const confirm = screen.getByRole('button', { name: 'Confirm values' }) + expect(confirm).not.toBeDisabled() + fireEvent.click(confirm) expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({ files: [expect.any(File)], protocolKey: storedProtocolDataFixture.protocolKey, @@ -211,7 +215,7 @@ describe('ChooseRobotToRunProtocolSlideout', () => { showSlideout: true, }) const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) expect(proceedButton).toBeDisabled() screen.getByText( @@ -235,15 +239,17 @@ describe('ChooseRobotToRunProtocolSlideout', () => { showSlideout: true, }) const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) fireEvent.click(proceedButton) + fireEvent.click(screen.getByRole('button', { name: 'Confirm values' })) expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({ files: [expect.any(File)], protocolKey: storedProtocolDataFixture.protocolKey, }) expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - expect(screen.getByText('run creation error')).toBeInTheDocument() + // TODO( jr, 3.13.24): fix this when page 2 is completed of the multislideout + // expect(screen.getByText('run creation error')).toBeInTheDocument() }) it('renders error state when run creation error code is 409', () => { @@ -260,20 +266,22 @@ describe('ChooseRobotToRunProtocolSlideout', () => { showSlideout: true, }) const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) + const link = screen.getByRole('link', { name: 'Go to Robot' }) + fireEvent.click(link) + expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name') fireEvent.click(proceedButton) + fireEvent.click(screen.getByRole('button', { name: 'Confirm values' })) expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({ files: [expect.any(File)], protocolKey: storedProtocolDataFixture.protocolKey, }) expect(mockTrackCreateProtocolRunEvent).toHaveBeenCalled() - screen.getByText( - 'This robot is busy and can’t run this protocol right now.' - ) - const link = screen.getByRole('link', { name: 'Go to Robot' }) - fireEvent.click(link) - expect(link.getAttribute('href')).toEqual('/devices/opentrons-robot-name') + // TODO( jr, 3.13.24): fix this when page 2 is completed of the multislideout + // screen.getByText( + // 'This robot is busy and can’t run this protocol right now.' + // ) }) it('renders apply historic offsets as determinate if candidates available', () => { @@ -311,9 +319,10 @@ describe('ChooseRobotToRunProtocolSlideout', () => { ) expect(screen.getByRole('checkbox')).toBeChecked() const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) fireEvent.click(proceedButton) + fireEvent.click(screen.getByRole('button', { name: 'Confirm values' })) expect(mockCreateRunFromProtocolSource).toHaveBeenCalledWith({ files: [expect.any(File)], protocolKey: storedProtocolDataFixture.protocolKey, @@ -350,9 +359,10 @@ describe('ChooseRobotToRunProtocolSlideout', () => { expect(screen.getByRole('checkbox')).toBeChecked() const proceedButton = screen.getByRole('button', { - name: 'Proceed to setup', + name: 'Continue to parameters', }) fireEvent.click(proceedButton) + fireEvent.click(screen.getByRole('button', { name: 'Confirm values' })) expect(vi.mocked(useCreateRunFromProtocol)).nthCalledWith( 2, expect.any(Object), diff --git a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx index fab0fbcd756..4e37afe28b0 100644 --- a/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/ChooseRobotToRunProtocolSlideout/index.tsx @@ -8,13 +8,16 @@ import { Icon, Flex, DIRECTION_COLUMN, - SIZE_1, PrimaryButton, + DIRECTION_ROW, + SecondaryButton, + SPACING, } from '@opentrons/components' import { getRobotUpdateDisplayInfo } from '../../redux/robot-update' import { OPENTRONS_USB } from '../../redux/discovery' import { appShellRequestor } from '../../redux/shell/remote' +import { useFeatureFlag } from '../../redux/config' import { useTrackCreateProtocolRunEvent } from '../Devices/hooks' import { ApplyHistoricOffsets } from '../ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '../ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' @@ -50,7 +53,8 @@ export function ChooseRobotToRunProtocolSlideoutComponent( srcFiles, mostRecentAnalysis, } = storedProtocolData - + const enableRunTimeParametersFF = useFeatureFlag('enableRunTimeParameters') + const [currentPage, setCurrentPage] = React.useState(1) const [selectedRobot, setSelectedRobot] = React.useState(null) const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( storedProtocolData, @@ -140,8 +144,27 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ? mostRecentAnalysis?.robotType ?? null : null + const SinglePageButtonWithoutFF = ( + + {isCreatingRun ? ( + + ) : ( + t('shared:proceed_to_setup') + )} + + ) + return ( - - - {isCreatingRun ? ( - + {enableRunTimeParametersFF ? ( + currentPage === 1 ? ( + <> + + setCurrentPage(2)} + width="100%" + disabled={ + isCreatingRun || + selectedRobot == null || + isSelectedRobotOnDifferentSoftwareVersion + } + > + {t('shared:continue_to_param')} + + ) : ( - t('shared:proceed_to_setup') - )} - + + setCurrentPage(1)} width="50%"> + {t('shared:change_robot')} + + + {isCreatingRun ? ( + + ) : ( + t('shared:confirm_values') + )} + + + ) + ) : ( + SinglePageButtonWithoutFF + )} } selectedRobot={selectedRobot} diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 85ce0f3128c..9221e4a4d26 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -216,7 +216,7 @@ export function AddFixtureModal({ justifyContent={JUSTIFY_SPACE_BETWEEN} padding={`${SPACING.spacing8} ${SPACING.spacing16}`} backgroundColor={COLORS.grey20} - borderRadius={BORDERS.borderRadiusSize1} + borderRadius={BORDERS.borderRadius8} > {fixtureDisplayName} @@ -253,7 +253,7 @@ export function AddFixtureModal({ const FIXTURE_BUTTON_STYLE = css` background-color: ${COLORS.grey35}; cursor: default; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadiusFull}; box-shadow: none; &:focus { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx index 00464783c23..f3b008320af 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/__tests__/DeviceDetailsDeckConfiguration.test.tsx @@ -15,7 +15,7 @@ import { useIsRobotViewable, useRunStatuses } from '../../Devices/hooks' import { DeckFixtureSetupInstructionsModal } from '../DeckFixtureSetupInstructionsModal' import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import { DeviceDetailsDeckConfiguration } from '../' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import type { MaintenanceRun } from '@opentrons/api-client' import type * as OpentronsComponents from '@opentrons/components' @@ -30,7 +30,7 @@ vi.mock('@opentrons/components', async importOriginal => { vi.mock('@opentrons/react-api-client') vi.mock('../DeckFixtureSetupInstructionsModal') vi.mock('../../Devices/hooks') -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') vi.mock('../../../resources/devices/hooks/useIsEstopNotDisengaged') const ROBOT_NAME = 'otie' diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index 4e51bd06fb0..70d25116bca 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -31,7 +31,7 @@ import { SINGLE_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { StyledText } from '../../atoms/text' import { Banner } from '../../atoms/Banner' import { DeckFixtureSetupInstructionsModal } from './DeckFixtureSetupInstructionsModal' @@ -118,7 +118,7 @@ export function DeviceDetailsDeckConfiguration({ { return ( diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index e80bf386580..eb0f37da7b2 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -14,7 +14,7 @@ import { RUN_STATUS_FINISHING, RUN_STATUS_SUCCEEDED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, - RunStatus, + RUN_STATUS_AWAITING_RECOVERY, } from '@opentrons/api-client' import { useModulesQuery, @@ -107,9 +107,9 @@ import { getIsFixtureMismatch } from '../../../resources/deck_configuration/util import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useMostRecentRunId } from '../../ProtocolUpload/hooks/useMostRecentRunId' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' -import type { Run, RunError } from '@opentrons/api-client' +import type { Run, RunError, RunStatus } from '@opentrons/api-client' import type { State } from '../../../redux/types' import type { HeaterShakerModule } from '../../../redux/modules/types' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -126,6 +126,7 @@ const CANCELLABLE_STATUSES = [ RUN_STATUS_PAUSE_REQUESTED, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_IDLE, + RUN_STATUS_AWAITING_RECOVERY, ] const RUN_OVER_STATUSES: RunStatus[] = [ RUN_STATUS_FAILED, @@ -325,7 +326,7 @@ export function ProtocolRunHeader({ - + {getDisplayLocation( offset.location, getLabwareDefinitionsFromCommands(commands), @@ -97,7 +103,12 @@ export function CurrentOffsetsTable( )} {labwareDisplayName} - + diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx index abae4830e68..98bfe60da4a 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx @@ -24,7 +24,7 @@ import { useRobotType, } from '../../../hooks' import { SetupLabwarePositionCheck } from '..' -import { useNotifyRunQuery } from '../../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../../resources/runs' import type { Mock } from 'vitest' @@ -35,7 +35,7 @@ vi.mock('../../../../../redux/config') vi.mock('../../../hooks') vi.mock('../../../hooks/useLPCSuccessToast') vi.mock('@opentrons/react-api-client') -vi.mock('../../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../../resources/runs') const DISABLED_REASON = 'MOCK_DISABLED_REASON' const ROBOT_NAME = 'otie' diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index 383aa273588..a6e2ce6da0d 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { Flex, + BORDERS, SPACING, JUSTIFY_CENTER, DIRECTION_COLUMN, @@ -26,7 +27,7 @@ import { CurrentOffsetsTable } from './CurrentOffsetsTable' import { useLaunchLPC } from '../../../LabwarePositionCheck/useLaunchLPC' import { StyledText } from '../../../../atoms/text' import { getLatestCurrentOffsets } from './utils' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { LabwareOffset } from '@opentrons/api-client' @@ -104,6 +105,7 @@ export function SetupLabwarePositionCheck( backgroundColor={COLORS.grey10} alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_CENTER} + borderRadius={BORDERS.borderRadius8} > {i18n.format(t('no_labware_offset_data'), 'capitalize')} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/LiquidDetailCard.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/LiquidDetailCard.tsx index 1359c827565..391ae829456 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/LiquidDetailCard.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/LiquidDetailCard.tsx @@ -27,18 +27,28 @@ import { import { getIsOnDevice } from '../../../../redux/config' import { getWellRangeForLiquidLabwarePair } from './utils' +export const CARD_OUTLINE_BORDER_STYLE = css` + border-style: ${BORDERS.styleSolid}; + border-width: 1px; + border-color: ${COLORS.grey30}; + border-radius: ${BORDERS.borderRadius8}; + &:hover { + border-color: ${COLORS.grey55}; + } +` + const LIQUID_CARD_STYLE = css` - ${BORDERS.cardOutlineBorder} + ${CARD_OUTLINE_BORDER_STYLE} &:hover { border: 1px solid ${COLORS.grey60}; - border-radius: ${BORDERS.borderRadiusSize1}; + border-radius: ${BORDERS.borderRadius8}; cursor: pointer; } ` const LIQUID_CARD_ODD_STYLE = css` border-color: ${COLORS.grey30}; border: ${SPACING.spacing4} solid ${COLORS.grey30}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius12}; ` interface LiquidDetailCardProps { liquidId: string @@ -70,8 +80,8 @@ export function LiquidDetailCard(props: LiquidDetailCardProps): JSX.Element { background-color: ${isOnDevice ? COLORS.blue30 : COLORS.blue10}; border: ${isOnDevice ? SPACING.spacing4 : `1px`} solid ${COLORS.blue50}; border-radius: ${isOnDevice - ? BORDERS.borderRadiusSize3 - : BORDERS.borderRadiusSize1}; + ? BORDERS.borderRadius12 + : BORDERS.borderRadius8}; ` const volumePerWellRange = getWellRangeForLiquidLabwarePair( volumeByWell, @@ -89,7 +99,7 @@ export function LiquidDetailCard(props: LiquidDetailCardProps): JSX.Element { return isOnDevice ? ( setSelectedValue(liquidId)} width="19.875rem" @@ -98,8 +108,8 @@ export function LiquidDetailCard(props: LiquidDetailCardProps): JSX.Element { > ) => { return renderWithProviders(, { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index 43869cd6f98..a5057f07bc4 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -185,7 +185,7 @@ export const LocationConflictModal = ( flexDirection={DIRECTION_ROW} alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_SPACE_BETWEEN} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius8} > {t('protocol_specifies')} @@ -201,7 +201,7 @@ export const LocationConflictModal = ( flexDirection={DIRECTION_ROW} justifyContent={JUSTIFY_SPACE_BETWEEN} alignItems={ALIGN_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius8} > {t('currently_configured')} @@ -288,6 +288,7 @@ export const LocationConflictModal = ( flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing20} alignItems={ALIGN_CENTER} + borderRadius={BORDERS.borderRadius8} > {t('protocol_specifies')} @@ -302,6 +303,7 @@ export const LocationConflictModal = ( flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing20} alignItems={ALIGN_CENTER} + borderRadius={BORDERS.borderRadius8} > {t('currently_configured')} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx index 12988f521b6..ad011257454 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/NotConfiguredModal.tsx @@ -62,7 +62,7 @@ export const NotConfiguredModal = ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx index adf3a9575b1..97e4460aae1 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupFixtureList.tsx @@ -168,7 +168,7 @@ export function FixtureListItem({ border={BORDERS.styleSolid} borderColor={COLORS.grey30} borderWidth="1px" - borderRadius={BORDERS.radiusSoftCorners} + borderRadius={BORDERS.borderRadius4} padding={SPACING.spacing16} backgroundColor={COLORS.white} > diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index ca4413bb5e9..ea05e8d4550 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -35,7 +35,7 @@ import { TertiaryButton } from '../../../../atoms/buttons' import { StatusLabel } from '../../../../atoms/StatusLabel' import { StyledText } from '../../../../atoms/text' import { Tooltip } from '../../../../atoms/Tooltip' -import { useChainLiveCommands } from '../../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../../resources/runs' import { ModuleSetupModal } from '../../../ModuleCard/ModuleSetupModal' import { ModuleWizardFlows } from '../../../ModuleWizardFlows' import { getModulePrepCommands } from '../../getModulePrepCommands' @@ -373,7 +373,7 @@ export function ModulesListItem({ border={BORDERS.styleSolid} borderColor={COLORS.grey30} borderWidth="1px" - borderRadius={BORDERS.radiusSoftCorners} + borderRadius={BORDERS.borderRadius4} padding={SPACING.spacing16} backgroundColor={COLORS.white} > diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index c772a7acbab..05df2fc9cef 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -14,7 +14,7 @@ import { mockMagneticModuleGen2, mockThermocycler, } from '../../../../../redux/modules/__fixtures__' -import { useChainLiveCommands } from '../../../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../../../resources/runs' import { ModuleSetupModal } from '../../../../ModuleCard/ModuleSetupModal' import { ModuleWizardFlows } from '../../../../ModuleWizardFlows' import { @@ -38,7 +38,7 @@ vi.mock('../UnMatchedModuleWarning') vi.mock('../../../../ModuleCard/ModuleSetupModal') vi.mock('../../../../ModuleWizardFlows') vi.mock('../MultipleModulesModal') -vi.mock('../../../../../resources/runs/hooks') +vi.mock('../../../../../resources/runs') vi.mock('../../../../../redux/config') const ROBOT_NAME = 'otie' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index b2359f77dba..65ea98c906f 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -95,7 +95,7 @@ import { getIsFixtureMismatch } from '../../../../resources/deck_configuration/u import { useDeckConfigurationCompatibility } from '../../../../resources/deck_configuration/hooks' import { useMostRecentCompletedAnalysis } from '../../../LabwarePositionCheck/useMostRecentCompletedAnalysis' import { useMostRecentRunId } from '../../../ProtocolUpload/hooks/useMostRecentRunId' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { UseQueryResult } from 'react-query' import type * as ReactRouterDom from 'react-router-dom' import type { Mock } from 'vitest' @@ -148,7 +148,7 @@ vi.mock('../../../../resources/deck_configuration/utils') vi.mock('../../../../resources/deck_configuration/hooks') vi.mock('../../../LabwarePositionCheck/useMostRecentCompletedAnalysis') vi.mock('../../../ProtocolUpload/hooks/useMostRecentRunId') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const ROBOT_NAME = 'otie' const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 15f2dd374c5..92dc247b922 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -40,7 +40,7 @@ import { SetupLiquids } from '../SetupLiquids' import { SetupModuleAndDeck } from '../SetupModuleAndDeck' import { EmptySetupStep } from '../EmptySetupStep' import { ProtocolRunSetup } from '../ProtocolRunSetup' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type * as SharedData from '@opentrons/shared-data' diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx index bea43391bb9..12ee29a86cd 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/SetupPipetteCalibration.test.tsx @@ -9,13 +9,13 @@ import { mockTipRackDefinition } from '../../../../redux/custom-labware/__fixtur import { useRunPipetteInfoByMount } from '../../hooks' import { SetupPipetteCalibrationItem } from '../SetupPipetteCalibrationItem' import { SetupInstrumentCalibration } from '../SetupInstrumentCalibration' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { PipetteInfo } from '../../hooks' vi.mock('../../hooks') vi.mock('../SetupPipetteCalibrationItem') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const ROBOT_NAME = 'otie' const RUN_ID = '1' diff --git a/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts b/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts index 07d4c838b85..f352ee2e40d 100644 --- a/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts +++ b/app/src/organisms/Devices/ProtocolRun/useLabwareOffsetForLabware.ts @@ -1,9 +1,9 @@ import { getLoadedLabwareDefinitionsByUri } from '@opentrons/shared-data' -import { getCurrentOffsetForLabwareInLocation } from '../../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' -import { getLabwareDefinitionUri } from '../../Devices/ProtocolRun/utils/getLabwareDefinitionUri' -import { getLabwareOffsetLocation } from '../../Devices/ProtocolRun/utils/getLabwareOffsetLocation' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { getCurrentOffsetForLabwareInLocation } from './utils/getCurrentOffsetForLabwareInLocation' +import { getLabwareDefinitionUri } from './utils/getLabwareDefinitionUri' +import { getLabwareOffsetLocation } from './utils/getLabwareOffsetLocation' +import { useNotifyRunQuery } from '../../../resources/runs' import type { LabwareOffset } from '@opentrons/api-client' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' diff --git a/app/src/organisms/Devices/RecentProtocolRuns.tsx b/app/src/organisms/Devices/RecentProtocolRuns.tsx index 4b07081e48d..245aa77f9a3 100644 --- a/app/src/organisms/Devices/RecentProtocolRuns.tsx +++ b/app/src/organisms/Devices/RecentProtocolRuns.tsx @@ -19,7 +19,7 @@ import { StyledText } from '../../atoms/text' import { useCurrentRunId } from '../ProtocolUpload/hooks' import { HistoricalProtocolRun } from './HistoricalProtocolRun' import { useIsRobotViewable, useRunStatuses } from './hooks' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' interface RecentProtocolRunsProps { robotName: string @@ -41,7 +41,7 @@ export function RecentProtocolRuns({ {t('run_a_protocol')} @@ -103,6 +109,9 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { as={Link} textTransform={TYPOGRAPHY.textTransformCapitalize} id={`RobotOverflowMenu_${robot.name}_robotSettings`} + css={css` + border-radius: 0 0 ${BORDERS.borderRadius8} ${BORDERS.borderRadius8}; + `} > {t('robot_settings')} @@ -115,6 +124,9 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { as={Link} textTransform={TYPOGRAPHY.textTransformCapitalize} id={`RobotOverflowMenu_${robot.name}_robotSettings_${runId}`} + css={css` + border-radius: ${BORDERS.borderRadius8}; + `} > {t('robot_settings')} @@ -125,12 +137,18 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { {t('why_is_this_robot_unavailable')} dispatch(removeRobot(robot.name))} id={`RobotOverflowMenu_${String(robot.name)}_removeRobot`} + css={css` + border-radius: 0 0 ${BORDERS.borderRadius8} ${BORDERS.borderRadius8}; + `} > {t('forget_unavailable_robot')} @@ -156,7 +174,7 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { @@ -113,7 +118,7 @@ export const RobotOverviewOverflowMenu = ( - {isRobotOnWrongVersionOfSoftware && - !isRobotUnavailable && - !isEstopNotDisengaged ? ( + {isUpdateSoftwareItemVisible ? ( handleUpdateBuildroot(robot)} data-testid={`RobotOverviewOverflowMenu_updateSoftware_${String( robot.name )}`} + css={css` + border-radius: ${BORDERS.borderRadius8} ${BORDERS.borderRadius8} + 0 0; + `} > {t('update_robot_software')} @@ -149,6 +156,14 @@ export const RobotOverviewOverflowMenu = ( isEstopNotDisengaged } data-testid={`RobotOverflowMenu_${robot.name}_runProtocol`} + css={ + !isUpdateSoftwareItemVisible + ? css` + border-radius: ${BORDERS.borderRadius8} + ${BORDERS.borderRadius8} 0 0; + ` + : undefined + } > {t('run_a_protocol')} @@ -199,6 +214,10 @@ export const RobotOverviewOverflowMenu = ( data-testid={`RobotOverviewOverflowMenu_robotSettings_${String( robot.name )}`} + css={css` + border-radius: 0 0 ${BORDERS.borderRadius8} + ${BORDERS.borderRadius8}; + `} > {t('robot_settings')} diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx index f72db5e2671..ad839244ceb 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx @@ -39,7 +39,7 @@ import { useTipLengthCalibrations, useRobot, } from '../../../hooks' -import { useNotifyAllRunsQuery } from '../../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../../resources/runs' import type { State, Dispatch } from '../../../../../redux/types' import type { ResetConfigRequest } from '../../../../../redux/robot-admin/types' @@ -169,7 +169,7 @@ export function DeviceResetSlideout({ flexDirection={DIRECTION_ROW} alignItems={ALIGN_CENTER} backgroundColor={COLORS.yellow30} - borderRadius={BORDERS.borderRadiusSize1} + borderRadius={BORDERS.borderRadius4} padding={SPACING.spacing8} marginBottom={SPACING.spacing24} > diff --git a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx index 273839e048f..e90fae77a86 100644 --- a/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx +++ b/app/src/organisms/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx @@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN, - TYPOGRAPHY, SPACING, COLORS, PrimaryButton, @@ -185,13 +184,6 @@ export function RenameRobotSlideout({ {t('rename_robot_input_limitation_detail')} - - {t('robot_name')} - )} /> diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx index bef8dce44b7..3e6f4641b14 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx @@ -41,7 +41,7 @@ import type { RobotInitializationStatus } from '../../../../resources/health/hoo const UPDATE_PROGRESS_BAR_STYLE = css` margin-top: ${SPACING.spacing24}; margin-bottom: ${SPACING.spacing24}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius8}; background: ${COLORS.grey30}; width: 17.12rem; ` diff --git a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx index a659259e698..f02ad6ae3ce 100644 --- a/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx +++ b/app/src/organisms/Devices/RobotSettings/UpdateBuildroot/UpdateRobotModal.tsx @@ -46,7 +46,7 @@ export const FOOTER_BUTTON_STYLE = css` text-transform: lowercase; padding-left: ${SPACING.spacing16}; padding-right: ${SPACING.spacing16}; - border-radius: ${BORDERS.borderRadiusSize1}; + border-radius: ${BORDERS.borderRadius8}; margin-top: ${SPACING.spacing16}; margin-bottom: ${SPACING.spacing16}; diff --git a/app/src/organisms/Devices/RobotStatusHeader.tsx b/app/src/organisms/Devices/RobotStatusHeader.tsx index 224d2963809..e9dd34568ea 100644 --- a/app/src/organisms/Devices/RobotStatusHeader.tsx +++ b/app/src/organisms/Devices/RobotStatusHeader.tsx @@ -34,7 +34,7 @@ import { OPENTRONS_USB, } from '../../redux/discovery' import { getNetworkInterfaces, fetchStatus } from '../../redux/networking' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { IconName, StyleProps } from '@opentrons/components' import type { DiscoveredRobot } from '../../redux/discovery/types' diff --git a/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx b/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx index 5fcbbccadbe..aa4693135ed 100644 --- a/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx +++ b/app/src/organisms/Devices/__tests__/RecentProtocolRuns.test.tsx @@ -4,7 +4,7 @@ import { screen } from '@testing-library/react' import { describe, it, vi, beforeEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import { i18n } from '../../../i18n' import { useIsRobotViewable, useRunStatuses } from '../hooks' import { RecentProtocolRuns } from '../RecentProtocolRuns' @@ -13,7 +13,7 @@ import { HistoricalProtocolRun } from '../HistoricalProtocolRun' import type { Runs } from '@opentrons/api-client' import type { AxiosError } from 'axios' -vi.mock('../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../resources/runs') vi.mock('../hooks') vi.mock('../../ProtocolUpload/hooks') vi.mock('../HistoricalProtocolRun') diff --git a/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx b/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx index b1f21ae5839..38d73b9a944 100644 --- a/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx +++ b/app/src/organisms/Devices/__tests__/RobotStatusHeader.test.tsx @@ -19,7 +19,7 @@ import { import { getNetworkInterfaces } from '../../../redux/networking' import { useIsFlex } from '../hooks' import { RobotStatusHeader } from '../RobotStatusHeader' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { DiscoveryClientRobotAddress } from '../../../redux/discovery/types' import type { SimpleInterfaceStatus } from '../../../redux/networking/types' @@ -31,7 +31,7 @@ vi.mock('../../../organisms/RunTimeControl/hooks') vi.mock('../../../redux/discovery') vi.mock('../../../redux/networking') vi.mock('../hooks') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') const MOCK_OTIE = { name: 'otie', diff --git a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts index 77f06e074c9..457c0a75287 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts +++ b/app/src/organisms/Devices/hooks/__tests__/useIsRobotBusy.test.ts @@ -14,8 +14,8 @@ import { } from '../../../EmergencyStop' import { useIsRobotBusy } from '../useIsRobotBusy' import { useIsFlex } from '../useIsFlex' -import { useNotifyCurrentMaintenanceRun } from '../../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyCurrentMaintenanceRun } from '../../../../resources/maintenance_runs' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import type { Sessions, Runs } from '@opentrons/api-client' import type { AxiosError } from 'axios' @@ -23,8 +23,8 @@ import type { AxiosError } from 'axios' vi.mock('@opentrons/react-api-client') vi.mock('../../../ProtocolUpload/hooks') vi.mock('../useIsFlex') -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') -vi.mock('../../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../../resources/runs') +vi.mock('../../../../resources/maintenance_runs') const mockEstopStatus = { data: { diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx index 8fc7cff7d64..a327e420b05 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolAnalysisErrors.test.tsx @@ -9,9 +9,9 @@ import { } from '@opentrons/react-api-client' import { useProtocolAnalysisErrors } from '..' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' -import { RUN_ID_2 } from '../../../../organisms/RunTimeControl/__fixtures__' +import { RUN_ID_2 } from '../../../RunTimeControl/__fixtures__' import type { Run, Protocol } from '@opentrons/api-client' import type { @@ -20,7 +20,7 @@ import type { } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') describe('useProtocolAnalysisErrors hook', () => { beforeEach(() => { diff --git a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx index cf57b815dd7..7c0ad0363a9 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useProtocolDetailsForRun.test.tsx @@ -9,9 +9,9 @@ import { } from '@opentrons/react-api-client' import { useProtocolDetailsForRun } from '..' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' -import { RUN_ID_2 } from '../../../../organisms/RunTimeControl/__fixtures__' +import { RUN_ID_2 } from '../../../RunTimeControl/__fixtures__' import type { Protocol, Run } from '@opentrons/api-client' import { @@ -20,7 +20,7 @@ import { } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const PROTOCOL_ID = 'fake_protocol_id' const PROTOCOL_ANALYSIS = { diff --git a/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx index 897dbd13394..a067332dd82 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useRunCalibrationStatus.test.tsx @@ -11,7 +11,7 @@ import { useIsFlex, useRunPipetteInfoByMount, } from '..' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { PipetteInfo } from '..' import { Provider } from 'react-redux' @@ -20,7 +20,7 @@ import { createStore } from 'redux' vi.mock('../useDeckCalibrationStatus') vi.mock('../useIsFlex') vi.mock('../useRunPipetteInfoByMount') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') let wrapper: React.FunctionComponent<{ children: React.ReactNode }> diff --git a/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx index e4399c493db..07546e8b382 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useRunCreatedAtTimestamp.test.tsx @@ -2,15 +2,15 @@ import { renderHook } from '@testing-library/react' import { vi, it, expect, describe, beforeEach } from 'vitest' import { when } from 'vitest-when' -import { mockIdleUnstartedRun } from '../../../../organisms/RunTimeControl/__fixtures__' +import { mockIdleUnstartedRun } from '../../../RunTimeControl/__fixtures__' import { formatTimestamp } from '../../utils' import { useRunCreatedAtTimestamp } from '../useRunCreatedAtTimestamp' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { UseQueryResult } from 'react-query' import type { Run } from '@opentrons/api-client' -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') vi.mock('../../utils') const MOCK_RUN_ID = '1' diff --git a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx index 62275d66318..34365a075e7 100644 --- a/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx +++ b/app/src/organisms/Devices/hooks/__tests__/useStoredProtocolAnalysis.test.tsx @@ -25,14 +25,14 @@ import { PIPETTE_ENTITY, STORED_PROTOCOL_ANALYSIS, } from '../__fixtures__/storedProtocolAnalysis' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { Protocol, Run } from '@opentrons/api-client' vi.mock('@opentrons/api-client') vi.mock('@opentrons/react-api-client') vi.mock('../../../../redux/protocol-storage/selectors') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const store: Store = createStore(vi.fn(), {}) diff --git a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts index 772039b22d2..671cfb39fcb 100644 --- a/app/src/organisms/Devices/hooks/useIsRobotBusy.ts +++ b/app/src/organisms/Devices/hooks/useIsRobotBusy.ts @@ -5,8 +5,8 @@ import { useCurrentAllSubsystemUpdatesQuery, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import { DISENGAGED } from '../../EmergencyStop' import { useIsFlex } from './useIsFlex' diff --git a/app/src/organisms/Devices/hooks/useLastRunCommandKey.ts b/app/src/organisms/Devices/hooks/useLastRunCommandKey.ts index 15b35c5f1ed..b51160abf2d 100644 --- a/app/src/organisms/Devices/hooks/useLastRunCommandKey.ts +++ b/app/src/organisms/Devices/hooks/useLastRunCommandKey.ts @@ -1,6 +1,7 @@ import { useAllCommandsQuery } from '@opentrons/react-api-client' import { useRunStatus } from '../../RunTimeControl/hooks' import { + RUN_STATUS_AWAITING_RECOVERY, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, RUN_STATUS_FINISHING, RUN_STATUS_IDLE, @@ -21,6 +22,7 @@ const LIVE_RUN_STATUSES = [ RUN_STATUS_RUNNING, RUN_STATUS_FINISHING, RUN_STATUS_BLOCKED_BY_OPEN_DOOR, + RUN_STATUS_AWAITING_RECOVERY, ] const LIVE_RUN_COMMANDS_POLL_MS = 3000 diff --git a/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts b/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts index 996c44989d4..1c86de6ecf5 100644 --- a/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts +++ b/app/src/organisms/Devices/hooks/useProtocolAnalysisErrors.ts @@ -4,7 +4,7 @@ import { useProtocolQuery, } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import { AnalysisError } from '@opentrons/shared-data' diff --git a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts index f610b623d5c..57c50666488 100644 --- a/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts +++ b/app/src/organisms/Devices/hooks/useProtocolDetailsForRun.ts @@ -6,7 +6,7 @@ import { useProtocolAnalysisAsDocumentQuery, } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { RobotType, diff --git a/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts b/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts index 03def4f2a4a..72936c75514 100644 --- a/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts +++ b/app/src/organisms/Devices/hooks/useRunCreatedAtTimestamp.ts @@ -1,6 +1,6 @@ import { formatTimestamp } from '../utils' import { EMPTY_TIMESTAMP } from '../constants' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' export function useRunCreatedAtTimestamp(runId: string | null): string { const runRecord = useNotifyRunQuery(runId) diff --git a/app/src/organisms/Devices/hooks/useRunStatuses.ts b/app/src/organisms/Devices/hooks/useRunStatuses.ts index d7c8a3cf422..bba83f76299 100644 --- a/app/src/organisms/Devices/hooks/useRunStatuses.ts +++ b/app/src/organisms/Devices/hooks/useRunStatuses.ts @@ -1,4 +1,5 @@ import { + RUN_STATUS_AWAITING_RECOVERY, RUN_STATUS_FAILED, RUN_STATUS_IDLE, RUN_STATUS_PAUSED, @@ -21,7 +22,12 @@ export function useRunStatuses(): RunStatusesInfo { const runStatus = useRunStatus(currentRunId) const isRunIdle = runStatus === RUN_STATUS_IDLE const isRunRunning = - runStatus === RUN_STATUS_PAUSED || runStatus === RUN_STATUS_RUNNING + // todo(mm, 2024-03-13): Does this intentionally exclude + // RUN_STATUS_FINISHING, RUN_STATUS_STOP_REQUESTED, + // and RUN_STATUS_BLOCKED_BY_OPEN_DOOR? + runStatus === RUN_STATUS_PAUSED || + runStatus === RUN_STATUS_RUNNING || + runStatus === RUN_STATUS_AWAITING_RECOVERY const isRunTerminal = runStatus === RUN_STATUS_SUCCEEDED || runStatus === RUN_STATUS_STOPPED || diff --git a/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts b/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts index 0a7571b1f6b..64b83e855c3 100644 --- a/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts +++ b/app/src/organisms/Devices/hooks/useStoredProtocolAnalysis.ts @@ -7,7 +7,7 @@ import { import { useProtocolQuery } from '@opentrons/react-api-client' import { getStoredProtocol } from '../../../redux/protocol-storage' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' import type { State } from '../../../redux/types' diff --git a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx index 8fe2d7970cd..7696c2ccb6f 100644 --- a/app/src/organisms/DropTipWizard/BeforeBeginning.tsx +++ b/app/src/organisms/DropTipWizard/BeforeBeginning.tsx @@ -187,7 +187,7 @@ export const BeforeBeginning = ( const UNSELECTED_OPTIONS_STYLE = css` background-color: ${COLORS.white}; border: 1px solid ${COLORS.grey30}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; height: 12.5625rem; width: 14.5625rem; cursor: pointer; @@ -205,7 +205,7 @@ const UNSELECTED_OPTIONS_STYLE = css` justify-content: ${JUSTIFY_FLEX_START}; background-color: ${COLORS.blue35}; border-width: 0; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; padding: ${SPACING.spacing24}; height: 5.25rem; width: 57.8125rem; diff --git a/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx b/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx index 0562efc9ae7..34540b1c516 100644 --- a/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx +++ b/app/src/organisms/DropTipWizard/__tests__/TipsAttachedModal.test.tsx @@ -10,12 +10,12 @@ import { handleTipsAttachedModal } from '../TipsAttachedModal' import { LEFT } from '@opentrons/shared-data' import { mockPipetteInfo } from '../../../redux/pipettes/__fixtures__' import { ROBOT_MODEL_OT3 } from '../../../redux/discovery' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import type { PipetteModelSpecs } from '@opentrons/shared-data' import type { HostConfig } from '@opentrons/api-client' -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') vi.mock('../../../resources/useNotifyService') const MOCK_ACTUAL_PIPETTE = { diff --git a/app/src/organisms/DropTipWizard/index.tsx b/app/src/organisms/DropTipWizard/index.tsx index 49396e76b4d..a8253dbbf8f 100644 --- a/app/src/organisms/DropTipWizard/index.tsx +++ b/app/src/organisms/DropTipWizard/index.tsx @@ -18,7 +18,7 @@ import { useDeckConfigurationQuery, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { LegacyModalShell } from '../../molecules/LegacyModal' import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' @@ -27,7 +27,7 @@ import { getIsOnDevice } from '../../redux/config' import { useChainMaintenanceCommands, useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs/hooks' +} from '../../resources/runs' import { StyledText } from '../../atoms/text' import { Jog } from '../../molecules/JogControls' import { ExitConfirmation } from './ExitConfirmation' @@ -522,7 +522,7 @@ export const DropTipWizardComponent = ( top="16px" border={BORDERS.lineBorder} boxShadow={BORDERS.shadowSmall} - borderRadius={BORDERS.borderRadiusSize4} + borderRadius={BORDERS.borderRadius16} position={POSITION_ABSOLUTE} backgroundColor={COLORS.white} > diff --git a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx index 47f9e82cb3f..33d581ea5e4 100644 --- a/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx +++ b/app/src/organisms/FirmwareUpdateModal/FirmwareUpdateTakeover.tsx @@ -6,7 +6,7 @@ import { useCurrentAllSubsystemUpdatesQuery, useSubsystemUpdateQuery, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { getTopPortalEl } from '../../App/portal' import { useIsUnboxingFlowOngoing } from '../RobotSettingsDashboard/NetworkSettings/hooks' import { UpdateInProgressModal } from './UpdateInProgressModal' diff --git a/app/src/organisms/FirmwareUpdateModal/UpdateInProgressModal.tsx b/app/src/organisms/FirmwareUpdateModal/UpdateInProgressModal.tsx index f674f39c379..8aa9eb8bef0 100644 --- a/app/src/organisms/FirmwareUpdateModal/UpdateInProgressModal.tsx +++ b/app/src/organisms/FirmwareUpdateModal/UpdateInProgressModal.tsx @@ -41,7 +41,7 @@ export function UpdateInProgressModal( height="17.25rem" width="100%" backgroundColor={COLORS.grey35} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} flexDirection={DIRECTION_COLUMN} padding={SPACING.spacing32} justifyContent={ALIGN_CENTER} diff --git a/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx b/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx index fa2d26dc9ec..0a16da311e3 100644 --- a/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx +++ b/app/src/organisms/FirmwareUpdateModal/UpdateResultsModal.tsx @@ -76,7 +76,7 @@ export function UpdateResultsModal( height="11.5rem" width="100%" backgroundColor={COLORS.green35} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} flexDirection={DIRECTION_COLUMN} color={COLORS.grey60} padding={SPACING.spacing24} diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx index 4ef3942e413..6c49288b30e 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateModal.test.tsx @@ -119,12 +119,13 @@ describe('FirmwareUpdateModal', () => { vi.advanceTimersByTime(3000) }) screen.getByText('Firmware is up to date.') + screen.getByLabelText('check') act(() => { vi.advanceTimersByTime(3000) }) await waitFor(() => expect(props.proceed).toHaveBeenCalled()) }) - it('does not render text or a progress bar until instrument update status is known', () => { + it('does not render text until instrument update status is known', () => { vi.mocked(useSubsystemUpdateQuery).mockReturnValue({ data: { data: { @@ -159,6 +160,7 @@ describe('FirmwareUpdateModal', () => { vi.advanceTimersByTime(3000) }) screen.getByText('A firmware update is required, instrument is updating') + screen.getByLabelText('spinner') expect(updateSubsystem).toHaveBeenCalled() }) it('calls refetch instruments and then proceed once update is complete', async () => { diff --git a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx index dd0aaa2e001..3816b85261f 100644 --- a/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx +++ b/app/src/organisms/FirmwareUpdateModal/__tests__/FirmwareUpdateTakeover.test.tsx @@ -14,7 +14,7 @@ import { UpdateNeededModal } from '../UpdateNeededModal' import { UpdateInProgressModal } from '../UpdateInProgressModal' import { useIsUnboxingFlowOngoing } from '../../RobotSettingsDashboard/NetworkSettings/hooks' import { FirmwareUpdateTakeover } from '../FirmwareUpdateTakeover' -import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../../resources/maintenance_runs' import type { BadPipette, PipetteData } from '@opentrons/api-client' @@ -22,7 +22,7 @@ vi.mock('@opentrons/react-api-client') vi.mock('../UpdateNeededModal') vi.mock('../UpdateInProgressModal') vi.mock('../../RobotSettingsDashboard/NetworkSettings/hooks') -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') const render = () => { return renderWithProviders(, { diff --git a/app/src/organisms/FirmwareUpdateModal/index.tsx b/app/src/organisms/FirmwareUpdateModal/index.tsx index 0dfe6b1e5f0..f669b871445 100644 --- a/app/src/organisms/FirmwareUpdateModal/index.tsx +++ b/app/src/organisms/FirmwareUpdateModal/index.tsx @@ -9,7 +9,6 @@ import { Icon, RESPONSIVENESS, JUSTIFY_CENTER, - BORDERS, COLORS, } from '@opentrons/components' import { @@ -17,7 +16,6 @@ import { useSubsystemUpdateQuery, useUpdateSubsystemMutation, } from '@opentrons/react-api-client' -import { ProgressBar } from '../../atoms/ProgressBar' import { StyledText } from '../../atoms/text' import { BadGripper, BadPipette, Subsystem } from '@opentrons/api-client' @@ -55,11 +53,6 @@ const MODAL_STYLE = css` height: 31.5625rem; } ` -const OUTER_STYLES = css` - border-radius: ${BORDERS.borderRadiusSize4}; - background: ${COLORS.grey30}; - width: 13.374rem; -` const SPINNER_STYLE = css` color: ${COLORS.grey50}; @@ -81,7 +74,7 @@ export const FirmwareUpdateModal = ( isOnDevice, } = props const [updateId, setUpdateId] = React.useState(null) - const [firmwareText, setFirmwareText] = React.useState('') + const [firmwareText, setFirmwareText] = React.useState(null) const { data: attachedInstruments, refetch: refetchInstruments, @@ -113,7 +106,6 @@ export const FirmwareUpdateModal = ( }, []) const { data: updateData } = useSubsystemUpdateQuery(updateId) const status = updateData?.data.updateStatus - const percentComplete = updateData?.data.updateProgress ?? 0 React.useEffect(() => { if ((status != null || updateNeeded) && firmwareText !== description) { @@ -140,24 +132,27 @@ export const FirmwareUpdateModal = ( return ( - - {firmwareText.length ? firmwareText : 'Checking for updates...'} - - {status != null || updateNeeded ? ( - - ) : null} - {firmwareText.length ? null : ( + {status != null || updateNeeded || !firmwareText ? ( + ) : ( + )} + + {firmwareText ?? 'Checking for updates...'} + ) } diff --git a/app/src/organisms/GripperWizardFlows/MovePin.tsx b/app/src/organisms/GripperWizardFlows/MovePin.tsx index 736a97af275..61c156b43de 100644 --- a/app/src/organisms/GripperWizardFlows/MovePin.tsx +++ b/app/src/organisms/GripperWizardFlows/MovePin.tsx @@ -19,7 +19,7 @@ import calibratingFrontJaw from '../../assets/videos/gripper-wizards/CALIBRATING import calibratingRearJaw from '../../assets/videos/gripper-wizards/CALIBRATING_REAR_JAW.webm' import type { Coordinates } from '@opentrons/shared-data' -import type { CreateMaintenanceCommand } from '../../resources/runs/hooks' +import type { CreateMaintenanceCommand } from '../../resources/runs' import type { GripperWizardStepProps, MovePinStep } from './types' interface MovePinProps extends GripperWizardStepProps, MovePinStep { diff --git a/app/src/organisms/GripperWizardFlows/index.tsx b/app/src/organisms/GripperWizardFlows/index.tsx index 3c7b6d80b5b..8ad69b4a8d1 100644 --- a/app/src/organisms/GripperWizardFlows/index.tsx +++ b/app/src/organisms/GripperWizardFlows/index.tsx @@ -15,7 +15,7 @@ import { useCreateMaintenanceCommandMutation, useDeleteMaintenanceRunMutation, } from '@opentrons/react-api-client' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { LegacyModalShell } from '../../molecules/LegacyModal' import { getTopPortalEl } from '../../App/portal' import { WizardHeader } from '../../molecules/WizardHeader' @@ -24,7 +24,7 @@ import { getIsOnDevice } from '../../redux/config' import { useChainMaintenanceCommands, useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs/hooks' +} from '../../resources/runs' import { getGripperWizardSteps } from './getGripperWizardSteps' import { GRIPPER_FLOW_TYPES, SECTIONS } from './constants' import { BeforeBeginning } from './BeforeBeginning' @@ -357,7 +357,7 @@ export const GripperWizard = ( top="16px" border={BORDERS.lineBorder} boxShadow={BORDERS.shadowSmall} - borderRadius={BORDERS.borderRadiusSize4} + borderRadius={BORDERS.borderRadius8} position={POSITION_ABSOLUTE} backgroundColor={COLORS.white} > diff --git a/app/src/organisms/InstrumentInfo/index.tsx b/app/src/organisms/InstrumentInfo/index.tsx index b850c9bd341..4b15ff0e15a 100644 --- a/app/src/organisms/InstrumentInfo/index.tsx +++ b/app/src/organisms/InstrumentInfo/index.tsx @@ -21,7 +21,7 @@ import { StyledText } from '../../atoms/text' import { MediumButton } from '../../atoms/buttons' import { FLOWS } from '../PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '../GripperWizardFlows/constants' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../resources/runs' import type { InstrumentData } from '@opentrons/api-client' import type { PipetteMount } from '@opentrons/shared-data' @@ -184,7 +184,7 @@ interface InfoItemProps extends StyleProps { function InfoItem(props: InfoItemProps): JSX.Element { return ( ` flex-direction: ${DIRECTION_COLUMN}; align-items: ${ALIGN_FLEX_START}; padding: ${SPACING.spacing24}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius12}; background-color: ${({ isAttached }) => isAttached ? COLORS.green35 : COLORS.grey35}; &:active { diff --git a/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx b/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx index 6c614404866..a350e13f6b9 100644 --- a/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx +++ b/app/src/organisms/InstrumentMountItem/ProtocolInstrumentMountItem.tsx @@ -37,7 +37,7 @@ export const MountItem = styled.div<{ isReady: boolean }>` flex-direction: ${DIRECTION_COLUMN}; align-items: ${ALIGN_FLEX_START}; padding: ${SPACING.spacing16} ${SPACING.spacing24}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius8}; background-color: ${({ isReady }) => isReady ? COLORS.green35 : COLORS.yellow35}; &:active { diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index 56479bb7e75..b49162d25df 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -55,10 +55,10 @@ const LABWARE_DESCRIPTION_STYLE = css` grid-gap: ${SPACING.spacing8}; padding: ${SPACING.spacing16}; background-color: ${COLORS.grey20}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { background-color: ${COLORS.grey35}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius8}; } ` diff --git a/app/src/organisms/InterventionModal/PauseInterventionContent.tsx b/app/src/organisms/InterventionModal/PauseInterventionContent.tsx index e5e35426903..b1d2c51f600 100644 --- a/app/src/organisms/InterventionModal/PauseInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/PauseInterventionContent.tsx @@ -48,13 +48,13 @@ export function PauseInterventionContent({ const PAUSE_HEADER_STYLE = css` align-items: ${ALIGN_CENTER}; background-color: ${COLORS.grey10}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; grid-gap: ${SPACING.spacing6}; padding: ${SPACING.spacing16}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { align-self: ${ALIGN_CENTER}; background-color: ${COLORS.grey35}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius8}; grid-gap: ${SPACING.spacing32}; padding: ${SPACING.spacing24}; min-width: 36.5rem; diff --git a/app/src/organisms/InterventionModal/index.tsx b/app/src/organisms/InterventionModal/index.tsx index c97c3a591f4..3b2c5bc26a4 100644 --- a/app/src/organisms/InterventionModal/index.tsx +++ b/app/src/organisms/InterventionModal/index.tsx @@ -59,7 +59,7 @@ const MODAL_STYLE = { maxHeight: '100%', width: '47rem', border: `6px ${BORDERS.styleSolid} ${COLORS.blue50}`, - borderRadius: BORDERS.radiusSoftCorners, + borderRadius: BORDERS.borderRadius8, boxShadow: BORDERS.smallDropShadow, } as const @@ -78,10 +78,8 @@ const CONTENT_STYLE = { flexDirection: DIRECTION_COLUMN, alignItems: ALIGN_FLEX_START, gridGap: SPACING.spacing24, - padding: `${SPACING.spacing32}`, - borderRadius: `0px 0px ${String(BORDERS.radiusSoftCorners)} ${String( - BORDERS.radiusSoftCorners - )}`, + padding: SPACING.spacing32, + borderRadius: BORDERS.borderRadius8, } as const const FOOTER_STYLE = { diff --git a/app/src/organisms/LabwareOffsetTabs/index.tsx b/app/src/organisms/LabwareOffsetTabs/index.tsx index beb72102901..f133ad52beb 100644 --- a/app/src/organisms/LabwareOffsetTabs/index.tsx +++ b/app/src/organisms/LabwareOffsetTabs/index.tsx @@ -68,9 +68,9 @@ export function LabwareOffsetTabs({ border={BORDERS.lineBorder} // remove left upper corner border radius when first tab is active borderRadius={`${ - currentTab === 'table' ? '0' : BORDERS.radiusSoftCorners - } ${BORDERS.radiusSoftCorners} ${BORDERS.radiusSoftCorners} ${ - BORDERS.radiusSoftCorners + currentTab === 'table' ? '0' : BORDERS.borderRadius4 + } ${BORDERS.borderRadius4} ${BORDERS.borderRadius4} ${ + BORDERS.borderRadius4 }`} paddingX={SPACING.spacing16} > diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index f6756b8060d..de632137f09 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -13,7 +13,7 @@ import { RobotMotionLoader } from './RobotMotionLoader' import attachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' import attachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' import attachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import type { Jog } from '../../molecules/JogControls/types' diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index 97fe3137690..dac9cbf3301 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -28,7 +28,7 @@ import { } from './utils/labware' import { UnorderedList } from '../../molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { getIsOnDevice } from '../../redux/config' import { getDisplayLocation } from './utils/getDisplayLocation' diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx index a1681d90e17..a1278cd5673 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx @@ -11,7 +11,7 @@ import { import detachProbe1 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' import detachProbe8 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm' import detachProbe96 from '../../assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { GenericWizardTile } from '../../molecules/GenericWizardTile' import type { Jog } from '../../molecules/JogControls/types' diff --git a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx index d8be65e2051..110b7430799 100644 --- a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx +++ b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx @@ -102,7 +102,7 @@ const ErrorTextArea = styled.textarea` width: 30rem; background-color: #f8f8f8; border: ${BORDERS.lineBorder}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; padding: ${SPACING.spacing8}; margin: ${SPACING.spacing16} 0; font-size: ${TYPOGRAPHY.fontSizeCaption}; diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx index e5e2a118d82..3a12db38c51 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -8,7 +8,7 @@ import { import { StyledText } from '../../../atoms/text' import { RobotMotionLoader } from '../RobotMotionLoader' import { getPrepCommands } from './getPrepCommands' -import { useChainRunCommands } from '../../../resources/runs/hooks' +import { useChainRunCommands } from '../../../resources/runs' import type { RegisterPositionAction } from '../types' import type { Jog } from '../../../molecules/JogControls' import { WizardRequiredEquipmentList } from '../../../molecules/WizardRequiredEquipmentList' diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx index 2edb77616ad..440c6c89586 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -37,10 +37,10 @@ import { DetachProbe } from './DetachProbe' import { PickUpTip } from './PickUpTip' import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' -import { useChainMaintenanceCommands } from '../../resources/runs/hooks' +import { useChainMaintenanceCommands } from '../../resources/runs' import { FatalErrorModal } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import type { Axis, Sign, StepSize } from '../../molecules/JogControls/types' diff --git a/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx index 3676e0837a7..d2f994af258 100644 --- a/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx +++ b/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx @@ -48,7 +48,7 @@ export function LiveOffsetValue(props: OffsetVectorProps): JSX.Element { diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx index 72141eb28ae..5f1f8692f8c 100644 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx @@ -18,7 +18,7 @@ import { MoveLabwareCreateCommand, RobotType, } from '@opentrons/shared-data' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { UnorderedList } from '../../molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '../Devices/ProtocolRun/utils/getCurrentOffsetForLabwareInLocation' import { TipConfirmation } from './TipConfirmation' diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx index 6855b185870..464970c9166 100644 --- a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx @@ -388,13 +388,13 @@ const TerseTable = styled('table')` margin: ${SPACING.spacing16} 0; text-align: left; tr td:first-child { - border-top-left-radius: ${BORDERS.borderRadiusSize3}; - border-bottom-left-radius: ${BORDERS.borderRadiusSize3}; + border-top-left-radius: ${BORDERS.borderRadius12}; + border-bottom-left-radius: ${BORDERS.borderRadius12}; padding-left: ${SPACING.spacing12}; } tr td:last-child { - border-top-right-radius: ${BORDERS.borderRadiusSize3}; - border-bottom-right-radius: ${BORDERS.borderRadiusSize3}; + border-top-right-radius: ${BORDERS.borderRadius12}; + border-bottom-right-radius: ${BORDERS.borderRadius12}; padding-right: ${SPACING.spacing12}; } ` diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx index a0c31074a5c..f4ecdf58154 100644 --- a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx @@ -12,7 +12,7 @@ import { } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' import { UnorderedList } from '../../molecules/UnorderedList' -import { useChainRunCommands } from '../../resources/runs/hooks' +import { useChainRunCommands } from '../../resources/runs' import { getLabwareDef, getLabwareDefinitionsFromCommands, diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx index d4632045666..560a1bb70b1 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx @@ -19,9 +19,11 @@ import { import { FLEX_ROBOT_TYPE, fixtureTiprack300ul } from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' -import { useCreateTargetedMaintenanceRunMutation } from '../../../resources/runs/hooks' +import { + useCreateTargetedMaintenanceRunMutation, + useNotifyRunQuery, +} from '../../../resources/runs' import { useMostRecentCompletedAnalysis } from '../useMostRecentCompletedAnalysis' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' import { useLaunchLPC } from '../useLaunchLPC' import { LabwarePositionCheck } from '..' @@ -31,9 +33,8 @@ import type { LabwareDefinition2 } from '@opentrons/shared-data' vi.mock('../') vi.mock('@opentrons/react-api-client') -vi.mock('../../../resources/runs/hooks') vi.mock('../useMostRecentCompletedAnalysis') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') const MOCK_RUN_ID = 'mockRunId' const MOCK_MAINTENANCE_RUN_ID = 'mockMaintenanceRunId' diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx index 3c24c90bfd9..d3a87ab91ee 100644 --- a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx +++ b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx @@ -5,11 +5,13 @@ import { useDeleteMaintenanceRunMutation, } from '@opentrons/react-api-client' -import { useCreateTargetedMaintenanceRunMutation } from '../../resources/runs/hooks' +import { + useCreateTargetedMaintenanceRunMutation, + useNotifyRunQuery, +} from '../../resources/runs' import { LabwarePositionCheck } from '.' import { useMostRecentCompletedAnalysis } from './useMostRecentCompletedAnalysis' import { getLabwareDefinitionsFromCommands } from './utils/labware' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' import type { RobotType } from '@opentrons/shared-data' diff --git a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts index 28d759466ab..0af8c075a58 100644 --- a/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts +++ b/app/src/organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis.ts @@ -4,7 +4,7 @@ import { useProtocolQuery, } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' diff --git a/app/src/organisms/ModuleCard/TestShakeSlideout.tsx b/app/src/organisms/ModuleCard/TestShakeSlideout.tsx index 08d81682e32..55a2d3de4e5 100644 --- a/app/src/organisms/ModuleCard/TestShakeSlideout.tsx +++ b/app/src/organisms/ModuleCard/TestShakeSlideout.tsx @@ -168,7 +168,7 @@ export const TestShakeSlideout = ( ) : null} { return ( diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 8e3ff101c18..944e9bd27e3 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -23,7 +23,7 @@ import { useAttachedPipettesFromInstrumentsQuery } from '../../organisms/Devices import { useChainMaintenanceCommands, useCreateTargetedMaintenanceRunMutation, -} from '../../resources/runs/hooks' +} from '../../resources/runs' import { getIsOnDevice } from '../../redux/config' import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' import { getModuleCalibrationSteps } from './getModuleCalibrationSteps' diff --git a/app/src/organisms/NetworkSettings/ConnectingNetwork.tsx b/app/src/organisms/NetworkSettings/ConnectingNetwork.tsx index b376826b2aa..5a3e77ad2c6 100644 --- a/app/src/organisms/NetworkSettings/ConnectingNetwork.tsx +++ b/app/src/organisms/NetworkSettings/ConnectingNetwork.tsx @@ -28,7 +28,7 @@ export function ConnectingNetwork({ backgroundColor={COLORS.grey35} flex="1" justifyContent={JUSTIFY_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} > diff --git a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx b/app/src/organisms/NetworkSettings/DisplayWifiList.tsx index 1ccb4729935..8105f77c940 100644 --- a/app/src/organisms/NetworkSettings/DisplayWifiList.tsx +++ b/app/src/organisms/NetworkSettings/DisplayWifiList.tsx @@ -33,7 +33,7 @@ const NETWORK_ROW_STYLE = css` background-color: ${COLORS.grey35}; margin-bottom: ${SPACING.spacing8}; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; &:hover { border: none; @@ -107,7 +107,7 @@ export function DisplayWifiList({ onClick={handleJoinAnotherNetwork} height="5rem" backgroundColor={COLORS.grey35} - borderRadius={BORDERS.borderRadiusSize4} + borderRadius={BORDERS.borderRadius16} color={COLORS.black90} css={NETWORK_ROW_STYLE} padding={`${SPACING.spacing20} ${SPACING.spacing32}`} diff --git a/app/src/organisms/NetworkSettings/FailedToConnect.tsx b/app/src/organisms/NetworkSettings/FailedToConnect.tsx index a220b4eaecc..b6c0a628bc8 100644 --- a/app/src/organisms/NetworkSettings/FailedToConnect.tsx +++ b/app/src/organisms/NetworkSettings/FailedToConnect.tsx @@ -41,7 +41,7 @@ export function FailedToConnect({ flex="1" backgroundColor={COLORS.red35} justifyContent={JUSTIFY_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} > void @@ -74,10 +52,12 @@ export function SetWifiCred({ setPassword(e.target.value)} type={showPassword ? 'text' : 'password'} - css={SSID_INPUT_FIELD_STYLE} + onBlur={e => e.target.focus()} + autoFocus /> setInputSsid(e.target.value)} type="text" - css={SSID_INPUT_FIELD_STYLE} error={errorMessage} + onBlur={e => e.target.focus()} + autoFocus /> e != null && setInputSsid(String(e))} + onChange={e => { + e != null && setInputSsid(e) + }} keyboardRef={keyboardRef} /> diff --git a/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx b/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx index 5487f7635a3..b0a4bb2d2a4 100644 --- a/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx +++ b/app/src/organisms/NetworkSettings/WifiConnectionDetails.tsx @@ -117,7 +117,7 @@ const DisplayConnectionStatus = ({ flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing32} backgroundColor={COLORS.green35} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_CENTER} > diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolDetails/ProtocolDetailsSkeleton.tsx b/app/src/organisms/OnDeviceDisplay/ProtocolDetails/ProtocolDetailsSkeleton.tsx index d1693df1557..aa241725f18 100644 --- a/app/src/organisms/OnDeviceDisplay/ProtocolDetails/ProtocolDetailsSkeleton.tsx +++ b/app/src/organisms/OnDeviceDisplay/ProtocolDetails/ProtocolDetailsSkeleton.tsx @@ -10,7 +10,7 @@ export function ProtocolDetailsHeaderChipSkeleton(): JSX.Element { width="12.17875rem" height="2.75rem" backgroundSize="99rem" - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} /> ) } @@ -21,7 +21,7 @@ export function ProcotolDetailsHeaderTitleSkeleton(): JSX.Element { width="42rem" height="3rem" backgroundSize="99rem" - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} /> ) } @@ -34,28 +34,28 @@ export function ProtocolDetailsSectionContentSkeleton(): JSX.Element { width="12rem" height="1.75rem" backgroundSize="99rem" - borderRadius={BORDERS.borderRadiusSize5} + borderRadius={BORDERS.borderRadius40} /> @@ -63,7 +63,7 @@ export function ProtocolDetailsSectionContentSkeleton(): JSX.Element { width="18rem" height="2.25rem" backgroundSize="99rem" - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} /> diff --git a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx index cb0b39160dc..23a51d26439 100644 --- a/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx +++ b/app/src/organisms/OnDeviceDisplay/ProtocolSetup/ProtocolSetupSkeleton.tsx @@ -11,13 +11,13 @@ export function ProtocolSetupTitleSkeleton(): JSX.Element { height="2.25rem" width="11.937rem" backgroundSize="99rem" - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} /> ) @@ -29,7 +29,7 @@ const SetupSkeleton = (): JSX.Element => { height="5.5rem" width="100%" backgroundSize="99rem" - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} /> ) } diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun.tsx index 8f07093e973..94531c26476 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun.tsx @@ -25,7 +25,7 @@ export function EmptyRecentRun(): JSX.Element { flexDirection={DIRECTION_COLUMN} height="27.25rem" justifyContent={JUSTIFY_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} > ) : ( diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing.tsx index 4f66f4f756e..0d3f8cabf61 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing.tsx @@ -24,7 +24,7 @@ export function ServerInitializing(): JSX.Element { flexDirection={DIRECTION_COLUMN} height="27.25rem" justifyContent={JUSTIFY_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} gridGap={SPACING.spacing32} > diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx index cc869e1afb5..1cac85c3727 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCard.test.tsx @@ -18,7 +18,7 @@ import { useTrackEvent } from '../../../../redux/analytics' import { useCloneRun } from '../../../ProtocolUpload/hooks' import { useHardwareStatusText } from '../hooks' import { RecentRunProtocolCard } from '../' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import { useRobotInitializationStatus, INIT_STATUS, @@ -34,7 +34,7 @@ vi.mock('../../../../organisms/RunTimeControl/hooks') vi.mock('../../../../organisms/ProtocolUpload/hooks') vi.mock('../../../../redux/analytics') vi.mock('../hooks') -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') vi.mock('../../../../resources/health/hooks') const RUN_ID = 'mockRunId' diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx index 1015ee8cfac..85e956ed977 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/__tests__/RecentRunProtocolCarousel.test.tsx @@ -3,14 +3,14 @@ import { screen } from '@testing-library/react' import { beforeEach, describe, it, vi } from 'vitest' import { renderWithProviders } from '../../../../__testing-utils__' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import { RecentRunProtocolCard, RecentRunProtocolCarousel } from '..' import type { RunData } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') vi.mock('../RecentRunProtocolCard') -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') const mockRun = { actions: [], diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal.tsx index ee164472ef0..d424bb96b1a 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal.tsx @@ -25,7 +25,7 @@ export function CancelingRunModal(): JSX.Element { justifyContent={JUSTIFY_CENTER} alignItems={ALIGN_CENTER} backgroundColor={COLORS.grey35} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} width="41.625rem" height="17.25rem" gridGap={SPACING.spacing24} diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx index 21b082a5f99..14a25e0d3f5 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/CurrentRunningProtocolCommand.tsx @@ -227,7 +227,7 @@ export function CurrentRunningProtocolCommand({ diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx index 15a32b6256f..d90ca63de6b 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunFailedModal.tsx @@ -90,7 +90,7 @@ export function RunFailedModal({ gridGap={SPACING.spacing8} maxHeight="11rem" backgroundColor={COLORS.grey35} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} padding={`${SPACING.spacing16} ${SPACING.spacing20}`} > diff --git a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx index 802de64035a..a19fabcb8a8 100644 --- a/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx +++ b/app/src/organisms/OnDeviceDisplay/RunningProtocol/RunningProtocolCommandList.tsx @@ -232,7 +232,7 @@ export function RunningProtocolCommandList({ fontSize="1.375rem" lineHeight="1.75rem" fontWeight={TYPOGRAPHY.fontWeightRegular} - borderRadius={BORDERS.borderRadiusSize2} + borderRadius={BORDERS.borderRadius8} gridGap="0.875rem" > diff --git a/app/src/organisms/OpenDoorAlertModal/index.tsx b/app/src/organisms/OpenDoorAlertModal/index.tsx index abdb21ba00f..4a4d141911b 100644 --- a/app/src/organisms/OpenDoorAlertModal/index.tsx +++ b/app/src/organisms/OpenDoorAlertModal/index.tsx @@ -22,7 +22,7 @@ export function OpenDoorAlertModal(): JSX.Element { + + + + {t('no_parameters')} + + + ) +} diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/NoParameter.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/NoParameter.test.tsx new file mode 100644 index 00000000000..40cfb8f48de --- /dev/null +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/NoParameter.test.tsx @@ -0,0 +1,37 @@ +import * as React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect } from 'vitest' + +import { BORDERS, COLORS } from '@opentrons/components' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' + +import { NoParameter } from '../NoParameter' + +const render = () => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('NoParameter', () => { + it('should render text and icon with proper color', () => { + render() + screen.getByLabelText('alert') + screen.getByText('No parameters specified in this protocol') + }) + + it('should have proper styles', () => { + render() + expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( + `background-color: ${COLORS.grey30}` + ) + expect(screen.getByTestId('NoRunTimeParameter')).toHaveStyle( + `border-radius: ${BORDERS.borderRadius8}` + ) + expect(screen.getByLabelText('alert')).toHaveStyle( + `color: ${COLORS.grey60}` + ) + }) +}) diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx new file mode 100644 index 00000000000..8c7724e3add --- /dev/null +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/__tests__/ProtocolParameters.test.tsx @@ -0,0 +1,132 @@ +import * as React from 'react' +import { describe, it, vi, beforeEach, afterEach } from 'vitest' +import { screen } from '@testing-library/react' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { NoParameter } from '../NoParameter' +import { ProtocolParameters } from '..' + +import type { RunTimeParameter } from '@opentrons/shared-data' + +vi.mock('../NoParameter') + +const mockRunTimeParameter: RunTimeParameter[] = [ + { + displayName: 'Trash Tips', + variableName: 'TIP_TRASH', + description: + 'to throw tip into the trash or to not throw tip into the trash', + type: 'boolean', + default: true, + }, + { + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + }, + { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + description: 'default module offsets for temp, H-S, and none', + type: 'str', + choices: [ + { + displayName: 'No offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, + { + displayName: 'pipette mount', + variableName: 'mont', + description: 'pipette mount', + type: 'str', + choices: [ + { + displayName: 'Left', + value: 'left', + }, + { + displayName: 'Right', + value: 'right', + }, + ], + default: 'left', + }, +] + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} + +describe('ProtocolParameters', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + runTimeParameters: mockRunTimeParameter, + } + vi.mocked(NoParameter).mockReturnValue(
mock NoParameter
) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should render banner when RunTimeParameters are existing', () => { + render(props) + screen.getByText('Listed values are view-only') + screen.getByText('Start setup to customize values') + }) + + it('should render table header', () => { + render(props) + screen.getByText('Name') + screen.getByText('Default Value') + screen.getByText('Range') + }) + + it('should render parameters default information', () => { + render(props) + screen.getByText('Trash Tips') + screen.getByText('On') + screen.getByText('On, off') + + screen.getByText('EtoH Volume') + screen.getByText('6.5 mL') + screen.getByText('1.5-10') + + screen.getByText('Default Module Offsets') + screen.getByText('No offsets') + screen.getByText('3 choices') + + screen.getByText('pipette mount') + screen.getByText('Left') + screen.getByText('Left, Right') + }) + + it('should render empty display when protocol does not have any parameter', () => { + props = { + runTimeParameters: [], + } + render(props) + screen.getByText('mock NoParameter') + }) +}) diff --git a/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx new file mode 100644 index 00000000000..76b1f006587 --- /dev/null +++ b/app/src/organisms/ProtocolDetails/ProtocolParameters/index.tsx @@ -0,0 +1,182 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' + +import { + BORDERS, + DIRECTION_COLUMN, + Flex, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' +import { StyledText } from '../../../atoms/text' +import { Banner } from '../../../atoms/Banner' +import { NoParameter } from './NoParameter' + +import type { RunTimeParameter } from '@opentrons/shared-data' + +interface ProtocolParametersProps { + runTimeParameters: RunTimeParameter[] +} + +export function ProtocolParameters({ + runTimeParameters, +}: ProtocolParametersProps): JSX.Element { + const { t } = useTranslation('protocol_details') + + return ( + + {runTimeParameters.length > 0 ? ( + + + + + {t('listed_values_are_view_only')} + + + {t('start_setup_customize_values')} + + + + + + ) : ( + + )} + + ) +} + +interface ProtocolParameterItemsProps { + runTimeParameters: RunTimeParameter[] +} + +function ProtocolParameterItems({ + runTimeParameters, +}: ProtocolParameterItemsProps): JSX.Element { + const { t } = useTranslation('protocol_details') + + const formattedValue = (runTimeParameter: RunTimeParameter): string => { + const { type, default: defaultValue } = runTimeParameter + const suffix = + 'suffix' in runTimeParameter && runTimeParameter.suffix != null + ? runTimeParameter.suffix + : '' + switch (type) { + case 'int': + case 'float': + return `${defaultValue.toString()} ${suffix}` + case 'boolean': + return Boolean(defaultValue) ? t('on') : t('off') + case 'str': + if ('choices' in runTimeParameter && runTimeParameter.choices != null) { + const choice = runTimeParameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return choice.displayName + } + } + break + } + return '' + } + + const formatRange = ( + runTimeParameter: RunTimeParameter, + minMax: string + ): string => { + const { type } = runTimeParameter + const choices = + 'choices' in runTimeParameter ? runTimeParameter.choices : [] + const count = choices.length + + switch (type) { + case 'int': + case 'float': + return minMax + case 'boolean': + return t('on_off') + case 'str': + if (count > 2) { + return t('choices', { count }) + } else { + return choices.map(choice => choice.displayName).join(', ') + } + } + return '' + } + + return ( + + + {t('name')} + {t('default_value')} + {t('range')} + + + {runTimeParameters.map((parameter: RunTimeParameter, index: number) => { + const min = 'min' in parameter ? parameter.min : 0 + const max = 'max' in parameter ? parameter.max : 0 + return ( + + + {parameter.displayName} + + + {formattedValue(parameter)} + + + + {formatRange(parameter, `${min}-${max}`)} + + + + ) + })} + + + ) +} + +const StyledTable = styled.table` + width: 100%; + border-collapse: collapse; + text-align: left; +` + +const StyledTableHeader = styled.th` + ${TYPOGRAPHY.labelSemiBold} + padding: ${SPACING.spacing8}; + border-bottom: ${BORDERS.lineBorder}; +` + +interface StyledTableRowProps { + isLast: boolean +} + +const StyledTableRow = styled.tr` + padding: ${SPACING.spacing8}; + border-bottom: ${props => (props.isLast ? 'none' : BORDERS.lineBorder)}; +` + +interface StyledTableCellProps { + isLast: boolean +} + +const StyledTableCell = styled.td` + padding-left: ${SPACING.spacing8}; + padding-top: ${SPACING.spacing12}; + padding-bottom: ${props => (props.isLast ? 0 : SPACING.spacing12)}; +` diff --git a/app/src/organisms/ProtocolDetails/ProtocolStats.tsx b/app/src/organisms/ProtocolDetails/ProtocolStats.tsx index 16b778ff5ed..357182a4e11 100644 --- a/app/src/organisms/ProtocolDetails/ProtocolStats.tsx +++ b/app/src/organisms/ProtocolDetails/ProtocolStats.tsx @@ -13,11 +13,9 @@ import { SPACING, TYPOGRAPHY, } from '@opentrons/components' -import { - getPipetteNameSpecs, - ProtocolAnalysisOutput, -} from '@opentrons/shared-data' +import { getPipetteNameSpecs } from '@opentrons/shared-data' import { StyledText } from '../../atoms/text' +import type { ProtocolAnalysisOutput } from '@opentrons/shared-data' interface ProtocolStatsProps { analysis: ProtocolAnalysisOutput | null @@ -196,7 +194,7 @@ export const StatRow = (props: StatRowProps): JSX.Element => {
+ ('robot_config') const [ showChooseRobotToRunProtocolSlideout, @@ -214,6 +217,9 @@ export function ProtocolDetails( const isAnalyzing = useSelector((state: State) => getIsProtocolAnalysisInProgress(state, protocolKey) ) + + const runTimeParameters = useRunTimeParameters(protocolKey) + const analysisStatus = getAnalysisStatus(isAnalyzing, mostRecentAnalysis) if (analysisStatus === 'stale') { @@ -327,6 +333,9 @@ export function ProtocolDetails( stats: enableProtocolStats ? ( ) : null, + parameters: enableRunTimeParameters ? ( + + ) : null, } const deckMap = @@ -394,7 +403,7 @@ export function ProtocolDetails( @@ -587,10 +596,25 @@ export function ProtocolDetails( gridGap={SPACING.spacing8} > + {enableRunTimeParameters && mostRecentAnalysis != null && ( + { + setCurrentTab('parameters') + }} + > + + {i18n.format(t('parameters'), 'capitalize')} + + + )} setCurrentTab('robot_config')} + onClick={() => { + setCurrentTab('robot_config') + }} > {i18n.format(t('hardware'), 'capitalize')} @@ -599,7 +623,9 @@ export function ProtocolDetails( setCurrentTab('labware')} + onClick={() => { + setCurrentTab('labware') + }} > {i18n.format(t('labware'), 'capitalize')} @@ -609,7 +635,9 @@ export function ProtocolDetails( setCurrentTab('liquids')} + onClick={() => { + setCurrentTab('liquids') + }} > {i18n.format(t('liquids'), 'capitalize')} @@ -620,7 +648,9 @@ export function ProtocolDetails( setCurrentTab('stats')} + onClick={() => { + setCurrentTab('stats') + }} > {i18n.format(t('stats'), 'capitalize')} @@ -632,13 +662,11 @@ export function ProtocolDetails( backgroundColor={COLORS.white} // remove left upper corner border radius when first tab is active borderRadius={`${ - currentTab === 'robot_config' - ? '0' - : BORDERS.radiusSoftCorners - } ${BORDERS.radiusSoftCorners} ${BORDERS.radiusSoftCorners} ${ - BORDERS.radiusSoftCorners + currentTab === 'robot_config' ? '0' : BORDERS.borderRadius4 + } ${BORDERS.borderRadius4} ${BORDERS.borderRadius4} ${ + BORDERS.borderRadius4 }`} - padding={`${SPACING.spacing16} ${SPACING.spacing16} 0 ${SPACING.spacing16}`} + padding={SPACING.spacing16} > {contentsByTabName[currentTab]}
diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index 6e0ef6d1053..d4e50a9b004 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -414,7 +414,7 @@ function LabwareLatch({ diff --git a/app/src/organisms/ProtocolSetupLiquids/LiquidDetails.tsx b/app/src/organisms/ProtocolSetupLiquids/LiquidDetails.tsx index 15185a46308..19f1b13f153 100644 --- a/app/src/organisms/ProtocolSetupLiquids/LiquidDetails.tsx +++ b/app/src/organisms/ProtocolSetupLiquids/LiquidDetails.tsx @@ -23,7 +23,7 @@ import type { LabwareByLiquidId, ParsedLiquid } from '@opentrons/api-client' const Table = styled('table')` table-layout: ${SPACING.spacingAuto}; width: 100%; - border-spacing: 0 ${BORDERS.borderRadiusSize2}; + border-spacing: 0 ${BORDERS.borderRadius8}; text-align: ${TYPOGRAPHY.textAlignLeft}; color: ${COLORS.grey60}; ` @@ -46,13 +46,13 @@ const TableDatum = styled('td')` white-space: break-spaces; text-overflow: ${WRAP}; &:first-child { - border-top-left-radius: ${BORDERS.borderRadiusSize3}; - border-bottom-left-radius: ${BORDERS.borderRadiusSize3}; + border-top-left-radius: ${BORDERS.borderRadius12}; + border-bottom-left-radius: ${BORDERS.borderRadius12}; width: 20%; } &:last-child { - border-top-right-radius: ${BORDERS.borderRadiusSize3}; - border-bottom-right-radius: ${BORDERS.borderRadiusSize3}; + border-top-right-radius: ${BORDERS.borderRadius12}; + border-bottom-right-radius: ${BORDERS.borderRadius12}; } ` diff --git a/app/src/organisms/ProtocolSetupLiquids/index.tsx b/app/src/organisms/ProtocolSetupLiquids/index.tsx index 69b722795a8..a8d8d6ba47e 100644 --- a/app/src/organisms/ProtocolSetupLiquids/index.tsx +++ b/app/src/organisms/ProtocolSetupLiquids/index.tsx @@ -78,7 +78,7 @@ export function LiquidsList(props: LiquidsListProps): JSX.Element { return ( {t('setup_instructions_description')}
diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index cd3250045d8..ead32d65d38 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -14,7 +14,7 @@ import { import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' -import { useChainLiveCommands } from '../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../resources/runs' import { mockRobotSideAnalysis } from '../../CommandText/__fixtures__' import { useAttachedModules, @@ -40,7 +40,7 @@ import { ProtocolSetupModulesAndDeck } from '..' import type { CutoutConfig, DeckConfiguration } from '@opentrons/shared-data' vi.mock('@opentrons/react-api-client') -vi.mock('../../../resources/runs/hooks') +vi.mock('../../../resources/runs') vi.mock('../../../redux/discovery') vi.mock('../../../organisms/Devices/hooks') vi.mock( diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx index 349e3633bf1..4f4fb33ab00 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCloneRun.test.tsx @@ -7,12 +7,12 @@ import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' import { useHost, useCreateRunMutation } from '@opentrons/react-api-client' import { useCloneRun } from '../useCloneRun' -import { useNotifyRunQuery } from '../../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../../resources/runs' import type { HostConfig } from '@opentrons/api-client' vi.mock('@opentrons/react-api-client') -vi.mock('../../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../../resources/runs') const HOST_CONFIG: HostConfig = { hostname: 'localhost' } const RUN_ID: string = 'run_id' diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx index 24f49066cd5..af4c9edf012 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useCurrentRunId.test.tsx @@ -3,9 +3,9 @@ import { renderHook } from '@testing-library/react' import { describe, it, afterEach, expect, vi } from 'vitest' import { useCurrentRunId } from '../useCurrentRunId' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') describe('useCurrentRunId hook', () => { afterEach(() => { diff --git a/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx b/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx index f5bfe186884..e385b2d8f77 100644 --- a/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx +++ b/app/src/organisms/ProtocolUpload/hooks/__tests__/useMostRecentRunId.test.tsx @@ -2,10 +2,10 @@ import { when } from 'vitest-when' import { renderHook } from '@testing-library/react' import { describe, it, afterEach, vi, expect } from 'vitest' -import { useNotifyAllRunsQuery } from '../../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../../resources/runs' import { useMostRecentRunId } from '../useMostRecentRunId' -vi.mock('../../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../../resources/runs') describe('useMostRecentRunId hook', () => { afterEach(() => { diff --git a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts index 8512520d00f..c7ba887ab54 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCloneRun.ts @@ -2,7 +2,7 @@ import { useQueryClient } from 'react-query' import { useHost, useCreateRunMutation } from '@opentrons/react-api-client' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { Run } from '@opentrons/api-client' diff --git a/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts b/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts index a1f1b288ddb..6510f7e672e 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCurrentRun.ts @@ -1,5 +1,5 @@ import { useCurrentRunId } from './useCurrentRunId' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import type { Run } from '@opentrons/api-client' diff --git a/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts b/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts index ad9f970b668..135ba73c504 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useCurrentRunId.ts @@ -1,4 +1,4 @@ -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import type { AxiosError } from 'axios' import type { UseAllRunsQueryOptions } from '@opentrons/react-api-client/src/runs/useAllRunsQuery' diff --git a/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts b/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts index 80dd694e905..f8f9898d170 100644 --- a/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts +++ b/app/src/organisms/ProtocolUpload/hooks/useMostRecentRunId.ts @@ -1,6 +1,6 @@ import last from 'lodash/last' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' export function useMostRecentRunId(): string | null { const { data: allRuns } = useNotifyAllRunsQuery() diff --git a/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx b/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx index ab37ec6c37f..8dffbac17ec 100644 --- a/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx +++ b/app/src/organisms/ProtocolsLanding/ProtocolCard.tsx @@ -94,7 +94,7 @@ export function ProtocolCard(props: ProtocolCardProps): JSX.Element | null { return ( {t('start_setup')} @@ -160,6 +166,10 @@ export function ProtocolOverflowMenu( {t('shared:delete')} diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx index d1654558078..275e9490011 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/ModuleCalibrationOverflowMenu.tsx @@ -16,7 +16,7 @@ import { import { Tooltip } from '../../../atoms/Tooltip' import { OverflowBtn } from '../../../atoms/MenuList/OverflowBtn' import { MenuItem } from '../../../atoms/MenuList/MenuItem' -import { useChainLiveCommands } from '../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../resources/runs' import { useMenuHandleClickOutside } from '../../../atoms/MenuList/hooks' import { useRunStatuses } from '../../Devices/hooks' import { getModulePrepCommands } from '../../Devices/getModulePrepCommands' diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx index aa6a1a68536..b127dcbb669 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx @@ -4,6 +4,7 @@ import { saveAs } from 'file-saver' import { Flex, + BORDERS, COLORS, POSITION_ABSOLUTE, DIRECTION_COLUMN, @@ -38,6 +39,7 @@ import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsE import type { PipetteName } from '@opentrons/shared-data' import type { DeleteCalRequestParams } from '@opentrons/api-client' import type { SelectablePipettes } from '../../PipetteWizardFlows/types' +import { css } from 'styled-components' interface OverflowMenuProps { calType: 'pipetteOffset' | 'tipLength' @@ -194,7 +196,7 @@ export function OverflowMenu({ ref={calsOverflowWrapperRef} whiteSpace="nowrap" zIndex={10} - borderRadius="4px 4px 0px 0px" + borderRadius={BORDERS.borderRadius8} boxShadow="0px 1px 3px rgba(0, 0, 0, 0.2)" position={POSITION_ABSOLUTE} backgroundColor={COLORS.white} @@ -203,7 +205,12 @@ export function OverflowMenu({ flexDirection={DIRECTION_COLUMN} > {isPipetteForFlex ? ( - + {t( ot3PipCal == null ? 'robot_calibration:calibrate_pipette' @@ -212,13 +219,24 @@ export function OverflowMenu({ ) : ( <> - + {t('download_calibration_data')} {t('robot_calibration:delete_calibration_data')} diff --git a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx index 4528c6bfe7b..44bcb21836c 100644 --- a/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx +++ b/app/src/organisms/RobotSettingsCalibration/CalibrationDetails/__tests__/ModuleCalibrationOverflowMenu.test.tsx @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { i18n } from '../../../../i18n' import { renderWithProviders } from '../../../../__testing-utils__' import { ModuleWizardFlows } from '../../../ModuleWizardFlows' -import { useChainLiveCommands } from '../../../../resources/runs/hooks' +import { useChainLiveCommands } from '../../../../resources/runs' import { mockThermocyclerGen2 } from '../../../../redux/modules/__fixtures__' import { useRunStatuses } from '../../../Devices/hooks' import { useIsEstopNotDisengaged } from '../../../../resources/devices/hooks/useIsEstopNotDisengaged' @@ -17,7 +17,7 @@ import type { Mount } from '@opentrons/components' vi.mock('@opentrons/react-api-client') vi.mock('../../../ModuleWizardFlows') vi.mock('../../../Devices/hooks') -vi.mock('../../../../resources/runs/hooks') +vi.mock('../../../../resources/runs') vi.mock('../../../../resources/devices/hooks/useIsEstopNotDisengaged') const mockPipetteOffsetCalibrations = [ diff --git a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx index ebe9772241a..6f361a15b48 100644 --- a/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx +++ b/app/src/organisms/RobotSettingsDashboard/DeviceReset.tsx @@ -40,7 +40,7 @@ const OptionButton = styled.input` const OptionLabel = styled.label` padding: ${SPACING.spacing16} ${SPACING.spacing24}; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; color: ${({ isSelected }) => isSelected === true ? COLORS.white : COLORS.black90}; background: ${({ isSelected }) => diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx index fcae6ce7937..2e78cfe2a46 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/EthernetConnectionDetails.tsx @@ -25,7 +25,7 @@ const STRETCH_LIST_STYLE = css` width: 100%; padding: ${SPACING.spacing16}; background-color: ${COLORS.grey35}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius8}; ` interface EthernetConnectionDetailsProps { diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx index fd515306a4d..5657f30e674 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/NetworkDetailsModal.tsx @@ -78,7 +78,7 @@ function ListItem({ itemName, itemValue }: ListItemProps): JSX.Element { padding={`${SPACING.spacing16} ${SPACING.spacing24}`} backgroundColor={COLORS.grey40} justifyContent={JUSTIFY_SPACE_BETWEEN} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius8} > {itemName} diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx index 82423db475e..852f0f65862 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/WifiConnectionDetails.tsx @@ -88,7 +88,7 @@ export function WifiConnectionDetails({ width="100%" padding={SPACING.spacing24} backgroundColor={COLORS.green35} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} onClick={() => setShowNetworkDetailModal(true)} alignItems={ALIGN_CENTER} > diff --git a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx index 08f61fbfcb8..3aadf02256d 100644 --- a/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx +++ b/app/src/organisms/RobotSettingsDashboard/NetworkSettings/index.tsx @@ -113,7 +113,7 @@ function NetworkSettingButton({ paddingX={SPACING.spacing24} paddingY={SPACING.spacing20} backgroundColor={backgroundColor} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} css={PUSHED_STATE_STYLE} onClick={onClick} > diff --git a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx index 50d69fc7ffc..3b708fa3253 100644 --- a/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx +++ b/app/src/organisms/RobotSettingsDashboard/RobotSystemVersion.tsx @@ -83,7 +83,7 @@ export function RobotSystemVersion({ flexDirection={DIRECTION_ROW} padding={`${SPACING.spacing16} ${SPACING.spacing24}`} justifyContent={JUSTIFY_SPACE_BETWEEN} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius8} > props.isActive ? COLORS.purple50 : COLORS.purple35}; ` diff --git a/app/src/organisms/RobotSettingsDashboard/TouchscreenBrightness.tsx b/app/src/organisms/RobotSettingsDashboard/TouchscreenBrightness.tsx index f16f2273a33..1ce6a75ce83 100644 --- a/app/src/organisms/RobotSettingsDashboard/TouchscreenBrightness.tsx +++ b/app/src/organisms/RobotSettingsDashboard/TouchscreenBrightness.tsx @@ -35,7 +35,7 @@ interface BrightnessTileProps { const BrightnessTile = styled(Box)` width: 100%; height: 8.75rem; - border-radius: ${BORDERS.borderRadiusSize2}; + border-radius: ${BORDERS.borderRadius8}; background: ${(props: BrightnessTileProps) => props.isActive ? COLORS.blue50 : COLORS.blue35}; ` diff --git a/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx b/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx index 36b6628f697..180486f76a8 100644 --- a/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx +++ b/app/src/organisms/RobotSettingsDashboard/UpdateChannel.tsx @@ -33,7 +33,7 @@ const SettingButton = styled.input` const SettingButtonLabel = styled.label` padding: ${SPACING.spacing24}; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; cursor: pointer; background: ${({ isSelected }) => isSelected === true ? COLORS.blue50 : COLORS.blue35}; diff --git a/app/src/organisms/RunPreview/index.tsx b/app/src/organisms/RunPreview/index.tsx index 605db840cc9..da10a6b7456 100644 --- a/app/src/organisms/RunPreview/index.tsx +++ b/app/src/organisms/RunPreview/index.tsx @@ -19,7 +19,7 @@ import { import { StyledText } from '../../atoms/text' import { useMostRecentCompletedAnalysis } from '../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useNotifyLastRunCommandKey } from '../../resources/runs/useNotifyLastRunCommandKey' +import { useNotifyLastRunCommandKey } from '../../resources/runs' import { CommandText } from '../CommandText' import { Divider } from '../../atoms/structure' import { NAV_BAR_WIDTH } from '../../App/constants' @@ -117,7 +117,7 @@ export const RunPreviewComponent = ( index === jumpedIndex ? '#F5E3FF' : backgroundColor } color={COLORS.black90} - borderRadius={BORDERS.radiusSoftCorners} + borderRadius={BORDERS.borderRadius8} padding={SPACING.spacing8} css={css` transition: background-color ${COLOR_FADE_MS}ms ease-out, diff --git a/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx b/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx index d4657b06174..aba56366b27 100644 --- a/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx +++ b/app/src/organisms/RunProgressMeter/__tests__/RunProgressMeter.test.tsx @@ -18,7 +18,10 @@ import { InterventionModal } from '../../InterventionModal' import { ProgressBar } from '../../../atoms/ProgressBar' import { useRunStatus } from '../../RunTimeControl/hooks' import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useNotifyLastRunCommandKey } from '../../../resources/runs/useNotifyLastRunCommandKey' +import { + useNotifyLastRunCommandKey, + useNotifyRunQuery, +} from '../../../resources/runs' import { useDownloadRunLog } from '../../Devices/hooks' import { mockUseAllCommandsResponseNonDeterministic, @@ -31,7 +34,6 @@ import { mockRunData, } from '../../InterventionModal/__fixtures__' import { RunProgressMeter } from '..' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' import { renderWithProviders } from '../../../__testing-utils__' import type * as ApiClient from '@opentrons/react-api-client' @@ -45,11 +47,10 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { }) vi.mock('../../RunTimeControl/hooks') vi.mock('../../LabwarePositionCheck/useMostRecentCompletedAnalysis') -vi.mock('../../../resources/runs/useNotifyLastRunCommandKey') +vi.mock('../../../resources/runs') vi.mock('../../Devices/hooks') vi.mock('../../../atoms/ProgressBar') vi.mock('../../InterventionModal') -vi.mock('../../../resources/runs/useNotifyRunQuery') const render = (props: React.ComponentProps) => { return renderWithProviders(, { diff --git a/app/src/organisms/RunProgressMeter/index.tsx b/app/src/organisms/RunProgressMeter/index.tsx index 532862dff91..0b5d2ae5424 100644 --- a/app/src/organisms/RunProgressMeter/index.tsx +++ b/app/src/organisms/RunProgressMeter/index.tsx @@ -42,7 +42,7 @@ import { ProgressBar } from '../../atoms/ProgressBar' import { useDownloadRunLog, useRobotType } from '../Devices/hooks' import { InterventionTicks } from './InterventionTicks' import { isInterventionCommand } from '../InterventionModal/utils' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { RunStatus } from '@opentrons/api-client' @@ -259,14 +259,14 @@ export function RunProgressMeter(props: RunProgressMeterProps): JSX.Element { outerStyles={css` height: 0.375rem; background-color: ${COLORS.grey30}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; position: relative; overflow: initial; `} innerStyles={css` height: 0.375rem; background-color: ${COLORS.grey60}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; `} > { }) vi.mock('../../ProtocolUpload/hooks') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') describe('useRunControls hook', () => { it('returns run controls hooks', () => { diff --git a/app/src/organisms/RunTimeControl/hooks.ts b/app/src/organisms/RunTimeControl/hooks.ts index 1c676077d98..e7a961e558a 100644 --- a/app/src/organisms/RunTimeControl/hooks.ts +++ b/app/src/organisms/RunTimeControl/hooks.ts @@ -20,7 +20,7 @@ import { useCurrentRunId, useRunCommands, } from '../ProtocolUpload/hooks' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { UseQueryOptions } from 'react-query' import type { RunAction, RunStatus, Run, RunData } from '@opentrons/api-client' diff --git a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx index 0e33a4a2807..9f5279aa18f 100644 --- a/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx +++ b/app/src/organisms/SendProtocolToFlexSlideout/__tests__/SendProtocolToFlexSlideout.test.tsx @@ -34,7 +34,7 @@ import { getNetworkInterfaces } from '../../../redux/networking' import { getIsProtocolAnalysisInProgress } from '../../../redux/protocol-storage/selectors' import { storedProtocolData as storedProtocolDataFixture } from '../../../redux/protocol-storage/__fixtures__' import { SendProtocolToFlexSlideout } from '..' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import type * as ApiClient from '@opentrons/react-api-client' @@ -51,7 +51,7 @@ vi.mock('../../../redux/discovery') vi.mock('../../../redux/networking') vi.mock('../../../redux/custom-labware') vi.mock('../../../redux/protocol-storage/selectors') -vi.mock('../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../resources/runs') const render = ( props: React.ComponentProps diff --git a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx index fcc7c3c73fe..46b2062de39 100644 --- a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx +++ b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun' +import { useNotifyCurrentMaintenanceRun } from '../../resources/maintenance_runs' interface MaintenanceRunIds { currentRunId: string | null diff --git a/app/src/organisms/TakeoverModal/TakeoverModal.tsx b/app/src/organisms/TakeoverModal/TakeoverModal.tsx index 0012e663623..38c6196e0b9 100644 --- a/app/src/organisms/TakeoverModal/TakeoverModal.tsx +++ b/app/src/organisms/TakeoverModal/TakeoverModal.tsx @@ -77,7 +77,7 @@ export function TakeoverModal(props: TakeoverModalProps): JSX.Element { ({ diff --git a/app/src/organisms/TaskList/index.tsx b/app/src/organisms/TaskList/index.tsx index 9f2351330a6..c90547da5fa 100644 --- a/app/src/organisms/TaskList/index.tsx +++ b/app/src/organisms/TaskList/index.tsx @@ -223,7 +223,7 @@ function SubTask({ ? BORDERS.activeLineBorder : `1px solid ${COLORS.grey30}` } - borderRadius={BORDERS.radiusSoftCorners} + borderRadius={BORDERS.borderRadius4} gridGap={SPACING.spacing24} width="100%" > @@ -366,7 +366,7 @@ function Task({ border={ isActiveTask && !isTaskOpen ? BORDERS.activeLineBorder : undefined } - borderRadius={BORDERS.radiusSoftCorners} + borderRadius={BORDERS.borderRadius4} width="100%" > diff --git a/app/src/organisms/UpdateRobotSoftware/CompleteUpdateSoftware.tsx b/app/src/organisms/UpdateRobotSoftware/CompleteUpdateSoftware.tsx index 631551adde6..0f083ea5f26 100644 --- a/app/src/organisms/UpdateRobotSoftware/CompleteUpdateSoftware.tsx +++ b/app/src/organisms/UpdateRobotSoftware/CompleteUpdateSoftware.tsx @@ -33,7 +33,7 @@ export function CompleteUpdateSoftware({ gridGap={SPACING.spacing40} alignItems={ALIGN_CENTER} justifyContent={JUSTIFY_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} > diff --git a/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx b/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx index f416a65d141..2791d9af36e 100644 --- a/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx +++ b/app/src/pages/ConnectViaEthernet/DisplayConnectionStatus.tsx @@ -35,7 +35,7 @@ export function DisplayConnectionStatus({ { return renderWithProviders( diff --git a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx b/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx index f3d8a9c5515..513bccb9874 100644 --- a/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx +++ b/app/src/pages/Devices/DeviceDetails/DeviceDetailsComponent.tsx @@ -4,6 +4,7 @@ import { ALIGN_CENTER, Box, COLORS, + BORDERS, DIRECTION_COLUMN, Flex, SPACING, @@ -49,7 +50,7 @@ export function DeviceDetailsComponent({ {protocolRunDetailsContent} diff --git a/app/src/pages/Devices/RobotSettings/index.tsx b/app/src/pages/Devices/RobotSettings/index.tsx index ea2e8cabaaa..f9422adfcd4 100644 --- a/app/src/pages/Devices/RobotSettings/index.tsx +++ b/app/src/pages/Devices/RobotSettings/index.tsx @@ -107,7 +107,7 @@ export function RobotSettings(): JSX.Element | null { { getPipetteModelSpecs: vi.fn(), } }) -vi.mock('../../../resources/maintenance_runs/useNotifyCurrentMaintenanceRun') +vi.mock('../../../resources/maintenance_runs') vi.mock('../../../organisms/PipetteWizardFlows') vi.mock('../../../organisms/GripperWizardFlows') vi.mock('../../../organisms/DropTipWizard') diff --git a/app/src/pages/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx b/app/src/pages/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx index 509b8b7639e..c85a176c1e2 100644 --- a/app/src/pages/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx +++ b/app/src/pages/InstrumentsDashboard/PipetteRecalibrationODDWarning.tsx @@ -22,7 +22,7 @@ export const PipetteRecalibrationODDWarning = (): JSX.Element | null => { - - - {t('category')} - - { - setFilterBy(value as LabwareFilter) - }} - /> - + { + setFilterBy(value as LabwareFilter) + }} + title={t('category')} + /> {t('shared:sort_by')} @@ -161,7 +157,7 @@ export function Labware(): JSX.Element { e.target.focus()} /> )} /> diff --git a/app/src/pages/ProtocolDashboard/NoProtocols.tsx b/app/src/pages/ProtocolDashboard/NoProtocols.tsx index 85bc5f7cb52..22b740ead48 100644 --- a/app/src/pages/ProtocolDashboard/NoProtocols.tsx +++ b/app/src/pages/ProtocolDashboard/NoProtocols.tsx @@ -25,7 +25,7 @@ export function NoProtocols(): JSX.Element { flexDirection={DIRECTION_COLUMN} height="27.25rem" justifyContent={JUSTIFY_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} > {t('nothing_here_yet')} handleProtocolClick(longpress, protocol.id)} diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx index e27d18da0f7..2fce4e5b988 100644 --- a/app/src/pages/ProtocolDashboard/index.tsx +++ b/app/src/pages/ProtocolDashboard/index.tsx @@ -28,7 +28,7 @@ import { sortProtocols } from './utils' import { ProtocolCard } from './ProtocolCard' import { NoProtocols } from './NoProtocols' import { DeleteProtocolConfirmationModal } from './DeleteProtocolConfirmationModal' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' import type { Dispatch } from '../../redux/types' import type { ProtocolsOnDeviceSortKey } from '../../redux/config/types' diff --git a/app/src/pages/ProtocolDetails/EmptySection.tsx b/app/src/pages/ProtocolDetails/EmptySection.tsx index 6386ad731e4..29dbc6ecc6a 100644 --- a/app/src/pages/ProtocolDetails/EmptySection.tsx +++ b/app/src/pages/ProtocolDetails/EmptySection.tsx @@ -14,17 +14,23 @@ import { useTranslation } from 'react-i18next' import { StyledText } from '../../atoms/text' interface EmptySectionProps { - section: 'hardware' | 'labware' | 'liquids' + section: 'hardware' | 'labware' | 'liquids' | 'parameters' } export const EmptySection = (props: EmptySectionProps): JSX.Element => { const { section } = props const { t, i18n } = useTranslation('protocol_details') + let sectionText: string = t('not_in_protocol', { section: section }) + if (section === 'liquids') { + sectionText = t('liquids_not_in_protocol') + } else if (section === 'parameters') { + sectionText = t('no_parameters') + } return ( { aria-label="EmptySection_ot-alert" /> - {i18n.format( - section === 'liquids' - ? t('liquids_not_in_protocol') - : t('not_in_protocol', { section: section }), - 'capitalize' - )} + {i18n.format(sectionText, 'capitalize')} ) diff --git a/app/src/pages/ProtocolDetails/Hardware.tsx b/app/src/pages/ProtocolDetails/Hardware.tsx index 2b7d535255c..030990e848a 100644 --- a/app/src/pages/ProtocolDetails/Hardware.tsx +++ b/app/src/pages/ProtocolDetails/Hardware.tsx @@ -51,12 +51,12 @@ const TableDatum = styled('td')` white-space: break-spaces; text-overflow: ${WRAP}; &:first-child { - border-top-left-radius: ${BORDERS.borderRadiusSize4}; - border-bottom-left-radius: ${BORDERS.borderRadiusSize4}; + border-top-left-radius: ${BORDERS.borderRadius8}; + border-bottom-left-radius: ${BORDERS.borderRadius8}; } &:last-child { - border-top-right-radius: ${BORDERS.borderRadiusSize4}; - border-bottom-right-radius: ${BORDERS.borderRadiusSize4}; + border-top-right-radius: ${BORDERS.borderRadius8}; + border-bottom-right-radius: ${BORDERS.borderRadius8}; } ` diff --git a/app/src/pages/ProtocolDetails/Labware.tsx b/app/src/pages/ProtocolDetails/Labware.tsx index ab7dddcb6dc..9f495136c0e 100644 --- a/app/src/pages/ProtocolDetails/Labware.tsx +++ b/app/src/pages/ProtocolDetails/Labware.tsx @@ -42,12 +42,12 @@ const TableDatum = styled('td')` white-space: break-spaces; text-overflow: ${WRAP}; &:first-child { - border-top-left-radius: ${BORDERS.borderRadiusSize4}; - border-bottom-left-radius: ${BORDERS.borderRadiusSize4}; + border-top-left-radius: ${BORDERS.borderRadius16}; + border-bottom-left-radius: ${BORDERS.borderRadius16}; } &:last-child { - border-top-right-radius: ${BORDERS.borderRadiusSize4}; - border-bottom-right-radius: ${BORDERS.borderRadiusSize4}; + border-top-right-radius: ${BORDERS.borderRadius16}; + border-bottom-right-radius: ${BORDERS.borderRadius16}; } ` diff --git a/app/src/pages/ProtocolDetails/Liquids.tsx b/app/src/pages/ProtocolDetails/Liquids.tsx index 7d4b2f5e7ca..8c91b644743 100644 --- a/app/src/pages/ProtocolDetails/Liquids.tsx +++ b/app/src/pages/ProtocolDetails/Liquids.tsx @@ -31,7 +31,7 @@ import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' const Table = styled('table')` table-layout: ${SPACING.spacingAuto}; width: 100%; - border-spacing: 0 ${BORDERS.borderRadiusSize2}; + border-spacing: 0 ${BORDERS.borderRadius8}; margin: ${SPACING.spacing16} 0; text-align: ${TYPOGRAPHY.textAlignLeft}; ` @@ -52,13 +52,13 @@ const TableDatum = styled('td')` white-space: break-spaces; text-overflow: ${WRAP}; &:first-child { - border-top-left-radius: ${BORDERS.borderRadiusSize3}; - border-bottom-left-radius: ${BORDERS.borderRadiusSize3}; + border-top-left-radius: ${BORDERS.borderRadius12}; + border-bottom-left-radius: ${BORDERS.borderRadius12}; width: 80%; } &:last-child { - border-top-right-radius: ${BORDERS.borderRadiusSize3}; - border-bottom-right-radius: ${BORDERS.borderRadiusSize3}; + border-top-right-radius: ${BORDERS.borderRadius12}; + border-bottom-right-radius: ${BORDERS.borderRadius12}; } ` @@ -106,7 +106,7 @@ export const Liquids = (props: { protocolId: string }): JSX.Element => { alignItems={TYPOGRAPHY.textAlignCenter} > { { + const runTimeParameters = useRunTimeParameters(props.protocolId) + const { makeSnackbar } = useToaster() + const { t, i18n } = useTranslation('protocol_details') + + const makeSnack = (): void => { + makeSnackbar(t('start_setup_customize_values')) + } + + const getRange = (parameter: RunTimeParameter): string => { + const { type } = parameter + const min = 'min' in parameter ? parameter.min : 0 + const max = 'max' in parameter ? parameter.max : 0 + const numChoices = 'choices' in parameter ? parameter.choices.length : 0 + let range: string | null = null + if (numChoices === 2 && 'choices' in parameter) { + range = `${parameter.choices[0].displayName}, ${parameter.choices[1].displayName}` + } + + switch (type) { + case 'boolean': { + return t('on_off') + } + case 'float': + case 'int': { + return `${min}-${max}` + } + case 'str': { + return range ?? t('num_choices', { num: numChoices }) + } + default: + // Should never hit this case + return '' + } + } + + const getDefault = (parameter: RunTimeParameter): string => { + const { type, default: defaultValue } = parameter + const suffix = + 'suffix' in parameter && parameter.suffix != null ? parameter.suffix : '' + switch (type) { + case 'int': + case 'float': + return `${defaultValue.toString()} ${suffix}` + case 'boolean': + return Boolean(defaultValue) ? t('on') : t('off') + case 'str': + if ('choices' in parameter && parameter.choices != null) { + const choice = parameter.choices.find( + choice => choice.value === defaultValue + ) + if (choice != null) { + return choice.displayName + } + } + break + } + return '' + } + + return runTimeParameters.length > 0 ? ( + + + + + + {i18n.format(t('name'), 'capitalize')} + + + + + {i18n.format(t('default_value'), 'capitalize')} + + + + + {i18n.format(t('range'), 'capitalize')} + + + + + + {runTimeParameters.map((parameter, index) => { + return ( + + + + {parameter.displayName} + + + + + {getDefault(parameter)} + + + + + {getRange(parameter)} + + + + ) + })} + +
+ ) : ( + + ) +} diff --git a/app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx b/app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx index 3561bc66117..5e340240720 100644 --- a/app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx +++ b/app/src/pages/ProtocolDetails/__tests__/EmptySection.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { it, describe } from 'vitest' +import { screen } from '@testing-library/react' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { EmptySection } from '../EmptySection' @@ -17,22 +18,29 @@ describe('EmptySection', () => { props = { section: 'labware', } - const { getByText, getByLabelText } = render(props) - getByLabelText('EmptySection_ot-alert') - getByText('No labware is specified for this protocol') + render(props) + screen.getByLabelText('EmptySection_ot-alert') + screen.getByText('No labware is specified for this protocol') }) it('should render text for liquid', () => { props = { section: 'liquids', } - const { getByText } = render(props) - getByText('No liquids are specified for this protocol') + render(props) + screen.getByText('No liquids are specified for this protocol') }) it('should render text for hardware', () => { props = { section: 'hardware', } - const { getByText } = render(props) - getByText('No hardware is specified for this protocol') + render(props) + screen.getByText('No hardware is specified for this protocol') + }) + it('should render text for parameters', () => { + props = { + section: 'parameters', + } + render(props) + screen.getByText('No parameters specified in this protocol') }) }) diff --git a/app/src/pages/ProtocolDetails/__tests__/Parameters.test.tsx b/app/src/pages/ProtocolDetails/__tests__/Parameters.test.tsx new file mode 100644 index 00000000000..615972e53d9 --- /dev/null +++ b/app/src/pages/ProtocolDetails/__tests__/Parameters.test.tsx @@ -0,0 +1,145 @@ +import * as React from 'react' +import { when } from 'vitest-when' +import { it, describe, beforeEach, vi } from 'vitest' +import { screen } from '@testing-library/react' +import { i18n } from '../../../i18n' +import { useToaster } from '../../../organisms/ToasterOven' +import { renderWithProviders } from '../../../__testing-utils__' +import { useRunTimeParameters } from '../../Protocols/hooks' +import { Parameters } from '../Parameters' +import type { RunTimeParameter } from '@opentrons/shared-data' + +vi.mock('../../../organisms/ToasterOven') +vi.mock('../../Protocols/hooks') + +const mockRTPData: RunTimeParameter[] = [ + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'a dry run description', + type: 'boolean', + default: false, + }, + { + displayName: 'Use Gripper', + variableName: 'USE_GRIPPER', + description: '', + type: 'boolean', + default: true, + }, + { + displayName: 'Trash Tips', + variableName: 'TIP_TRASH', + description: 'throw tip in trash', + type: 'boolean', + default: true, + }, + { + displayName: 'Deactivate Temperatures', + variableName: 'DEACTIVATE_TEMP', + description: 'deactivate temperature?', + type: 'boolean', + default: true, + }, + { + displayName: 'Columns of Samples', + variableName: 'COLUMNS', + description: '', + suffix: 'mL', + type: 'int', + min: 1, + max: 14, + default: 4, + }, + { + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: '', + type: 'int', + min: 1, + max: 10, + default: 6, + }, + { + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '', + type: 'float', + min: 1.5, + max: 10.0, + default: 6.5, + }, + { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + description: '', + type: 'str', + choices: [ + { + displayName: 'no offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, + { + displayName: '2 choices', + variableName: 'TWO', + description: '', + type: 'str', + choices: [ + { + displayName: 'one choice', + value: '1', + }, + { + displayName: 'the second', + value: '2', + }, + ], + default: '2', + }, +] + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +const MOCK_MAKE_SNACK_BAR = vi.fn() +describe('Parameters', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + protocolId: 'mockId', + } + when(useToaster) + .calledWith() + .thenReturn({ + makeSnackBar: MOCK_MAKE_SNACK_BAR, + } as any) + vi.mocked(useRunTimeParameters).mockReturnValue(mockRTPData) + }) + it('renders the parameters labels and mock data', () => { + render(props) + screen.getByText('Name') + screen.getByText('Default value') + screen.getByText('Range') + screen.getByText('Dry Run') + screen.getByText('6.5') + screen.getByText('Use Gripper') + screen.getByText('Default Module Offsets') + screen.getByText('3 choices') + screen.getByText('EtoH Volume') + screen.getByText('one choice, the second') + }) +}) diff --git a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx index 004a31ef865..0e6bfb0da8c 100644 --- a/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx +++ b/app/src/pages/ProtocolDetails/__tests__/ProtocolDetails.test.tsx @@ -21,11 +21,13 @@ import { i18n } from '../../../i18n' import { useHardwareStatusText } from '../../../organisms/OnDeviceDisplay/RobotDashboard/hooks' import { useOffsetCandidatesForAnalysis } from '../../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { useMissingProtocolHardware } from '../../Protocols/hooks' -import { formatTimeWithUtcLabel } from '../../../resources/runs/utils' +import { useFeatureFlag } from '../../../redux/config' +import { formatTimeWithUtcLabel } from '../../../resources/runs' import { ProtocolDetails } from '..' import { Deck } from '../Deck' import { Hardware } from '../Hardware' import { Labware } from '../Labware' +import { Parameters } from '../Parameters' // Mock IntersectionObserver class IntersectionObserver { @@ -50,6 +52,8 @@ vi.mock('../../Protocols/hooks') vi.mock('../Deck') vi.mock('../Hardware') vi.mock('../Labware') +vi.mock('../Parameters') +vi.mock('../../../redux/config') const MOCK_HOST_CONFIG = {} as HostConfig const mockCreateRun = vi.fn((id: string) => {}) @@ -88,6 +92,7 @@ const render = (path = '/protocols/fakeProtocolId') => { describe('ODDProtocolDetails', () => { beforeEach(() => { + vi.mocked(useFeatureFlag).mockReturnValue(true) vi.mocked(useCreateRunMutation).mockReturnValue({ createRun: mockCreateRun, } as any) @@ -181,7 +186,11 @@ describe('ODDProtocolDetails', () => { vi.mocked(Hardware).mockReturnValue(
Mock Hardware
) vi.mocked(Labware).mockReturnValue(
Mock Labware
) vi.mocked(Deck).mockReturnValue(
Mock Initial Deck Layout
) + vi.mocked(Parameters).mockReturnValue(
Mock Parameters
) + render() + const parametersButton = screen.getByRole('button', { name: 'Parameters' }) + fireEvent.click(parametersButton) const hardwareButton = screen.getByRole('button', { name: 'Hardware' }) fireEvent.click(hardwareButton) screen.getByText('Mock Hardware') diff --git a/app/src/pages/ProtocolDetails/index.tsx b/app/src/pages/ProtocolDetails/index.tsx index 43e35739e31..caea2206208 100644 --- a/app/src/pages/ProtocolDetails/index.tsx +++ b/app/src/pages/ProtocolDetails/index.tsx @@ -44,19 +44,21 @@ import { getApplyHistoricOffsets, getPinnedProtocolIds, updateConfigValue, + useFeatureFlag, } from '../../redux/config' +import { useOffsetCandidatesForAnalysis } from '../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { useMissingProtocolHardware } from '../Protocols/hooks' +import { Parameters } from './Parameters' import { Deck } from './Deck' import { Hardware } from './Hardware' import { Labware } from './Labware' import { Liquids } from './Liquids' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel } from '../../resources/runs' import type { Protocol } from '@opentrons/api-client' import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { Dispatch } from '../../redux/types' import type { OnDeviceRouteParams } from '../../App/types' -import { useOffsetCandidatesForAnalysis } from '../../organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' interface ProtocolHeaderProps { title?: string | null @@ -155,6 +157,14 @@ const ProtocolHeader = ({ } const protocolSectionTabOptions = [ + 'Summary', + 'Parameters', + 'Hardware', + 'Labware', + 'Liquids', + 'Deck', +] as const +const protocolSectionTabOptionsWithoutParameters = [ 'Summary', 'Hardware', 'Labware', @@ -162,7 +172,9 @@ const protocolSectionTabOptions = [ 'Deck', ] as const -type TabOption = typeof protocolSectionTabOptions[number] +type TabOption = + | typeof protocolSectionTabOptions[number] + | typeof protocolSectionTabOptionsWithoutParameters[number] interface ProtocolSectionTabsProps { currentOption: TabOption @@ -173,9 +185,13 @@ const ProtocolSectionTabs = ({ currentOption, setCurrentOption, }: ProtocolSectionTabsProps): JSX.Element => { + const enableRtpFF = useFeatureFlag('enableRunTimeParameters') + const options = enableRtpFF + ? protocolSectionTabOptions + : protocolSectionTabOptionsWithoutParameters return ( - {protocolSectionTabOptions.map(option => { + {options.map(option => { return ( {
) break + case 'Parameters': + protocolSection = + break case 'Hardware': protocolSection = break @@ -285,6 +304,7 @@ export function ProtocolDetails(): JSX.Element | null { 'protocol_info', 'shared', ]) + const enableRtpFF = useFeatureFlag('enableRunTimeParameters') const { protocolId } = useParams() const { missingProtocolHardware, @@ -300,8 +320,11 @@ export function ProtocolDetails(): JSX.Element | null { const { makeSnackbar } = useToaster() const queryClient = useQueryClient() const [currentOption, setCurrentOption] = React.useState( - protocolSectionTabOptions[0] + enableRtpFF + ? protocolSectionTabOptions[0] + : protocolSectionTabOptionsWithoutParameters[0] ) + const [showMaxPinsAlert, setShowMaxPinsAlert] = React.useState(false) const { data: protocolRecord, diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index bd14dc90f1d..11906d3d1b8 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -51,7 +51,7 @@ import { useIsHeaterShakerInProtocol } from '../../../organisms/ModuleCard/hooks import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks' import { ConfirmAttachedModal } from '../../../pages/ProtocolSetup/ConfirmAttachedModal' import { ProtocolSetup } from '../../../pages/ProtocolSetup' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../../resources/runs' import { mockConnectableRobot } from '../../../redux/discovery/__fixtures__' import type { UseQueryResult } from 'react-query' @@ -107,7 +107,7 @@ vi.mock('../../../redux/discovery/selectors') vi.mock('../ConfirmAttachedModal') vi.mock('../../../organisms/ToasterOven') vi.mock('../../../resources/deck_configuration/hooks') -vi.mock('../../../resources/runs/useNotifyRunQuery') +vi.mock('../../../resources/runs') const render = (path = '/') => { return renderWithProviders( diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 98c29a987dd..d94d7c9e372 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -54,7 +54,7 @@ import { import { useRequiredProtocolHardwareFromAnalysis, useMissingProtocolHardwareFromAnalysis, -} from '../../pages/Protocols/hooks' +} from '../Protocols/hooks' import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo' import { ProtocolSetupLabware } from '../../organisms/ProtocolSetupLabware' import { ProtocolSetupModulesAndDeck } from '../../organisms/ProtocolSetupModulesAndDeck' @@ -74,7 +74,7 @@ import { } from '../../organisms/RunTimeControl/hooks' import { useToaster } from '../../organisms/ToasterOven' import { useIsHeaterShakerInProtocol } from '../../organisms/ModuleCard/hooks' -import { getLabwareSetupItemGroups } from '../../pages/Protocols/utils' +import { getLabwareSetupItemGroups } from '../Protocols/utils' import { getLocalRobot, getRobotSerialNumber } from '../../redux/discovery' import { ANALYTICS_PROTOCOL_PROCEED_TO_RUN, @@ -82,12 +82,12 @@ import { useTrackEvent, } from '../../redux/analytics' import { getIsHeaterShakerAttached } from '../../redux/config' -import { ConfirmAttachedModal } from '../../pages/ProtocolSetup/ConfirmAttachedModal' +import { ConfirmAttachedModal } from './ConfirmAttachedModal' import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' -import { CloseButton, PlayButton } from '../../pages/ProtocolSetup/Buttons' +import { CloseButton, PlayButton } from './Buttons' import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' import { getRequiredDeckConfig } from '../../resources/deck_configuration/utils' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' +import { useNotifyRunQuery } from '../../resources/runs' import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' import type { OnDeviceRouteParams } from '../../App/types' @@ -166,7 +166,7 @@ export function ProtocolSetupStep({ backgroundColor={ disabled ? COLORS.grey35 : backgroundColorByStepStatus[status] } - borderRadius={BORDERS.borderRadiusSize4} + borderRadius={BORDERS.borderRadius16} gridGap={SPACING.spacing16} padding={`${SPACING.spacing20} ${SPACING.spacing24}`} css={PUSHED_STATE_STYLE} diff --git a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx index 54a9c0455e0..ce09a610ff7 100644 --- a/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx +++ b/app/src/pages/Protocols/hooks/__tests__/hooks.test.tsx @@ -19,7 +19,11 @@ import { WASTE_CHUTE_RIGHT_ADAPTER_NO_COVER_FIXTURE, fixtureTiprack300ul, } from '@opentrons/shared-data' -import { useMissingProtocolHardware, useRequiredProtocolLabware } from '..' +import { + useMissingProtocolHardware, + useRequiredProtocolLabware, + useRunTimeParameters, +} from '../index' import type { Protocol } from '@opentrons/api-client' import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' @@ -29,7 +33,85 @@ vi.mock('../../../../organisms/Devices/hooks') vi.mock('../../../../redux/config') const PROTOCOL_ID = 'fake_protocol_id' - +const mockRTPData = [ + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'a dry run description', + type: 'boolean', + default: false, + }, + { + displayName: 'Use Gripper', + variableName: 'USE_GRIPPER', + description: '', + type: 'boolean', + default: true, + }, + { + displayName: 'Trash Tips', + variableName: 'TIP_TRASH', + description: 'throw tip in trash', + type: 'boolean', + default: true, + }, + { + displayName: 'Deactivate Temperatures', + variableName: 'DEACTIVATE_TEMP', + description: 'deactivate temperature?', + type: 'boolean', + default: true, + }, + { + displayName: 'Columns of Samples', + variableName: 'COLUMNS', + description: '', + suffix: 'mL', + type: 'int', + min: 1, + max: 14, + default: 4, + }, + { + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: '', + type: 'int', + min: 1, + max: 10, + default: 6, + }, + { + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '', + type: 'float', + min: 1.5, + max: 10.0, + default: 6.5, + }, + { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + description: '', + type: 'str', + choices: [ + { + displayName: 'no offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, +] const mockLabwareDef = fixtureTiprack300ul as LabwareDefinition2 const PROTOCOL_ANALYSIS = { id: 'fake analysis', @@ -83,6 +165,7 @@ const PROTOCOL_ANALYSIS = { completedAt: 'fakeCompletedAtTimestamp', }, ], + runTimeParameters: mockRTPData, } as any const NULL_COMMAND = { @@ -108,6 +191,26 @@ const NULL_PROTOCOL_ANALYSIS = { commands: [NULL_COMMAND], } as any +describe('useRunTimeParameters', () => { + beforeEach(() => { + when(vi.mocked(useProtocolQuery)) + .calledWith(PROTOCOL_ID) + .thenReturn({ + data: { + data: { analysisSummaries: [{ id: PROTOCOL_ANALYSIS.id } as any] }, + }, + } as UseQueryResult) + when(vi.mocked(useProtocolAnalysisAsDocumentQuery)) + .calledWith(PROTOCOL_ID, PROTOCOL_ANALYSIS.id, { enabled: true }) + .thenReturn({ + data: PROTOCOL_ANALYSIS, + } as UseQueryResult) + }) + it('return RTP', () => { + const { result } = renderHook(() => useRunTimeParameters(PROTOCOL_ID)) + expect(result.current).toBe(mockRTPData) + }) +}) describe('useRequiredProtocolLabware', () => { beforeEach(() => { when(vi.mocked(useProtocolQuery)) diff --git a/app/src/pages/Protocols/hooks/index.ts b/app/src/pages/Protocols/hooks/index.ts index 42c0ceae8f5..975d03c4690 100644 --- a/app/src/pages/Protocols/hooks/index.ts +++ b/app/src/pages/Protocols/hooks/index.ts @@ -12,6 +12,7 @@ import { SINGLE_SLOT_FIXTURES, getCutoutIdForSlotName, getDeckDefFromRobotType, + RunTimeParameter, } from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../utils' import { getProtocolUsesGripper } from '../../../organisms/ProtocolSetupInstruments/utils' @@ -182,6 +183,158 @@ export const useRequiredProtocolHardwareFromAnalysis = ( } } +/** + * Returns an array of RunTimeParameters objects that are optional by the given protocol ID. + * + * @param {string} protocolId The ID of the protocol for which required hardware is being retrieved. + * @returns {RunTimeParameters[]} An array of RunTimeParameters objects that are required by the given protocol ID. + */ + +export const useRunTimeParameters = ( + protocolId: string +): RunTimeParameter[] => { + const { data: protocolData } = useProtocolQuery(protocolId) + const { data: analysis } = useProtocolAnalysisAsDocumentQuery( + protocolId, + last(protocolData?.data.analysisSummaries)?.id ?? null, + { enabled: protocolData != null } + ) + + const mockData: RunTimeParameter[] = [ + { + displayName: 'Dry Run', + variableName: 'DRYRUN', + description: 'Is this a dry or wet run? Wet is true, dry is false', + type: 'boolean', + default: false, + }, + { + displayName: 'Use Gripper', + variableName: 'USE_GRIPPER', + description: 'For using the gripper.', + type: 'boolean', + default: true, + }, + { + displayName: 'Trash Tips', + variableName: 'TIP_TRASH', + description: + 'to throw tip into the trash or to not throw tip into the trash', + type: 'boolean', + default: true, + }, + { + displayName: 'Deactivate Temperatures', + variableName: 'DEACTIVATE_TEMP', + description: 'deactivate temperature on the module', + type: 'boolean', + default: true, + }, + { + displayName: 'Columns of Samples', + variableName: 'COLUMNS', + description: 'How many columns do you want?', + type: 'int', + min: 1, + max: 14, + default: 4, + }, + { + displayName: 'PCR Cycles', + variableName: 'PCR_CYCLES', + description: 'number of PCR cycles on a thermocycler', + type: 'int', + min: 1, + max: 10, + default: 6, + }, + { + displayName: 'EtoH Volume', + variableName: 'ETOH_VOLUME', + description: '70% ethanol volume', + type: 'float', + suffix: 'mL', + min: 1.5, + max: 10.0, + default: 6.5, + }, + { + displayName: 'Default Module Offsets', + variableName: 'DEFAULT_OFFSETS', + description: 'default module offsets for temp, H-S, and none', + type: 'str', + choices: [ + { + displayName: 'No offsets', + value: 'none', + }, + { + displayName: 'temp offset', + value: '1', + }, + { + displayName: 'heater-shaker offset', + value: '2', + }, + ], + default: 'none', + }, + { + displayName: 'pipette mount', + variableName: 'mont', + description: 'pipette mount', + type: 'str', + choices: [ + { + displayName: 'Left', + value: 'left', + }, + { + displayName: 'Right', + value: 'right', + }, + ], + default: 'left', + }, + { + displayName: 'short test case', + variableName: 'short 2 options', + description: 'this play 2 short options', + type: 'str', + choices: [ + { + displayName: 'OT-2', + value: 'ot2', + }, + { + displayName: 'Flex', + value: 'flex', + }, + ], + default: 'flex', + }, + { + displayName: 'long test case', + variableName: 'long 2 options', + description: 'this play 2 long options', + type: 'str', + choices: [ + { + displayName: 'I am kind of long text version', + value: 'ot2', + }, + { + displayName: 'I am kind of long text version. Today is 3/15', + value: 'flex', + }, + ], + default: 'flex', + }, + ] + // TODO(jr, 3/14/24): remove the mockData + return analysis?.runTimeParameters ?? mockData +} + /** * Returns an array of ProtocolHardware objects that are required by the given protocol ID. * diff --git a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx index a5e0c58fa93..7706a925826 100644 --- a/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx +++ b/app/src/pages/RobotDashboard/__tests__/RobotDashboard.test.tsx @@ -6,14 +6,16 @@ import { renderWithProviders } from '../../../__testing-utils__' import { useAllProtocolsQuery } from '@opentrons/react-api-client' import { i18n } from '../../../i18n' -import { EmptyRecentRun } from '../../../organisms/OnDeviceDisplay/RobotDashboard/EmptyRecentRun' -import { RecentRunProtocolCarousel } from '../../../organisms/OnDeviceDisplay/RobotDashboard' +import { + RecentRunProtocolCarousel, + EmptyRecentRun, +} from '../../../organisms/OnDeviceDisplay/RobotDashboard' import { Navigation } from '../../../organisms/Navigation' import { useMissingProtocolHardware } from '../../Protocols/hooks' import { getOnDeviceDisplaySettings } from '../../../redux/config' import { WelcomeModal } from '../WelcomeModal' import { RobotDashboard } from '..' -import { useNotifyAllRunsQuery } from '../../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../../resources/runs' import type { ProtocolResource } from '@opentrons/shared-data' import type * as ReactRouterDom from 'react-router-dom' @@ -36,7 +38,7 @@ vi.mock('../../../organisms/Navigation') vi.mock('../../Protocols/hooks') vi.mock('../../../redux/config') vi.mock('../WelcomeModal') -vi.mock('../../../resources/runs/useNotifyAllRunsQuery') +vi.mock('../../../resources/runs') const render = () => { return renderWithProviders( diff --git a/app/src/pages/RobotDashboard/index.tsx b/app/src/pages/RobotDashboard/index.tsx index 5b3b462481f..e0b699dea4b 100644 --- a/app/src/pages/RobotDashboard/index.tsx +++ b/app/src/pages/RobotDashboard/index.tsx @@ -21,7 +21,7 @@ import { AnalyticsOptInModal } from './AnalyticsOptInModal' import { WelcomeModal } from './WelcomeModal' import { RunData } from '@opentrons/api-client' import { ServerInitializing } from '../../organisms/OnDeviceDisplay/RobotDashboard/ServerInitializing' -import { useNotifyAllRunsQuery } from '../../resources/runs/useNotifyAllRunsQuery' +import { useNotifyAllRunsQuery } from '../../resources/runs' export const MAXIMUM_RECENT_RUN_PROTOCOLS = 8 diff --git a/app/src/pages/RobotSettingsDashboard/RobotSettingButton.tsx b/app/src/pages/RobotSettingsDashboard/RobotSettingButton.tsx index d1207ec1356..6f431f7aa14 100644 --- a/app/src/pages/RobotSettingsDashboard/RobotSettingButton.tsx +++ b/app/src/pages/RobotSettingsDashboard/RobotSettingButton.tsx @@ -27,7 +27,7 @@ const SETTING_BUTTON_STYLE = css` margin-bottom: ${SPACING.spacing8}; background-color: ${COLORS.grey35}; padding: ${SPACING.spacing20} ${SPACING.spacing24}; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; &:active { background-color: ${COLORS.grey50}; diff --git a/app/src/pages/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/RobotSettingsDashboard/RobotSettingsList.tsx index 335dd890690..34bd5084e00 100644 --- a/app/src/pages/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/RobotSettingsDashboard/RobotSettingsList.tsx @@ -207,7 +207,7 @@ function FeatureFlags(): JSX.Element { marginBottom={SPACING.spacing8} backgroundColor={COLORS.grey35} padding={`${SPACING.spacing20} ${SPACING.spacing24}`} - borderRadius={BORDERS.borderRadiusSize4} + borderRadius={BORDERS.borderRadius16} display={DISPLAY_FLEX} flexDirection={DIRECTION_ROW} gridGap={SPACING.spacing24} @@ -251,7 +251,7 @@ export function OnOffToggle(props: { isOn: boolean }): JSX.Element { alignItems={ALIGN_CENTER} backgroundColor={COLORS.transparent} padding={`${SPACING.spacing12} ${SPACING.spacing4}`} - borderRadius={BORDERS.borderRadiusSize4} + borderRadius={BORDERS.borderRadius16} > {props.isOn ? t('on') : t('off')} diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/RunSummary/index.tsx index 7b455663964..efb40315409 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/RunSummary/index.tsx @@ -9,6 +9,7 @@ import { ALIGN_FLEX_START, ALIGN_STRETCH, Btn, + BORDERS, COLORS, DIRECTION_COLUMN, DIRECTION_ROW, @@ -59,12 +60,11 @@ import { } from '../../redux/analytics' import { getLocalRobot } from '../../redux/discovery' import { RunFailedModal } from '../../organisms/OnDeviceDisplay/RunningProtocol' -import { formatTimeWithUtcLabel } from '../../resources/runs/utils' +import { formatTimeWithUtcLabel, useNotifyRunQuery } from '../../resources/runs' import { handleTipsAttachedModal } from '../../organisms/DropTipWizard/TipsAttachedModal' import { getPipettesWithTipAttached } from '../../organisms/DropTipWizard/getPipettesWithTipAttached' import { getPipetteModelSpecs, FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { useMostRecentRunId } from '../../organisms/ProtocolUpload/hooks/useMostRecentRunId' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' import type { OnDeviceRouteParams } from '../../App/types' import type { PipetteModelSpecs } from '@opentrons/shared-data' @@ -391,6 +391,7 @@ const SplashFrame = styled(Flex)` justify-content: ${JUSTIFY_CENTER}; align-items: ${ALIGN_CENTER}; grid-gap: ${SPACING.spacing40}; + border-radius: ${BORDERS.borderRadius8}; ` const ProtocolName = styled.h4` diff --git a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx index be0b16f591b..32f87a8047c 100644 --- a/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx +++ b/app/src/pages/RunningProtocol/__tests__/RunningProtocol.test.tsx @@ -32,8 +32,10 @@ import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks' import { useMostRecentCompletedAnalysis } from '../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' import { OpenDoorAlertModal } from '../../../organisms/OpenDoorAlertModal' import { RunningProtocol } from '..' -import { useNotifyLastRunCommandKey } from '../../../resources/runs/useNotifyLastRunCommandKey' -import { useNotifyRunQuery } from '../../../resources/runs/useNotifyRunQuery' +import { + useNotifyLastRunCommandKey, + useNotifyRunQuery, +} from '../../../resources/runs' import type { UseQueryResult } from 'react-query' import type { ProtocolAnalyses } from '@opentrons/api-client' @@ -50,9 +52,7 @@ vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol') vi.mock('../../../redux/discovery') vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol/CancelingRunModal') vi.mock('../../../organisms/OpenDoorAlertModal') -vi.mock('../../../resources/runs/useNotifyLastRunCommandKey') -vi.mock('../../../resources/runs/useNotifyRunQuery') - +vi.mock('../../../resources/runs') const RUN_ID = 'run_id' const ROBOT_NAME = 'otie' const PROTOCOL_ID = 'protocol_id' diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index a702b7bf881..2fc56806679 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -29,7 +29,10 @@ import { import { StepMeter } from '../../atoms/StepMeter' import { useMostRecentCompletedAnalysis } from '../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis' -import { useNotifyLastRunCommandKey } from '../../resources/runs/useNotifyLastRunCommandKey' +import { + useNotifyLastRunCommandKey, + useNotifyRunQuery, +} from '../../resources/runs' import { InterventionModal } from '../../organisms/InterventionModal' import { isInterventionCommand } from '../../organisms/InterventionModal/utils' import { @@ -50,7 +53,6 @@ import { CancelingRunModal } from '../../organisms/OnDeviceDisplay/RunningProtoc import { ConfirmCancelRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal' import { getLocalRobot } from '../../redux/discovery' import { OpenDoorAlertModal } from '../../organisms/OpenDoorAlertModal' -import { useNotifyRunQuery } from '../../resources/runs/useNotifyRunQuery' import type { OnDeviceRouteParams } from '../../App/types' diff --git a/app/src/redux/config/constants.ts b/app/src/redux/config/constants.ts index 34e8c943d8a..cf9adc32921 100644 --- a/app/src/redux/config/constants.ts +++ b/app/src/redux/config/constants.ts @@ -1,6 +1,10 @@ import type { DevInternalFlag } from './types' -export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = ['protocolStats'] +export const DEV_INTERNAL_FLAGS: DevInternalFlag[] = [ + 'protocolStats', + 'enableRunTimeParameters', + 'enableRunNotes', +] // action type constants export const INITIALIZED: 'config:INITIALIZED' = 'config:INITIALIZED' diff --git a/app/src/redux/config/schema-types.ts b/app/src/redux/config/schema-types.ts index dea58c435d2..dcdaff5748a 100644 --- a/app/src/redux/config/schema-types.ts +++ b/app/src/redux/config/schema-types.ts @@ -7,7 +7,10 @@ export type UpdateChannel = 'latest' | 'beta' | 'alpha' export type DiscoveryCandidates = string[] -export type DevInternalFlag = 'protocolStats' +export type DevInternalFlag = + | 'protocolStats' + | 'enableRunTimeParameters' + | 'enableRunNotes' export type FeatureFlags = Partial> diff --git a/app/src/redux/shell/types.ts b/app/src/redux/shell/types.ts index 379d22bd892..6502f92c439 100644 --- a/app/src/redux/shell/types.ts +++ b/app/src/redux/shell/types.ts @@ -19,13 +19,17 @@ export interface Remote { } } -interface NotifyRefetchData { +export interface NotifyRefetchData { refetchUsingHTTP: boolean - statusCode: never } +export interface NotifyUnsubscribeData { + unsubscribe: boolean +} + +export type NotifyBrokerResponses = NotifyRefetchData | NotifyUnsubscribeData export type NotifyNetworkError = 'ECONNFAILED' | 'ECONNREFUSED' -export type NotifyResponseData = NotifyRefetchData | NotifyNetworkError +export type NotifyResponseData = NotifyBrokerResponses | NotifyNetworkError interface File { sha512: string diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index d1ad8951421..0b4ce2fd1b0 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -171,4 +171,21 @@ describe('useNotifyService', () => { rerender() expect(mockHTTPRefetch).toHaveBeenCalledWith('once') }) + + it('should trigger a single HTTP refetch if the unsubscribe flag was returned', () => { + vi.mocked(appShellListener).mockImplementation( + (_: any, __: any, mockCb: any) => { + mockCb({ unsubscribe: true }) + } + ) + const { rerender } = renderHook(() => + useNotifyService({ + topic: MOCK_TOPIC, + setRefetchUsingHTTP: mockHTTPRefetch, + options: MOCK_OPTIONS, + } as any) + ) + rerender() + expect(mockHTTPRefetch).toHaveBeenCalledWith('once') + }) }) diff --git a/app/src/resources/maintenance_runs/index.ts b/app/src/resources/maintenance_runs/index.ts new file mode 100644 index 00000000000..ecd7a95a94d --- /dev/null +++ b/app/src/resources/maintenance_runs/index.ts @@ -0,0 +1 @@ +export * from './useNotifyCurrentMaintenanceRun' diff --git a/app/src/resources/runs/index.ts b/app/src/resources/runs/index.ts new file mode 100644 index 00000000000..be5fabb4970 --- /dev/null +++ b/app/src/resources/runs/index.ts @@ -0,0 +1,5 @@ +export * from './hooks' +export * from './utils' +export * from './useNotifyAllRunsQuery' +export * from './useNotifyRunQuery' +export * from './useNotifyLastRunCommandKey' diff --git a/app/src/resources/useNotifyService.ts b/app/src/resources/useNotifyService.ts index 3accf0b8082..8fcfd852575 100644 --- a/app/src/resources/useNotifyService.ts +++ b/app/src/resources/useNotifyService.ts @@ -77,7 +77,7 @@ export function useNotifyService({ properties: {}, }) } - } else if ('refetchUsingHTTP' in data) { + } else if ('refetchUsingHTTP' in data || 'unsubscribe' in data) { setRefetchUsingHTTP('once') } } diff --git a/components/src/atoms/CheckboxField/index.tsx b/components/src/atoms/CheckboxField/index.tsx index 00cb643f9e7..6cf761e38dc 100644 --- a/components/src/atoms/CheckboxField/index.tsx +++ b/components/src/atoms/CheckboxField/index.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { css } from 'styled-components' import { SPACING, TYPOGRAPHY } from '../../ui-style-constants' -import { COLORS } from '../../helix-design-system' +import { COLORS, BORDERS } from '../../helix-design-system' import { Flex, Box } from '../../primitives' import { Icon } from '../../icons' import { ALIGN_CENTER, JUSTIFY_CENTER, SIZE_1 } from '../../styles' @@ -128,7 +128,7 @@ export function CheckboxField(props: CheckboxFieldProps): JSX.Element { diff --git a/components/src/atoms/buttons/AlertPrimaryButton.tsx b/components/src/atoms/buttons/AlertPrimaryButton.tsx index a31ce1ff021..9f097a7c339 100644 --- a/components/src/atoms/buttons/AlertPrimaryButton.tsx +++ b/components/src/atoms/buttons/AlertPrimaryButton.tsx @@ -1,11 +1,11 @@ import styled from 'styled-components' -import { BORDERS, TYPOGRAPHY, SPACING } from '../../ui-style-constants' -import { COLORS } from '../../helix-design-system' +import { TYPOGRAPHY, SPACING } from '../../ui-style-constants' +import { BORDERS, COLORS } from '../../helix-design-system' import { NewAlertPrimaryBtn, styleProps } from '../../primitives' export const AlertPrimaryButton = styled(NewAlertPrimaryBtn)` background-color: ${COLORS.red50}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; padding-left: ${SPACING.spacing16}; padding-right: ${SPACING.spacing16}; text-transform: ${TYPOGRAPHY.textTransformNone}; diff --git a/components/src/atoms/buttons/PrimaryButton.tsx b/components/src/atoms/buttons/PrimaryButton.tsx index 8005b021930..5e5c3544a1e 100644 --- a/components/src/atoms/buttons/PrimaryButton.tsx +++ b/components/src/atoms/buttons/PrimaryButton.tsx @@ -1,11 +1,11 @@ import styled from 'styled-components' -import { BORDERS, TYPOGRAPHY, SPACING } from '../../ui-style-constants' -import { COLORS } from '../../helix-design-system' +import { TYPOGRAPHY, SPACING } from '../../ui-style-constants' +import { BORDERS, COLORS } from '../../helix-design-system' import { NewPrimaryBtn, styleProps } from '../../primitives' export const PrimaryButton = styled(NewPrimaryBtn)` background-color: ${COLORS.blue50}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; box-shadow: none; padding-left: ${SPACING.spacing16}; padding-right: ${SPACING.spacing16}; diff --git a/components/src/atoms/buttons/SecondaryButton.tsx b/components/src/atoms/buttons/SecondaryButton.tsx index 00e456ba100..4746e1f6932 100644 --- a/components/src/atoms/buttons/SecondaryButton.tsx +++ b/components/src/atoms/buttons/SecondaryButton.tsx @@ -1,7 +1,9 @@ import styled from 'styled-components' -import { BORDERS, TYPOGRAPHY, SPACING } from '../../ui-style-constants' + +import { TYPOGRAPHY, SPACING } from '../../ui-style-constants' import { isntStyleProp, styleProps } from '../../primitives' -import { COLORS } from '../../helix-design-system' +import { BORDERS, COLORS } from '../../helix-design-system' + import type { StyleProps } from '../../index' interface SecondaryButtonProps extends StyleProps { @@ -16,7 +18,7 @@ export const SecondaryButton = styled.button.withConfig({ color: ${props => (props.isDangerous ? COLORS.red50 : COLORS.blue50)}; border: ${BORDERS.lineBorder}; border-color: ${props => (props.isDangerous ? COLORS.red50 : 'initial')}; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8} ${SPACING.spacing16}; text-transform: ${TYPOGRAPHY.textTransformNone}; background-color: ${COLORS.transparent}; diff --git a/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx b/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx index 4725f5d96ff..3a56b84d0c9 100644 --- a/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/AlertPrimaryButton.test.tsx @@ -3,8 +3,8 @@ import { describe, it, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../testing/utils' -import { COLORS } from '../../../helix-design-system' -import { BORDERS, TYPOGRAPHY, SPACING } from '../../../ui-style-constants' +import { BORDERS, COLORS } from '../../../helix-design-system' +import { TYPOGRAPHY, SPACING } from '../../../ui-style-constants' import { AlertPrimaryButton } from '../AlertPrimaryButton' @@ -31,7 +31,7 @@ describe('AlertPrimaryButton', () => { expect(button).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSizeP}`) expect(button).toHaveStyle(`font-weight: ${TYPOGRAPHY.fontWeightSemiBold}`) expect(button).toHaveStyle(`line-height: ${TYPOGRAPHY.lineHeight20}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.radiusSoftCorners}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) expect(button).toHaveStyle( `text-transform: ${TYPOGRAPHY.textTransformNone}` ) diff --git a/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx b/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx index 3dc166514ba..4ec8c16357a 100644 --- a/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/PrimaryButton.test.tsx @@ -3,8 +3,8 @@ import { describe, it, beforeEach, expect } from 'vitest' import { fireEvent, screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../testing/utils' -import { COLORS } from '../../../helix-design-system' -import { BORDERS, TYPOGRAPHY, SPACING } from '../../../ui-style-constants' +import { BORDERS, COLORS } from '../../../helix-design-system' +import { TYPOGRAPHY, SPACING } from '../../../ui-style-constants' import { PrimaryButton } from '../PrimaryButton' const render = (props: React.ComponentProps) => { @@ -30,7 +30,7 @@ describe('PrimaryButton', () => { expect(button).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSizeP}`) expect(button).toHaveStyle(`font-weight: ${TYPOGRAPHY.fontWeightSemiBold}`) expect(button).toHaveStyle(`line-height: ${TYPOGRAPHY.lineHeight20}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.radiusSoftCorners}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) expect(button).toHaveStyle( `text-transform: ${TYPOGRAPHY.textTransformNone}` ) diff --git a/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx b/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx index 9d8bbaf35d1..c2f1df7f388 100644 --- a/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx +++ b/components/src/atoms/buttons/__tests__/SecondaryButton.test.tsx @@ -3,8 +3,8 @@ import { describe, it, beforeEach, expect } from 'vitest' import { screen } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../testing/utils' -import { BORDERS, TYPOGRAPHY, SPACING } from '../../../ui-style-constants' -import { COLORS } from '../../../helix-design-system' +import { TYPOGRAPHY, SPACING } from '../../../ui-style-constants' +import { BORDERS, COLORS } from '../../../helix-design-system' import { SecondaryButton } from '../SecondaryButton' @@ -31,7 +31,7 @@ describe('SecondaryButton', () => { expect(button).toHaveStyle(`font-size: ${TYPOGRAPHY.fontSizeP}`) expect(button).toHaveStyle(`font-weight: ${TYPOGRAPHY.fontWeightSemiBold}`) expect(button).toHaveStyle(`line-height: ${TYPOGRAPHY.lineHeight20}`) - expect(button).toHaveStyle(`border-radius: ${BORDERS.radiusSoftCorners}`) + expect(button).toHaveStyle(`border-radius: ${BORDERS.borderRadius8}`) expect(button).toHaveStyle( `text-transform: ${TYPOGRAPHY.textTransformNone}` ) diff --git a/components/src/hardware-sim/Deck/FlexTrash.tsx b/components/src/hardware-sim/Deck/FlexTrash.tsx index 0be63435543..038e21f857a 100644 --- a/components/src/hardware-sim/Deck/FlexTrash.tsx +++ b/components/src/hardware-sim/Deck/FlexTrash.tsx @@ -8,8 +8,8 @@ import { import { Icon } from '../../icons' import { Flex, Text } from '../../primitives' import { ALIGN_CENTER, JUSTIFY_CENTER } from '../../styles' -import { BORDERS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' -import { COLORS } from '../../helix-design-system' +import { SPACING, TYPOGRAPHY } from '../../ui-style-constants' +import { COLORS, BORDERS } from '../../helix-design-system' import { RobotCoordsForeignObject } from './RobotCoordsForeignObject' import type { RobotType } from '@opentrons/shared-data' @@ -94,7 +94,7 @@ export const FlexTrash = ({ > = args => { width="8.75rem" flexDirection={DIRECTION_COLUMN} alignItems={ALIGN_CENTER} - borderRadius={BORDERS.borderRadiusSize3} + borderRadius={BORDERS.borderRadius12} marginRight={SPACING.spacing8} marginBottom={SPACING.spacing8} padding={SPACING.spacing16} diff --git a/components/src/index.ts b/components/src/index.ts index 99867c4c03e..6e38096c4a5 100644 --- a/components/src/index.ts +++ b/components/src/index.ts @@ -27,8 +27,6 @@ export * from './tooltips' export * from './styles' // new ui-overhaul style vars export * from './ui-style-constants' -// helix design system -export * from './helix-design-system' // helix design system export * from './helix-design-system' diff --git a/components/src/modals/ModalShell.tsx b/components/src/modals/ModalShell.tsx index 62bb638e4d1..4990ef47ce8 100644 --- a/components/src/modals/ModalShell.tsx +++ b/components/src/modals/ModalShell.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import styled from 'styled-components' -import { BORDERS, SPACING } from '../ui-style-constants' -import { COLORS } from '../helix-design-system' +import { SPACING } from '../ui-style-constants' +import { BORDERS, COLORS } from '../helix-design-system' import { StyleProps, styleProps } from '../primitives' import { POSITION_FIXED, @@ -104,7 +104,7 @@ const ModalArea = styled.div< overflow-y: ${OVERFLOW_AUTO}; max-height: 100%; width: 100%; - border-radius: ${BORDERS.radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius8}; box-shadow: ${BORDERS.smallDropShadow}; height: ${({ isFullPage }) => (isFullPage ? '100%' : 'auto')}; background-color: ${COLORS.white}; diff --git a/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx b/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx index 960a21f61ea..9521bbbf8ea 100644 --- a/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx +++ b/components/src/molecules/LocationIcon/__tests__/LocationIcon.test.tsx @@ -2,8 +2,8 @@ import * as React from 'react' import { describe, it, beforeEach, expect } from 'vitest' import { renderWithProviders } from '../../../testing/utils' import { screen } from '@testing-library/react' -import { BORDERS, SPACING } from '../../../ui-style-constants' -import { COLORS } from '../../../helix-design-system' +import { SPACING } from '../../../ui-style-constants' +import { BORDERS, COLORS } from '../../../helix-design-system' import { LocationIcon } from '..' @@ -27,9 +27,7 @@ describe('LocationIcon', () => { expect(locationIcon).toHaveStyle('height: 2rem') expect(locationIcon).toHaveStyle('width: max-content') expect(locationIcon).toHaveStyle(`border: 2px solid ${COLORS.black90}`) - expect(locationIcon).toHaveStyle( - `border-radius: ${BORDERS.borderRadiusSize3}` - ) + expect(locationIcon).toHaveStyle(`border-radius: ${BORDERS.borderRadius12}`) }) it('should render slot name', () => { diff --git a/components/src/molecules/LocationIcon/index.tsx b/components/src/molecules/LocationIcon/index.tsx index f2279600fe2..1916692a474 100644 --- a/components/src/molecules/LocationIcon/index.tsx +++ b/components/src/molecules/LocationIcon/index.tsx @@ -4,8 +4,8 @@ import { css } from 'styled-components' import { Icon } from '../../icons' import { Flex, Text } from '../../primitives' import { ALIGN_CENTER } from '../../styles' -import { BORDERS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' -import { COLORS } from '../../helix-design-system' +import { SPACING, TYPOGRAPHY } from '../../ui-style-constants' +import { BORDERS, COLORS } from '../../helix-design-system' import type { IconName } from '../../icons' import type { StyleProps } from '../../primitives' @@ -33,7 +33,7 @@ const LOCATION_ICON_STYLE = css<{ }>` align-items: ${ALIGN_CENTER}; border: 2px solid ${props => props.color ?? COLORS.black90}; - border-radius: ${BORDERS.borderRadiusSize3}; + border-radius: ${BORDERS.borderRadius12}; height: ${props => props.height ?? SPACING.spacing32}; width: ${props => props.width ?? 'max-content'}; padding: ${SPACING.spacing4} diff --git a/components/src/molecules/RoundTab.tsx b/components/src/molecules/RoundTab.tsx index 71c83e5a390..29dd54e2e16 100644 --- a/components/src/molecules/RoundTab.tsx +++ b/components/src/molecules/RoundTab.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { css } from 'styled-components' -import { TYPOGRAPHY, BORDERS, SPACING } from '../ui-style-constants' -import { COLORS } from '../helix-design-system' +import { TYPOGRAPHY, SPACING } from '../ui-style-constants' +import { COLORS, BORDERS } from '../helix-design-system' import { POSITION_RELATIVE } from '../styles' import { Btn } from '../primitives' @@ -10,7 +10,7 @@ const defaultTabStyle = css` color: ${COLORS.black90}; background-color: ${COLORS.purple30}; border: 0px ${BORDERS.styleSolid} ${COLORS.purple30}; - border-radius: ${BORDERS.borderRadiusSize2}; + border-radius: ${BORDERS.borderRadius8}; padding: ${SPACING.spacing8} ${SPACING.spacing16}; position: ${POSITION_RELATIVE}; diff --git a/components/src/tooltips/Tooltip.tsx b/components/src/tooltips/Tooltip.tsx index cea7e35ca20..70617d6dcfa 100644 --- a/components/src/tooltips/Tooltip.tsx +++ b/components/src/tooltips/Tooltip.tsx @@ -1,12 +1,11 @@ import * as React from 'react' import { css } from 'styled-components' -import { radiusSoftCorners } from '../ui-style-constants/borders' +import { BORDERS, COLORS } from '../helix-design-system' import { fontSizeH4 } from '../ui-style-constants/typography' import { spacing8 } from '../ui-style-constants/spacing' import { ARROW_SIZE_PX } from './styles' import { Box } from '../primitives' -import { COLORS } from '../helix-design-system' import type { CSSProperties } from 'react' import type { FlattenSimpleInterpolation } from 'styled-components' @@ -63,7 +62,7 @@ export const Tooltip = React.forwardRef(function TooltipComponent( filter: drop-shadow(0px 1px 3px rgba(0, 0, 0, 0.2)); cursor: pointer; font-size: ${fontSize}; - border-radius: ${radiusSoftCorners}; + border-radius: ${BORDERS.borderRadius4}; ` return visible ? ( diff --git a/components/src/ui-style-constants/borders.ts b/components/src/ui-style-constants/borders.ts deleted file mode 100644 index b097a6269c0..00000000000 --- a/components/src/ui-style-constants/borders.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { css } from 'styled-components' -import { COLORS } from '../helix-design-system' - -export const radiusSoftCorners = '4px' -export const radiusRoundEdge = '20px' -export const styleSolid = 'solid' - -// touch screen -export const borderRadiusSize1 = radiusSoftCorners -export const borderRadiusSize2 = '8px' -export const borderRadiusSize3 = '12px' -export const borderRadiusSize4 = '16px' -export const borderRadiusSize5 = '40px' -export const borderRadiusSize6 = '60px' - -export const tabBorder = css` - border-bottom-style: ${styleSolid}; - border-bottom-width: 2px; - border-bottom-color: ${COLORS.purple50}; -` - -export const activeLineBorder = `1px ${styleSolid} ${COLORS.blue50}` -export const lineBorder = `1px ${styleSolid} ${COLORS.grey30}` -export const transparentLineBorder = `1px ${styleSolid} ${COLORS.transparent}` -export const cardOutlineBorder = css` - border-style: ${styleSolid}; - border-width: 1px; - border-color: ${COLORS.grey30}; - border-radius: ${radiusSoftCorners}; - &:hover { - border-color: ${COLORS.grey55}; - } -` - -export const bigDropShadow = '0 3px 6px rgba(255, 0, 0, 1)' -export const smallDropShadow = '0px 3px 6px rgba(0, 0, 0, 0.23)' - -// touch screen -export const shadowBig = '0px 3px 6px rgba(0,0,0,0.23)' -export const shadowSmall = '0px 0px 40px rgba(0,0,0,0.4)' diff --git a/components/src/ui-style-constants/index.ts b/components/src/ui-style-constants/index.ts index b33b88ba25e..21a599f031c 100644 --- a/components/src/ui-style-constants/index.ts +++ b/components/src/ui-style-constants/index.ts @@ -1,4 +1,3 @@ -export * as BORDERS from './borders' export * as RESPONSIVENESS from './responsiveness' export * as TYPOGRAPHY from './typography' export * as SPACING from './spacing' diff --git a/hardware-testing/hardware_testing/abr_tools/abr_read_logs.py b/hardware-testing/hardware_testing/abr_tools/abr_read_logs.py index 2da0ed088d8..9f8958d1469 100644 --- a/hardware-testing/hardware_testing/abr_tools/abr_read_logs.py +++ b/hardware-testing/hardware_testing/abr_tools/abr_read_logs.py @@ -68,9 +68,10 @@ def get_error_info(file_results: Dict[str, Any]) -> Tuple[int, str, str, str, st return num_of_errors, error_type, error_code, error_instrument, error_level -def create_abr_data_sheet(storage_directory: str) -> None: +def create_abr_data_sheet(storage_directory: str, file_name: str) -> str: """Creates csv file to log ABR data.""" - sheet_location = os.path.join(storage_directory, "ABR-run-data.csv") + file_name_csv = file_name + ".csv" + sheet_location = os.path.join(storage_directory, file_name_csv) if os.path.exists(sheet_location): print(f"File {sheet_location} located. Not overwriting.") else: @@ -100,6 +101,7 @@ def create_abr_data_sheet(storage_directory: str) -> None: writer = csv.DictWriter(csvfile, fieldnames=headers) writer.writeheader() print(f"Created file. Located: {sheet_location}.") + return file_name_csv def create_data_dictionary( @@ -181,9 +183,9 @@ def create_data_dictionary( return runs_and_robots -def read_abr_data_sheet(storage_directory: str) -> Set[str]: +def read_abr_data_sheet(storage_directory: str, file_name_csv: str) -> Set[str]: """Reads current run sheet to determine what new run data should be added.""" - sheet_location = os.path.join(storage_directory, "ABR-run-data.csv") + sheet_location = os.path.join(storage_directory, file_name_csv) runs_in_sheet = set() # Read the CSV file with open(sheet_location, "r") as csv_start: @@ -201,10 +203,12 @@ def read_abr_data_sheet(storage_directory: str) -> Set[str]: def write_to_abr_sheet( - runs_and_robots: Dict[Any, Dict[str, Any]], storage_directory: str + runs_and_robots: Dict[Any, Dict[str, Any]], + storage_directory: str, + file_name_csv: str, ) -> None: """Write dict of data to abr csv.""" - sheet_location = os.path.join(storage_directory, "ABR-run-data.csv") + sheet_location = os.path.join(storage_directory, file_name_csv) list_of_runs = list(runs_and_robots.keys()) with open(sheet_location, "a", newline="") as f: writer = csv.writer(f) @@ -226,25 +230,44 @@ def write_to_abr_sheet( nargs=1, help="Path to long term storage directory for run logs.", ) + parser.add_argument( + "file_name", + metavar="FILE_NAME", + type=str, + nargs=1, + help="Name of google sheet and local csv to save data to.", + ) + parser.add_argument( + "google_sheet_tab_number", + metavar="GOOGLE_SHEET_TAB_NUMBER", + type=int, + nargs=1, + help="Google sheet tab number.", + ) args = parser.parse_args() storage_directory = args.storage_directory[0] + file_name = args.file_name[0] + tab_number = args.google_sheet_tab_number[0] try: sys.path.insert(0, storage_directory) import google_sheets_tool # type: ignore[import] - credentials_path = os.path.join(storage_directory, "abr.json") + credentials_path = os.path.join(storage_directory, "credentials.json") except ImportError: - raise ImportError("Make sure google_sheets_tool.py is in storage directory.") + raise ImportError( + "Check for google_sheets_tool.py and credentials.json in storage directory." + ) try: google_sheet = google_sheets_tool.google_sheet( - credentials_path, "ABR Run Data", tab_number=0 + credentials_path, file_name, tab_number=tab_number ) print("Connected to google sheet.") except FileNotFoundError: print("No google sheets credentials. Add credentials to storage notebook.") + runs_from_storage = get_run_ids_from_storage(storage_directory) - create_abr_data_sheet(storage_directory) - runs_in_sheet = read_abr_data_sheet(storage_directory) + file_name_csv = create_abr_data_sheet(storage_directory, file_name) + runs_in_sheet = read_abr_data_sheet(storage_directory, file_name_csv) runs_to_save = get_unseen_run_ids(runs_from_storage, runs_in_sheet) runs_and_robots = create_data_dictionary(runs_to_save, storage_directory) - write_to_abr_sheet(runs_and_robots, storage_directory) + write_to_abr_sheet(runs_and_robots, storage_directory, file_name_csv) diff --git a/hardware-testing/hardware_testing/abr_tools/abr_robots.py b/hardware-testing/hardware_testing/abr_tools/abr_robots.py deleted file mode 100644 index a1a93768f9e..00000000000 --- a/hardware-testing/hardware_testing/abr_tools/abr_robots.py +++ /dev/null @@ -1,15 +0,0 @@ -"""ABR Robot IPs.""" - -ABR_IPS = [ - "10.14.12.159", - "10.14.12.161", - "10.14.12.126", - "10.14.12.112", - "10.14.12.124", - "10.14.12.163", - "10.14.12.162", - "10.14.12.165", - "10.14.12.164", - "10.14.12.168", - "10.14.12.167", -] diff --git a/hardware-testing/hardware_testing/abr_tools/abr_run_logs.py b/hardware-testing/hardware_testing/abr_tools/abr_run_logs.py index 0e802ef2d12..c73df9e20bd 100644 --- a/hardware-testing/hardware_testing/abr_tools/abr_run_logs.py +++ b/hardware-testing/hardware_testing/abr_tools/abr_run_logs.py @@ -1,5 +1,4 @@ """ABR Run Log Pull.""" -from .abr_robots import ABR_IPS from typing import Set, Dict, Any import argparse import os @@ -112,8 +111,13 @@ def get_all_run_logs(storage_directory: str) -> None: Read each robot's list of unique run log IDs and compare them to all IDs in storage. Any ID that is not in storage, download the run log and put it in storage. """ + ip_json_file = os.path.join(storage_directory, "IPs.json") + ip_file = json.load(open(ip_json_file)) + ip_address_list = ip_file["ip_address_list"] + print(ip_address_list) + runs_from_storage = get_run_ids_from_storage(storage_directory) - for ip in ABR_IPS: + for ip in ip_address_list: try: runs = get_run_ids_from_robot(ip) runs_to_save = get_unseen_run_ids(runs, runs_from_storage) diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index f1d694cf105..350741ebc79 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -92,8 +92,9 @@ def BuildAsairSensor(simulate: bool, autosearch: bool = True) -> AsairSensorBase ui.print_info(f"Trying to connect to env sensor on port {port}") sensor = AsairSensor.connect(port) ser_id = sensor.get_serial() - ui.print_info(f"Found env sensor {ser_id} on port {port}") - return sensor + if len(ser_id) != 0: + ui.print_info(f"Found env sensor {ser_id} on port {port}") + return sensor except: # noqa: E722 pass use_sim = ui.get_user_answer("No env sensor found, use simulator?") diff --git a/hardware-testing/hardware_testing/gravimetric/tips.py b/hardware-testing/hardware_testing/gravimetric/tips.py index 520a959cd77..8edf66a5797 100644 --- a/hardware-testing/hardware_testing/gravimetric/tips.py +++ b/hardware-testing/hardware_testing/gravimetric/tips.py @@ -1,7 +1,12 @@ """Multi-Channel Tips.""" from typing import List, Dict -from opentrons.protocol_api import ProtocolContext, Well, Labware, InstrumentContext +from opentrons.protocol_api import ( + ProtocolContext, + Well, + Labware, + InstrumentContext, +) # Rows by Channel: # - Rear Racks (slot-row=C) @@ -100,34 +105,47 @@ def _get_racks(ctx: ProtocolContext) -> Dict[int, Labware]: } -def _unused_tips_for_racks(racks: List[Labware]) -> List[Well]: +def _unused_tips_for_racks( + ctx: ProtocolContext, pipette_mount: str, racks: List[Labware] +) -> List[Well]: wells: List[Well] = [] rows = "ABCDEFGH" for rack in racks: for col in range(1, 13): for row in rows: wellname = f"{row}{col}" - next_well = rack.next_tip(1, rack[wellname]) + next_well = rack.next_tip( + 1, + rack[wellname], + ) if next_well is not None and wellname == next_well.well_name: wells.append(rack[wellname]) return wells -def get_unused_tips(ctx: ProtocolContext, tip_volume: int) -> List[Well]: +def get_unused_tips( + ctx: ProtocolContext, tip_volume: int, pipette_mount: str +) -> List[Well]: """Use the labware's tip tracker to get a list of all unused tips for a given tip volume.""" racks = [ r for r in _get_racks(ctx).values() if r.wells()[0].max_volume == tip_volume ] - return _unused_tips_for_racks(racks) + return _unused_tips_for_racks(ctx, pipette_mount, racks) -def get_tips_for_single(ctx: ProtocolContext, tip_volume: int) -> List[Well]: +def get_tips_for_single( + ctx: ProtocolContext, tip_volume: int, pipette_mount: str +) -> List[Well]: """Get tips for single channel.""" - return get_unused_tips(ctx, tip_volume) + return get_unused_tips(ctx, tip_volume, pipette_mount) def get_tips_for_individual_channel_on_multi( - ctx: ProtocolContext, channel: int, tip_volume: int, pipette_volume: int + ctx: ProtocolContext, + channel: int, + tip_volume: int, + pipette_volume: int, + pipette_mount: str, ) -> List[Well]: """Get tips for a multi's channel.""" print(f"getting {tip_volume} tips for channel {channel}") @@ -140,7 +158,7 @@ def get_tips_for_individual_channel_on_multi( specific_racks: List[Labware] = [] for slot in slots: specific_racks.append(all_racks[slot]) - unused_tips = _unused_tips_for_racks(specific_racks) + unused_tips = _unused_tips_for_racks(ctx, pipette_mount, specific_racks) tips = [ tip for tip in unused_tips @@ -171,14 +189,14 @@ def get_tips( ) -> Dict[int, List[Well]]: """Get tips.""" if pipette.channels == 1: - return {0: get_tips_for_single(ctx, tip_volume)} + return {0: get_tips_for_single(ctx, tip_volume, pipette.mount)} elif pipette.channels == 8: if all_channels: return {0: get_tips_for_all_channels_on_multi(ctx, tip_volume)} else: return { channel: get_tips_for_individual_channel_on_multi( - ctx, channel, tip_volume, int(pipette.max_volume) + ctx, channel, tip_volume, int(pipette.max_volume), pipette.mount ) for channel in range(pipette.channels) } diff --git a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py index 207791f58ab..80d3993e6c5 100644 --- a/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py +++ b/hardware-testing/hardware_testing/production_qc/pipette_assembly_qc_ot3/__main__.py @@ -472,7 +472,6 @@ def _connect_to_fixture(test_config: TestConfig) -> PressureFixtureBase: fixture = connect_to_fixture( test_config.simulate or test_config.skip_fixture, side=test_config.fixture_side ) - fixture.connect() return fixture diff --git a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py index 2e5f13c11f4..7b75ab7a590 100644 --- a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py +++ b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p1000_multi_200ul.py @@ -287,7 +287,11 @@ def _transfer( # transfer if not same_tip: pipette.configure_for_volume(volume) - pipette.pick_up_tip(tips.next_tip(pipette.channels)) + pipette.pick_up_tip( + tips.next_tip( + pipette.channels, + ) + ) if pipette.current_volume > 0: pipette.dispense(pipette.current_volume, reservoir[source].top()) pipette.aspirate(volume, aspirate_pos) diff --git a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py index 43fd03d1f6c..5953ef76c0f 100644 --- a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py +++ b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_multi_1ul.py @@ -296,7 +296,11 @@ def _transfer( # transfer if not same_tip: pipette.configure_for_volume(volume) - pipette.pick_up_tip(tips.next_tip(pipette.channels)) + pipette.pick_up_tip( + tips.next_tip( + pipette.channels, + ) + ) if pipette.current_volume > 0: pipette.dispense(pipette.current_volume, reservoir[source].top()) pipette.aspirate(volume, aspirate_pos) diff --git a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py index 8160d43cb9c..17c76019dd5 100644 --- a/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py +++ b/hardware-testing/hardware_testing/protocols/installation_qualification/flex_iq_p50_single_1ul.py @@ -283,7 +283,11 @@ def _transfer( # transfer if not same_tip: pipette.configure_for_volume(volume) - pipette.pick_up_tip(tips.next_tip(pipette.channels)) + pipette.pick_up_tip( + tips.next_tip( + pipette.channels, + ) + ) if pipette.current_volume > 0: pipette.dispense(pipette.current_volume, reservoir[source].top()) pipette.aspirate(volume, aspirate_pos) diff --git a/hardware/opentrons_hardware/scripts/lld_data_script.py b/hardware/opentrons_hardware/scripts/lld_data_script.py new file mode 100644 index 00000000000..3baa2e4049e --- /dev/null +++ b/hardware/opentrons_hardware/scripts/lld_data_script.py @@ -0,0 +1,441 @@ +"""Script that can process previous real world data to test lld processes.""" +import csv +import os +import argparse +from typing import List, Optional, Tuple, Any, Dict +import matplotlib.pyplot as plot +import numpy +from abc import ABC, abstractmethod + +impossible_pressure = 9001.0 +accepted_error = 0.1 + + +class LLDAlgoABC(ABC): + """An instance of an lld algorithm.""" + + @staticmethod + @abstractmethod + def name() -> str: + """Name of this algorithm.""" + ... + + @abstractmethod + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + ... + + @abstractmethod + def reset(self) -> None: + """Reset simulator between runs.""" + ... + + +class LLDPresThresh(LLDAlgoABC): + """present day threshold based.""" + + threshold: float + + def __init__(self, thresh: float = -150) -> None: + """Init.""" + self.threshold = thresh + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("simple threshold") + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + return (pressure < self.threshold, pressure) + + def reset(self) -> None: + """Reset simulator between runs.""" + pass + + +class LLDSMAT(LLDAlgoABC): + """Simple moving average threshold.""" + + samples_n_smat: int + running_samples_smat: List[float] + threshold_smat: float + + def __init__(self, samples: int = 10, thresh: float = -15) -> None: + """Init.""" + self.samples_n_smat = samples + self.threshold_smat = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("simple moving avg thresh") + + def reset(self) -> None: + """Reset simulator between runs.""" + self.running_samples_smat = [impossible_pressure] * self.samples_n_smat + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + try: + next_ind = self.running_samples_smat.index(impossible_pressure) + # if no exception we're still filling the minimum samples + self.running_samples_smat[next_ind] = pressure + return (False, impossible_pressure) + except ValueError: # the array has been filled + pass + # left shift old samples + for i in range(self.samples_n_smat - 1): + self.running_samples_smat[i] = self.running_samples_smat[i + 1] + self.running_samples_smat[self.samples_n_smat - 1] = pressure + new_running_avg = sum(self.running_samples_smat) / self.samples_n_smat + return ( + new_running_avg < self.threshold_smat, + new_running_avg, + ) + + +class LLDSMAD(LLDAlgoABC): + """Simple moving average derivative.""" + + samples_n_smad: int + running_samples_smad: List[float] + derivative_threshold_smad: float + + def __init__(self, samples: int = 10, thresh: float = -2.5) -> None: + """Init.""" + self.samples_n_smad = samples + self.derivative_threshold_smad = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("simple moving avg der") + + def reset(self) -> None: + """Reset simulator between runs.""" + self.running_samples_smad = [impossible_pressure] * self.samples_n_smad + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + try: + next_ind = self.running_samples_smad.index(impossible_pressure) + # if no exception we're still filling the minimum samples + self.running_samples_smad[next_ind] = pressure + return (False, impossible_pressure) + except ValueError: # the array has been filled + pass + # store old running average + prev_running_avg = sum(self.running_samples_smad) / self.samples_n_smad + # left shift old samples + for i in range(self.samples_n_smad - 1): + self.running_samples_smad[i] = self.running_samples_smad[i + 1] + self.running_samples_smad[self.samples_n_smad - 1] = pressure + new_running_avg = sum(self.running_samples_smad) / self.samples_n_smad + return ( + (new_running_avg - prev_running_avg) < self.derivative_threshold_smad, + new_running_avg, + ) + + +class LLDWMAD(LLDAlgoABC): + """Weighted moving average derivative.""" + + samples_n_wmad: int + weights_wmad: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array( + [0.19, 0.17, 0.15, 0.13, 0.11, 0.09, 0.07, 0.05, 0.03, 0.01] + ) + running_samples_wmad: numpy.ndarray[Any, numpy.dtype[numpy.float32]] + derivative_threshold_wmad: float + + def __init__(self, samples: int = 10, thresh: float = -4) -> None: + """Init.""" + self.samples_n_wmad = samples + self.derivative_threshold_wmad = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("weighted moving avg der") + + def reset(self) -> None: + """Reset simulator between runs.""" + assert numpy.sum(self.weights_wmad) == 1 + self.running_samples_wmad = numpy.full(self.samples_n_wmad, impossible_pressure) + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + if numpy.isin(impossible_pressure, self.running_samples_wmad): + next_ind = numpy.where(self.running_samples_wmad == impossible_pressure)[0][ + 0 + ] + # if no exception we're still filling the minimum samples + self.running_samples_wmad[next_ind] = pressure + return (False, impossible_pressure) + # store old running average + prev_running_avg = numpy.sum( + numpy.multiply(self.running_samples_wmad, self.weights_wmad) + ) + # left shift old samples + for i in range(self.samples_n_wmad - 1): + self.running_samples_wmad[i] = self.running_samples_wmad[i + 1] + self.running_samples_wmad[self.samples_n_wmad - 1] = pressure + new_running_avg = numpy.sum( + numpy.multiply(self.running_samples_wmad, self.weights_wmad) + ) + return ( + (new_running_avg - prev_running_avg) < self.derivative_threshold_wmad, + new_running_avg, + ) + + +class LLDEMAD(LLDAlgoABC): + """Exponential moving average derivative.""" + + current_average_emad: float = impossible_pressure + smoothing_factor: float + derivative_threshold_emad: float + + def __init__(self, s_factor: float = 0.1, thresh: float = -2.5) -> None: + """Init.""" + self.smoothing_factor = s_factor + self.derivative_threshold_emad = thresh + self.reset() + + @staticmethod + def name() -> str: + """Name of this algorithm.""" + return "{:<30}".format("exponential moving avg der") + + def reset(self) -> None: + """Reset simulator between runs.""" + self.current_average_emad = impossible_pressure + + def tick(self, pressure: float) -> Tuple[bool, float]: + """Simulate firmware motor interrupt tick.""" + if self.current_average_emad == impossible_pressure: + self.current_average_emad = pressure + return (False, impossible_pressure) + else: + new_average = (pressure * self.smoothing_factor) + ( + self.current_average_emad * (1 - self.smoothing_factor) + ) + derivative = new_average - self.current_average_emad + self.current_average_emad = new_average + return ( + derivative < self.derivative_threshold_emad, + self.current_average_emad, + ) + + +def _running_avg( + time: List[float], + pressure: List[float], + z_travel: List[float], + p_travel: List[float], + no_plot: bool, + algorithm: LLDAlgoABC, + plot_name: str, +) -> Optional[Tuple[float, float, float]]: + algorithm.reset() + average = float(0) + running_time = [] + running_derivative = [] + running_avg = [] + return_val = None + for i in range(1, len(time)): + prev_avg = average + found, average = algorithm.tick(float(pressure[i])) + if found: + # if average < running_avg_threshold: + # print(f"found z height = {z_travel[i]}") + # print(f"at time = {time[i]}") + return_val = time[i], z_travel[i], p_travel[i] + if no_plot: + # once we find it we don't need to keep going + break + if average != impossible_pressure and prev_avg != impossible_pressure: + running_avg_derivative = average - prev_avg + running_time.append(time[i]) + running_derivative.append(running_avg_derivative) + running_avg.append(average) + + time_array: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array( + running_time + ) + derivative_array: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array( + running_derivative + ) + avg_array: numpy.ndarray[Any, numpy.dtype[numpy.float32]] = numpy.array(running_avg) + + if not no_plot: + plot.figure(plot_name) + avg_ax = plot.subplot(211) + avg_ax.set_title("Running Average") + plot.plot(time_array, avg_array) + der_ax = plot.subplot(212) + der_ax.set_title("Derivative") + plot.plot(time_array, derivative_array) + mng = plot.get_current_fig_manager() + if mng is not None: + mng.resize(*mng.window.maxsize()) # type: ignore[attr-defined] + plot.show() + + return return_val + + +def run( + args: argparse.Namespace, + algorithm: LLDAlgoABC, +) -> List[Tuple[float, List[float], str, str]]: + """Run the test with a given algorithm on all the data.""" + path = args.filepath + "/" + report_files = [ + file for file in os.listdir(args.filepath) if "final_report" in file + ] + final_results: List[Tuple[float, List[float], str, str]] = [] + for report_file in report_files: + with open(path + report_file, "r") as file: + reader = csv.reader(file) + reader_list = list(reader) + + number_of_trials = int(reader_list[34][2]) + + expected_height = reader_list[44][6] + # have a time list for each trial so the list lengths can all be equal + results: List[float] = [] + for trial in range(number_of_trials): + + time = [] + pressure = [] + z_travel = [] + p_travel = [] + for row in range((59 + number_of_trials), len(reader_list)): + current_time = reader_list[row][0] + current_pressure = reader_list[row][3 * trial + 2] + current_z_pos = reader_list[row][3 * trial + 3] + current_p_pos = reader_list[row][3 * trial + 4] + + if any( + [ + data == "" + for data in [current_pressure, current_z_pos, current_p_pos] + ] + ): + break + + time.append(float(current_time)) + pressure.append(float(current_pressure)) + z_travel.append(float(current_z_pos)) + p_travel.append(float(current_p_pos)) + + threshold_data = _running_avg( + time, + pressure, + z_travel, + p_travel, + args.no_plot, + algorithm, + f"{algorithm.name()} trial: {trial+1}", + ) + if threshold_data: + # threshold_time = threshold_data[0] + threshold_z_pos = threshold_data[1] + # threshold_p_pos = threshold_data[2] + # print( + # f"Threshold found at:\n\ttime: {threshold_time}\n\tz distance: {threshold_z_pos}\n\tp distance: {threshold_p_pos}" + # ) + results.append(float(threshold_z_pos)) + else: + print("No threshold found") + print( + f"{algorithm.name()}, expected {expected_height} max {max(results)} min{min(results)}, avg {sum(results)/len(results)}" + ) + final_results.append( + (float(expected_height), results, f"{algorithm.name()}", f"{report_file}") + ) + return final_results + + +def _check_for_failure(expected_height: float, results: List[float]) -> bool: + for result in results: + if abs(expected_height - result) > accepted_error: + return True + return False + + +def _score( + algorithms: List[LLDAlgoABC], analysis: List[Tuple[float, List[float], str, str]] +) -> Dict[str, int]: + algorithm_score: Dict[str, int] = {algo.name(): 0 for algo in algorithms} + a_score = len(analysis) + for a in analysis: + algorithm_score[a[2]] += a_score + a_score -= 2 + return dict(sorted(algorithm_score.items(), key=lambda item: item[1], reverse=True)) + + +def main() -> None: + """Main function.""" + # data starts at row 59 + number of trials + + parser = argparse.ArgumentParser() + parser.add_argument( + "--filepath", + type=str, + help="path to the input file", + default=None, + ) + parser.add_argument("--no-plot", action="store_true") + args = parser.parse_args() + + algorithms: List[LLDAlgoABC] = [ + LLDPresThresh(), + LLDSMAD(), + LLDWMAD(), + LLDEMAD(), + LLDSMAT(), + ] + analysis: List[Tuple[float, List[float], str, str]] = [] + for algorithm in algorithms: + algorithm_results = run(args, algorithm) + analysis.extend(algorithm_results) + print("\n\n") + for result in analysis: + res_string = ( + "FAILURE" if _check_for_failure(result[0], result[1]) else "success" + ) + print(f"Algorithm {result[2]} {res_string}") + + accuracy = sorted( + analysis, key=lambda acc: abs((sum(acc[1]) / len(acc[1])) - acc[0]) + ) + precision = sorted(analysis, key=lambda per: (max(per[1]) - min(per[1]))) + + accuracy_score: Dict[str, int] = _score(algorithms, accuracy) + precision_score: Dict[str, int] = _score(algorithms, precision) + algorithm_score: Dict[str, int] = {algo.name(): 0 for algo in algorithms} + + print("Accuracy Scores") + for a_name in accuracy_score.keys(): + print(f"{a_name} {accuracy_score[a_name]}") + + print("Precision Scores") + for a_name in precision_score.keys(): + print(f"{a_name} {precision_score[a_name]}") + # add the two scores together for final score so we can sort before printing + algorithm_score[a_name] = precision_score[a_name] + accuracy_score[a_name] + + algorithm_score = dict( + sorted(algorithm_score.items(), key=lambda item: item[1], reverse=True) + ) + print("Total Scores") + for a_name in algorithm_score.keys(): + print(f"{a_name} {algorithm_score[a_name]}") + + +if __name__ == "__main__": + main() diff --git a/package.json b/package.json index 5684dfae3b9..67e9f909547 100755 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "protocol-designer", "shared-data", "step-generation", - "webpack-config", "api-client", "react-api-client", "usb-bridge/node-client" @@ -51,7 +50,6 @@ "@testing-library/user-event": "13.5.0", "@types/express": "^4.17.11", "@types/glob": "7.1.3", - "@types/jest": "^26.0.20", "@types/lodash": "^4.14.191", "@types/multer": "^1.4.5", "@types/netmask": "^1.0.30", @@ -68,7 +66,6 @@ "@vitest/coverage-v8": "1.3.0", "ajv": "6.12.3", "aws-sdk": "^2.493.0", - "babel-jest": "^26.6.3", "babel-loader": "^8.2.2", "babel-plugin-styled-components": "2.0.7", "babel-plugin-unassert": "^3.0.1", @@ -89,7 +86,6 @@ "eslint-config-standard-with-typescript": "^43.0.1", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^27.6.3", "eslint-plugin-json": "^3.1.0", "eslint-plugin-n": "^16.6.2", "eslint-plugin-promise": "^4.3.1", @@ -109,7 +105,7 @@ "handlebars-loader": "^1.7.1", "html-webpack-plugin": "^3.2.0", "identity-obj-proxy": "^3.0.0", - "jest": "^26.6.3", + "jsdom": "^16.4.0", "lost": "^8.3.1", "madge": "^3.6.0", "mime": "^2.4.4", diff --git a/protocol-designer/README.md b/protocol-designer/README.md index c7bf1b3983a..ae8b91e2e52 100644 --- a/protocol-designer/README.md +++ b/protocol-designer/README.md @@ -1,10 +1,10 @@ -# Opentrons Protocol Designer Beta +# Opentrons Protocol Designer ## Overview Protocol Designer is a tool for scientists and technicians to create protocols for their [OT-2 personal pipetting robot][ot-2] without having to write any code. It provides visual feedback including liquid tracking and tip tracking to allow users to see exactly what their protocol will do at each step. The protocols are saved to Opentrons JSON Protocol files, which can be uploaded to the Opentrons Desktop App to run on a robot. -Protocol Designer Beta is optimized for [Chrome][chrome] browser. Other browsers are not fully supported. +Protocol Designer is optimized for [Chrome][chrome] browser. Other browsers are not fully supported. ## Build setup for development diff --git a/protocol-designer/cypress.json b/protocol-designer/cypress.json index 44203bdc3da..fa95795bfd6 100644 --- a/protocol-designer/cypress.json +++ b/protocol-designer/cypress.json @@ -2,5 +2,6 @@ "baseUrl": "http://localhost:5173", "video": false, "viewportWidth": 1440, - "viewportHeight": 900 + "viewportHeight": 900, + "pluginsFile": false } diff --git a/protocol-designer/cypress/integration/batchEdit.spec.js b/protocol-designer/cypress/integration/batchEdit.spec.js index 55071aae50a..300983ad9b0 100644 --- a/protocol-designer/cypress/integration/batchEdit.spec.js +++ b/protocol-designer/cypress/integration/batchEdit.spec.js @@ -1,5 +1,3 @@ -import { beforeEach, describe, it } from 'vitest' - describe('Batch Edit Transform', () => { beforeEach(() => { cy.visit('/') diff --git a/protocol-designer/cypress/integration/home.spec.js b/protocol-designer/cypress/integration/home.spec.js index 99e554e0d8f..c2f2bda9f92 100644 --- a/protocol-designer/cypress/integration/home.spec.js +++ b/protocol-designer/cypress/integration/home.spec.js @@ -1,5 +1,3 @@ -import { beforeEach, describe, it } from 'vitest' - describe('The Home Page', () => { beforeEach(() => { cy.visit('/') @@ -7,7 +5,7 @@ describe('The Home Page', () => { }) it('successfully loads', () => { - cy.title().should('equal', 'Opentrons Protocol Designer BETA') + cy.title().should('equal', 'Opentrons Protocol Designer') }) it('has the right charset', () => { diff --git a/protocol-designer/cypress/integration/migrations.spec.js b/protocol-designer/cypress/integration/migrations.spec.js index 0fad10a0a10..6bc1036477a 100644 --- a/protocol-designer/cypress/integration/migrations.spec.js +++ b/protocol-designer/cypress/integration/migrations.spec.js @@ -1,4 +1,3 @@ -import { beforeEach, describe, it } from 'vitest' import 'cypress-file-upload' import cloneDeep from 'lodash/cloneDeep' import { expectDeepEqual } from '@opentrons/shared-data/js/cypressUtils' diff --git a/protocol-designer/cypress/integration/mixSettings.spec.js b/protocol-designer/cypress/integration/mixSettings.spec.js index 67960c5dd94..809c92237b3 100644 --- a/protocol-designer/cypress/integration/mixSettings.spec.js +++ b/protocol-designer/cypress/integration/mixSettings.spec.js @@ -1,4 +1,3 @@ -import { describe, it } from 'vitest' const isMacOSX = Cypress.platform === 'darwin' const invalidInput = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()<>?,-' const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true } @@ -125,7 +124,7 @@ describe('Advanced Settings for Mix Form', () => { cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') @@ -144,7 +143,7 @@ describe('Advanced Settings for Mix Form', () => { it('verify functionality of flowrate in batch edit mix form', () => { // Batch editing the Flowrate value cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') diff --git a/protocol-designer/cypress/integration/settings.spec.js b/protocol-designer/cypress/integration/settings.spec.js index 5e70c779ffd..3f248d79ab0 100644 --- a/protocol-designer/cypress/integration/settings.spec.js +++ b/protocol-designer/cypress/integration/settings.spec.js @@ -1,4 +1,3 @@ -import { describe, it, before } from 'vitest' describe('The Settings Page', () => { const exptlSettingText = 'Disable module placement restrictions' @@ -142,7 +141,7 @@ describe('The Settings Page', () => { cy.contains(exptlSettingText).next().click() cy.get('button').contains('Continue').click() // Leave the settings page - cy.get("button[class*='navbar__tab__']").contains('FILE').click() + cy.get("button[id='NavTab_file']").contains('FILE').click() // Go back to settings cy.openSettingsPage() // The toggle is still on @@ -160,7 +159,7 @@ describe('The Settings Page', () => { cy.contains(exptlSettingText).next().click() cy.get('button').contains('Continue').click() // Leave the settings page - cy.get("button[class*='navbar__tab__']").contains('FILE') + cy.get("button[id='NavTab_file']").contains('FILE') // Go back to settings cy.openSettingsPage() // The toggle is still off diff --git a/protocol-designer/cypress/integration/sidebar.spec.js b/protocol-designer/cypress/integration/sidebar.spec.js index e967c0c7b38..7b71fc67cc2 100644 --- a/protocol-designer/cypress/integration/sidebar.spec.js +++ b/protocol-designer/cypress/integration/sidebar.spec.js @@ -1,5 +1,3 @@ -import { describe, it, beforeEach } from 'vitest' - describe('Desktop Navigation', () => { beforeEach(() => { cy.visit('/') @@ -7,7 +5,7 @@ describe('Desktop Navigation', () => { }) it('contains a working file button', () => { - cy.get("button[class*='navbar__tab__']") + cy.get("button[id='NavTab_file']") .contains('FILE') .parent() .should('have.prop', 'disabled') @@ -15,21 +13,21 @@ describe('Desktop Navigation', () => { }) it('contains a disabled liquids button', () => { - cy.get("button[class*='navbar__tab__']") + cy.get("button[id='NavTab_liquids']") .contains('LIQUIDS') .parent() .should('have.prop', 'disabled') }) it('contains a disabled design button', () => { - cy.get("button[class*='navbar__tab__']") + cy.get("button[id='NavTab_design']") .contains('DESIGN') .parent() .should('have.prop', 'disabled') }) it('contains a help button with external link', () => { - cy.get("a[class*='navbar__tab__']") + cy.get('a') .contains('HELP') .parent() .should('have.prop', 'href') @@ -37,13 +35,11 @@ describe('Desktop Navigation', () => { }) it('contains a settings button', () => { - cy.get("button[class*='navbar__tab__']") - .contains('Settings') - .should('exist') + cy.get('button').contains('Settings').should('exist') }) it('returns to the file controls when the file button is clicked', () => { - cy.get("button[class*='navbar__tab__']").contains('FILE').click() + cy.get("button[id='NavTab_file']").contains('FILE').click() cy.contains('Protocol File') }) }) diff --git a/protocol-designer/cypress/integration/transferSettings.spec.js b/protocol-designer/cypress/integration/transferSettings.spec.js index 4cbb114a47b..a4c831fddd4 100644 --- a/protocol-designer/cypress/integration/transferSettings.spec.js +++ b/protocol-designer/cypress/integration/transferSettings.spec.js @@ -1,5 +1,3 @@ -import { describe, it, before } from 'vitest' - const isMacOSX = Cypress.platform === 'darwin' const batchEditClickOptions = { [isMacOSX ? 'metaKey' : 'ctrlKey']: true } @@ -134,7 +132,7 @@ describe('Advanced Settings for Transfer Form', () => { cy.get('[data-test="StepItem_2"]').click(batchEditClickOptions) cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') @@ -153,7 +151,7 @@ describe('Advanced Settings for Transfer Form', () => { it('verify functionality of flowrate in batch edit transfer', () => { // Batch editing the Flowrate value cy.get('input[name="aspirate_flowRate"]').click({ force: true }) - cy.get('div[class*=FlowRateInput__description]').contains( + cy.contains( 'Our default aspirate speed is optimal for a P1000 Single-Channel GEN2 aspirating liquids with a viscosity similar to water' ) cy.get('input[name="aspirate_flowRate_customFlowRate"]').type('100') diff --git a/protocol-designer/cypress/support/commands.js b/protocol-designer/cypress/support/commands.js index 6de9b96aabc..09543c42330 100644 --- a/protocol-designer/cypress/support/commands.js +++ b/protocol-designer/cypress/support/commands.js @@ -34,14 +34,17 @@ Cypress.Commands.add('closeAnnouncementModal', () => { cy.get('[data-test="ComputingSpinner"]', { timeout: 30000 }).should( 'not.exist' ) - cy.get('button').contains('Got It!').should('be.visible').click() + cy.get('button') + .contains('Got It!') + .should('be.visible') + .click({ force: true }) }) // // File Page Actions // Cypress.Commands.add('openFilePage', () => { - cy.get('button[class*="navbar__tab__"]').contains('FILE').click() + cy.get('button[id="NavTab_file"]').contains('FILE').click() }) // @@ -87,7 +90,7 @@ Cypress.Commands.add( // Design Page Actions // Cypress.Commands.add('openDesignPage', () => { - cy.get('button[class*="navbar__tab__"]').contains('DESIGN').parent().click() + cy.get('button[id="NavTab_design"]').contains('DESIGN').parent().click() }) Cypress.Commands.add('addStep', stepName => { cy.get('button').contains('Add Step').click() @@ -98,7 +101,7 @@ Cypress.Commands.add('addStep', stepName => { // Settings Page Actions // Cypress.Commands.add('openSettingsPage', () => { - cy.get('button[class*="navbar__tab__"]').contains('Settings').click() + cy.get('button').contains('Settings').click() }) // Advance Settings for Transfer Steps diff --git a/protocol-designer/index.html b/protocol-designer/index.html index cfcafbedf22..9fbcfaf5875 100644 --- a/protocol-designer/index.html +++ b/protocol-designer/index.html @@ -7,7 +7,7 @@ - Protocol Designer + Opentrons Protocol Designer
diff --git a/protocol-designer/package.json b/protocol-designer/package.json index 7e8969f5885..564ebdb2fe1 100755 --- a/protocol-designer/package.json +++ b/protocol-designer/package.json @@ -8,8 +8,7 @@ "email": "engineering@opentrons.com" }, "name": "protocol-designer", - "type": "module", - "productName": "Opentrons Protocol Designer BETA", + "productName": "Opentrons Protocol Designer", "private": true, "version": "0.0.0-dev", "description": "Protocol designer app", diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx index 8f4c81491b3..172b5b1129a 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/AdapterControls.tsx @@ -14,7 +14,6 @@ import { moveDeckItem, openAddLabwareModal, } from '../../../labware-ingred/actions' -import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { selectors as labwareDefSelectors } from '../../../labware-defs' import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' import { BlockedSlot } from './BlockedSlot' @@ -54,68 +53,59 @@ export const AdapterControls = ( const customLabwareDefs = useSelector( labwareDefSelectors.getCustomLabwareDefsByURI ) - const activeDeckSetup = useSelector(getDeckSetupForActiveItem) - const labware = activeDeckSetup.labware const ref = React.useRef(null) - const [newSlot, setSlot] = React.useState(null) const dispatch = useDispatch() const adapterName = allLabware.find(labware => labware.id === labwareId)?.def.metadata .displayName ?? '' - const [{ itemType, draggedItem, isOver }, drop] = useDrop({ - accept: DND_TYPES.LABWARE, - canDrop: (item: DroppedItem) => { - const draggedDef = item.labwareOnDeck?.def - assert(draggedDef, 'no labware def of dragged item, expected it on drop') - - if (draggedDef != null) { - const isCustomLabware = getLabwareIsCustom( - customLabwareDefs, - item.labwareOnDeck - ) - return ( - getAdapterLabwareIsAMatch( - labwareId, - allLabware, - draggedDef.parameters.loadName - ) || isCustomLabware + const [{ itemType, draggedItem, isOver }, drop] = useDrop( + () => ({ + accept: DND_TYPES.LABWARE, + canDrop: (item: DroppedItem) => { + const draggedDef = item.labwareOnDeck?.def + console.assert( + draggedDef, + 'no labware def of dragged item, expected it on drop' ) - } - return true - }, - drop: (item: DroppedItem) => { - const droppedLabware = item - if (newSlot != null) { - dispatch(moveDeckItem(newSlot, labwareId)) - } else if (droppedLabware.labwareOnDeck != null) { - const droppedSlot = droppedLabware.labwareOnDeck.slot - dispatch(moveDeckItem(droppedSlot, labwareId)) - } - }, - hover: () => { - if (handleDragHover != null) { - handleDragHover() - } - }, - collect: (monitor: DropTargetMonitor) => ({ - itemType: monitor.getItemType(), - isOver: !!monitor.isOver(), - draggedItem: monitor.getItem() as DroppedItem, - }), - }) - const draggedLabware = Object.values(labware).find( - l => l.id === draggedItem?.labwareOnDeck?.id + if (draggedDef != null) { + const isCustomLabware = getLabwareIsCustom( + customLabwareDefs, + item.labwareOnDeck + ) + return ( + getAdapterLabwareIsAMatch( + labwareId, + allLabware, + draggedDef.parameters.loadName + ) || isCustomLabware + ) + } + return true + }, + drop: (item: DroppedItem) => { + const droppedLabware = item + if (droppedLabware.labwareOnDeck != null) { + const droppedSlot = droppedLabware.labwareOnDeck.slot + dispatch(moveDeckItem(droppedSlot, labwareId)) + } + }, + hover: () => { + if (handleDragHover != null) { + handleDragHover() + } + }, + collect: (monitor: DropTargetMonitor) => ({ + itemType: monitor.getItemType(), + isOver: !!monitor.isOver(), + draggedItem: monitor.getItem() as DroppedItem, + }), + }), + [] ) - React.useEffect(() => { - if (draggedLabware != null) { - setSlot(draggedLabware.slot) - } - }) - if ( selectedTerminalItemId !== START_TERMINAL_ITEM_ID || (itemType !== DND_TYPES.LABWARE && itemType !== null) diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx index 74131a71dbe..d75cefe5b71 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx @@ -7,7 +7,6 @@ import { getLabwareDisplayName } from '@opentrons/shared-data' import { DropTargetMonitor, useDrag, useDrop } from 'react-dnd' import { NameThisLabware } from './NameThisLabware' import { DND_TYPES } from '../../../constants' -import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { deleteContainer, duplicateLabware, @@ -39,10 +38,7 @@ export const EditLabware = (props: Props): JSX.Element | null => { const savedLabware = useSelector(labwareIngredSelectors.getSavedLabware) const dispatch = useDispatch>() const { t } = useTranslation('deck') - const activeDeckSetup = useSelector(getDeckSetupForActiveItem) - const labware = activeDeckSetup.labware const ref = React.useRef(null) - const [newSlot, setSlot] = React.useState(null) const { isTiprack } = labwareOnDeck.def.parameters const hasName = savedLabware[labwareOnDeck.id] @@ -52,119 +48,112 @@ export const EditLabware = (props: Props): JSX.Element | null => { dispatch(openIngredientSelector(labwareOnDeck.id)) } - const [, drag] = useDrag({ - type: DND_TYPES.LABWARE, - item: { labwareOnDeck }, - }) - - const [{ draggedLabware, isOver }, drop] = useDrop(() => ({ - accept: DND_TYPES.LABWARE, - canDrop: (item: DroppedItem) => { - const draggedLabware = item?.labwareOnDeck - const isDifferentSlot = - draggedLabware && draggedLabware.slot !== labwareOnDeck.slot - return isDifferentSlot && !swapBlocked - }, - drop: (item: DroppedItem) => { - const draggedLabware = item?.labwareOnDeck - if (newSlot != null) { - dispatch(moveDeckItem(newSlot, labwareOnDeck.slot)) - } else if (draggedLabware != null) { - dispatch(moveDeckItem(draggedLabware.slot, labwareOnDeck.slot)) - } - }, + const [, drag] = useDrag( + () => ({ + type: DND_TYPES.LABWARE, + item: { labwareOnDeck }, + }), + [labwareOnDeck] + ) - hover: (item: DroppedItem, monitor: DropTargetMonitor) => { - if (monitor.canDrop()) { + const [{ draggedLabware, isOver }, drop] = useDrop( + () => ({ + accept: DND_TYPES.LABWARE, + canDrop: (item: DroppedItem) => { + const draggedLabware = item?.labwareOnDeck + const isDifferentSlot = + draggedLabware && draggedLabware.slot !== labwareOnDeck.slot + return isDifferentSlot && !swapBlocked + }, + drop: (item: DroppedItem) => { + const draggedLabware = item?.labwareOnDeck + if (draggedLabware != null) { + dispatch(moveDeckItem(draggedLabware.slot, labwareOnDeck.slot)) + } + }, + hover: () => { setHoveredLabware(labwareOnDeck) - } - }, - collect: (monitor: DropTargetMonitor) => ({ - isOver: monitor.isOver(), - draggedLabware: monitor.getItem() as DroppedItem, + }, + collect: (monitor: DropTargetMonitor) => ({ + isOver: monitor.isOver(), + draggedLabware: monitor.getItem() as DroppedItem, + }), }), - })) - - const draggedItem = Object.values(labware).find( - l => l.id === draggedLabware?.labwareOnDeck?.id + [labwareOnDeck] ) React.useEffect(() => { - if (draggedItem != null) { - setSlot(draggedItem.slot) - setDraggedLabware(draggedItem) + if (draggedLabware?.labwareOnDeck != null) { + setDraggedLabware(draggedLabware?.labwareOnDeck) } else { setHoveredLabware(null) setDraggedLabware(null) } - }) + }, [draggedLabware]) + + let contents: React.ReactNode | null = null + + const isBeingDragged = + draggedLabware?.labwareOnDeck?.slot === labwareOnDeck.slot if (isYetUnnamed && !isTiprack) { - return ( + contents = ( ) + } else if (swapBlocked) { + contents = null + } else if (draggedLabware != null) { + contents = null } else { - const isBeingDragged = draggedItem?.slot === labwareOnDeck.slot - - let contents: React.ReactNode | null = null - - if (swapBlocked) { - contents = null - } else if (draggedLabware != null) { - contents = null - } else { - contents = ( - <> - {!isTiprack ? ( - - - {t('overlay.edit.name_and_liquids')} - - ) : ( -
- )} - dispatch(duplicateLabware(labwareOnDeck.id))} - > - - {t('overlay.edit.duplicate')} + contents = ( + <> + {!isTiprack ? ( + + + {t('overlay.edit.name_and_liquids')} - { - window.confirm( - `Are you sure you want to permanently delete this ${getLabwareDisplayName( - labwareOnDeck.def - )}?` - ) && dispatch(deleteContainer({ labwareId: labwareOnDeck.id })) - }} - > - - {t('overlay.edit.delete')} - - - ) - } - - drag(drop(ref)) - - const dragResult = ( -
- {contents} -
+ ) : ( +
+ )} + dispatch(duplicateLabware(labwareOnDeck.id))} + > + + {t('overlay.edit.duplicate')} + + { + window.confirm( + `Are you sure you want to permanently delete this ${getLabwareDisplayName( + labwareOnDeck.def + )}?` + ) && dispatch(deleteContainer({ labwareId: labwareOnDeck.id })) + }} + > + + {t('overlay.edit.delete')} + + ) - - return dragResult !== null ? dragResult : null } + + drag(drop(ref)) + + return ( +
+ {contents} +
+ ) } diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx index 48be1799a7f..778902a8b79 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx @@ -46,7 +46,7 @@ const REGULAR_OVERLAY_STYLE = css` display: flex; align-items: ${ALIGN_FLEX_START}; justify-content: ${JUSTIFY_SPACE_AROUND}; - border-radius: ${BORDERS.borderRadiusSize4}; + border-radius: ${BORDERS.borderRadius16}; bottom: 0; font-size: 0.7rem; position: ${POSITION_ABSOLUTE}; diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx index 14a27061cb3..2849506cefb 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx @@ -14,7 +14,6 @@ import { moveDeckItem, openAddLabwareModal, } from '../../../labware-ingred/actions' -import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { selectors as labwareDefSelectors } from '../../../labware-defs' import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' import { BlockedSlot } from './BlockedSlot' @@ -53,10 +52,7 @@ export const SlotControls = (props: SlotControlsProps): JSX.Element | null => { const customLabwareDefs = useSelector( labwareDefSelectors.getCustomLabwareDefsByURI ) - const activeDeckSetup = useSelector(getDeckSetupForActiveItem) - const labware = activeDeckSetup.labware const ref = React.useRef(null) - const [newSlot, setSlot] = React.useState(null) const dispatch = useDispatch() const { t } = useTranslation('deck') @@ -66,54 +62,50 @@ export const SlotControls = (props: SlotControlsProps): JSX.Element | null => { item: { labwareOnDeck: null }, }) - const [{ draggedItem, itemType, isOver }, drop] = useDrop({ - accept: DND_TYPES.LABWARE, - canDrop: (item: DroppedItem) => { - const draggedDef = item?.labwareOnDeck?.def - assert(draggedDef, 'no labware def of dragged item, expected it on drop') - - if (moduleType != null && draggedDef != null) { - // this is a module slot, prevent drop if the dragged labware is not compatible - const isCustomLabware = getLabwareIsCustom( - customLabwareDefs, - item.labwareOnDeck + const [{ draggedItem, itemType, isOver }, drop] = useDrop( + () => ({ + accept: DND_TYPES.LABWARE, + canDrop: (item: DroppedItem) => { + const draggedDef = item?.labwareOnDeck?.def + console.assert( + draggedDef, + 'no labware def of dragged item, expected it on drop' ) - return getLabwareIsCompatible(draggedDef, moduleType) || isCustomLabware - } - return true - }, - drop: (item: DroppedItem) => { - const droppedLabware = item - if (newSlot != null) { - dispatch(moveDeckItem(newSlot, slotId)) - } else if (droppedLabware.labwareOnDeck != null) { - const droppedSlot = droppedLabware.labwareOnDeck.slot - dispatch(moveDeckItem(droppedSlot, slotId)) - } - }, - hover: () => { - if (handleDragHover != null) { - handleDragHover() - } - }, - collect: (monitor: DropTargetMonitor) => ({ - itemType: monitor.getItemType(), - isOver: !!monitor.isOver(), - draggedItem: monitor.getItem() as DroppedItem, + if (moduleType != null && draggedDef != null) { + // this is a module slot, prevent drop if the dragged labware is not compatible + const isCustomLabware = getLabwareIsCustom( + customLabwareDefs, + item.labwareOnDeck + ) + + return ( + getLabwareIsCompatible(draggedDef, moduleType) || isCustomLabware + ) + } + return true + }, + drop: (item: DroppedItem) => { + const droppedLabware = item + if (droppedLabware.labwareOnDeck != null) { + const droppedSlot = droppedLabware.labwareOnDeck.slot + dispatch(moveDeckItem(droppedSlot, slotId)) + } + }, + hover: () => { + if (handleDragHover != null) { + handleDragHover() + } + }, + collect: (monitor: DropTargetMonitor) => ({ + itemType: monitor.getItemType(), + isOver: !!monitor.isOver(), + draggedItem: monitor.getItem() as DroppedItem, + }), }), - }) - - const draggedLabware = Object.values(labware).find( - l => l.id === draggedItem?.labwareOnDeck?.id + [] ) - React.useEffect(() => { - if (draggedLabware != null) { - setSlot(draggedLabware.slot) - } - }) - if ( selectedTerminalItemId !== START_TERMINAL_ITEM_ID || (itemType !== DND_TYPES.LABWARE && itemType !== null) || diff --git a/protocol-designer/src/components/OffDeckLabwareSlideout.tsx b/protocol-designer/src/components/OffDeckLabwareSlideout.tsx index c48479129a5..0fa281ef49c 100644 --- a/protocol-designer/src/components/OffDeckLabwareSlideout.tsx +++ b/protocol-designer/src/components/OffDeckLabwareSlideout.tsx @@ -92,7 +92,7 @@ export const OffDeckLabwareSlideout = ( > {offDeck == null ? ( { render(props) screen.getByText('Thermocycler module') screen.getByText('warning') - screen.getByText( - 'Slot 10 is occupied by a Heater-Shaker. Other modules cannot be placed in front of or behind a Heater-Shaker.' - ) + screen.getByText('Cannot place module') screen.getByText('mock SlotMap') }) it('renders a heater-shaker for flex and can select different slots', () => { diff --git a/protocol-designer/src/components/modals/EditModulesModal/index.tsx b/protocol-designer/src/components/modals/EditModulesModal/index.tsx index 4c3a42f808b..2e7bafa56b6 100644 --- a/protocol-designer/src/components/modals/EditModulesModal/index.tsx +++ b/protocol-designer/src/components/modals/EditModulesModal/index.tsx @@ -395,7 +395,11 @@ const EditModulesModalComponent = ( {slotIssue ? ( ) : null} diff --git a/protocol-designer/src/components/modules/FlexSlotMap.tsx b/protocol-designer/src/components/modules/FlexSlotMap.tsx index d03e068bd52..544b64ec07c 100644 --- a/protocol-designer/src/components/modules/FlexSlotMap.tsx +++ b/protocol-designer/src/components/modules/FlexSlotMap.tsx @@ -32,7 +32,7 @@ export function FlexSlotMap(props: FlexSlotMapProps): JSX.Element { void moveStep: (stepId: StepIdType, value: number) => void - setIsOver: React.Dispatch> findStepIndex: (stepId: StepIdType) => number + orderedStepIds: string[] } interface DropType { @@ -33,41 +31,39 @@ interface DropType { } const DragDropStepItem = (props: DragDropStepItemProps): JSX.Element => { - const { stepId, moveStep, clickDrop, setIsOver, findStepIndex } = props + const { stepId, moveStep, findStepIndex, orderedStepIds } = props const ref = React.useRef(null) - const [{ isDragging }, drag] = useDrag({ - type: DND_TYPES.STEP_ITEM, - item: { stepId }, - collect: (monitor: DragLayerMonitor) => ({ - isDragging: monitor.isDragging(), - }), - }) - - const [{ isOver, handlerId }, drop] = useDrop(() => ({ - accept: DND_TYPES.STEP_ITEM, - canDrop: () => { - return true - }, - drop: () => { - clickDrop() - }, - hover: (item: DropType) => { - const draggedId = item.stepId - if (draggedId !== stepId) { - const overIndex = findStepIndex(stepId) - moveStep(draggedId, overIndex) - } - }, - collect: (monitor: DropTargetOptions) => ({ - isOver: monitor.isOver(), - handlerId: monitor.getHandlerId(), + const [{ isDragging }, drag] = useDrag( + () => ({ + type: DND_TYPES.STEP_ITEM, + item: { stepId }, + collect: (monitor: DragLayerMonitor) => ({ + isDragging: monitor.isDragging(), + }), }), - })) + [orderedStepIds] + ) - React.useEffect(() => { - setIsOver(isOver) - }, [isOver]) + const [{ handlerId }, drop] = useDrop( + () => ({ + accept: DND_TYPES.STEP_ITEM, + canDrop: () => { + return true + }, + drop: (item: DropType) => { + const draggedId = item.stepId + if (draggedId !== stepId) { + const overIndex = findStepIndex(stepId) + moveStep(draggedId, overIndex) + } + }, + collect: (monitor: DropTargetOptions) => ({ + handlerId: monitor.getHandlerId(), + }), + }), + [orderedStepIds] + ) drag(drop(ref)) return ( @@ -90,42 +86,32 @@ export const DraggableStepItems = ( ): JSX.Element | null => { const { orderedStepIds, reorderSteps } = props const { t } = useTranslation('shared') - const [isOver, setIsOver] = React.useState(false) - const [stepIds, setStepIds] = React.useState(orderedStepIds) - - // needed to initalize stepIds - React.useEffect(() => { - setStepIds(orderedStepIds) - }, [orderedStepIds]) - - const clickDrop = (): void => { - if (!isEqual(orderedStepIds, stepIds)) { - if (confirm(t('confirm_reorder'))) { - reorderSteps(stepIds) - } - } - } const findStepIndex = (stepId: StepIdType): number => - stepIds.findIndex(id => stepId === id) + orderedStepIds.findIndex(id => stepId === id) const moveStep = (stepId: StepIdType, targetIndex: number): void => { - const currentIndex = orderedStepIds.findIndex(id => id === stepId) - - const newStepIds = [...orderedStepIds] - newStepIds.splice(currentIndex, 1) - newStepIds.splice(targetIndex, 0, stepId) - - setStepIds(newStepIds) + const currentIndex = findStepIndex(stepId) + + const currentRemoved = [ + ...orderedStepIds.slice(0, currentIndex), + ...orderedStepIds.slice(currentIndex + 1, orderedStepIds.length), + ] + const currentReinserted = [ + ...currentRemoved.slice(0, targetIndex), + stepId, + ...currentRemoved.slice(targetIndex, currentRemoved.length), + ] + if (confirm(t('confirm_reorder'))) { + reorderSteps(currentReinserted) + } } - const currentIds = isOver ? stepIds : orderedStepIds - return ( <> {({ makeStepOnContextMenu }) => - currentIds.map((stepId: StepIdType, index: number) => ( + orderedStepIds.map((stepId: StepIdType, index: number) => ( )) } diff --git a/protocol-designer/src/index.hbs b/protocol-designer/src/index.hbs deleted file mode 100644 index ab68be76554..00000000000 --- a/protocol-designer/src/index.hbs +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - {{htmlWebpackPlugin.options.title}} - - - - -
- - diff --git a/protocol-designer/src/localization/en/modal.json b/protocol-designer/src/localization/en/modal.json index 03562ba1bb3..edceb80718f 100644 --- a/protocol-designer/src/localization/en/modal.json +++ b/protocol-designer/src/localization/en/modal.json @@ -202,7 +202,7 @@ } }, "gate": { - "sign_up_below": "Sign Up For Opentrons Protocol Designer Beta", + "sign_up_below": "Sign Up For Opentrons Protocol Designer", "failed_verification": "Something Went Wrong", "sign_up_success": "Please confirm your email address to continue", "check_email": "We've sent a confirmation URL to your email that will take you to the Protocol Designer. Keep an eye out for a follow up email which contains links to resources such as our help documents." diff --git a/protocol-designer/src/step-forms/utils/index.ts b/protocol-designer/src/step-forms/utils/index.ts index dd279f492e3..34a23727dc9 100644 --- a/protocol-designer/src/step-forms/utils/index.ts +++ b/protocol-designer/src/step-forms/utils/index.ts @@ -11,7 +11,7 @@ import { SPAN7_8_10_11_SLOT, TC_SPAN_SLOTS } from '../../constants' import { hydrateField } from '../../steplist/fieldLevel' import { LabwareDefByDefURI } from '../../labware-defs' import type { DeckSlotId, ModuleType } from '@opentrons/shared-data' -import { +import type { AdditionalEquipmentOnDeck, InitialDeckSetup, ModuleOnDeck, @@ -120,6 +120,7 @@ export const getSlotIdsBlockedBySpanning = ( return [] } +// TODO(jr, 3/13/24): refactor this util it is messy and confusing export const getSlotIsEmpty = ( initialDeckSetup: InitialDeckSetup, slot: string, @@ -127,7 +128,15 @@ export const getSlotIsEmpty = ( since labware/wasteChute can still go on top of staging areas **/ includeStagingAreas?: boolean ): boolean => { + // special-casing the TC's slot A1 for the Flex if ( + slot === 'cutoutA1' && + Object.values(initialDeckSetup.modules).find( + module => module.type === THERMOCYCLER_MODULE_TYPE + ) + ) { + return false + } else if ( slot === SPAN7_8_10_11_SLOT && TC_SPAN_SLOTS.some(slot => !getSlotIsEmpty(initialDeckSetup, slot)) ) { @@ -157,11 +166,15 @@ export const getSlotIsEmpty = ( return additionalEquipment.location?.includes(slot) && includeStaging } }) - return ( [ ...values(initialDeckSetup.modules).filter( - (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.slot === slot + (moduleOnDeck: ModuleOnDeck) => { + const cutoutForSlotOt2 = slotToCutoutOt2Map[slot] + return cutoutForSlotOt2 != null + ? moduleOnDeck.slot === slot + : slot.includes(moduleOnDeck.slot) + } ), ...values(initialDeckSetup.labware).filter( (labware: LabwareOnDeckType) => labware.slot === slot diff --git a/protocol-designer/vite.config.ts b/protocol-designer/vite.config.ts index 2db2bd80b1a..70d055a6fd8 100644 --- a/protocol-designer/vite.config.ts +++ b/protocol-designer/vite.config.ts @@ -1,11 +1,12 @@ import path from 'path' -import { defineConfig } from 'vite' +import { UserConfig, defineConfig } from 'vite' import react from '@vitejs/plugin-react' import postCssImport from 'postcss-import' import postCssApply from 'postcss-apply' import postColorModFunction from 'postcss-color-mod-function' import postCssPresetEnv from 'postcss-preset-env' import lostCss from 'lost' +import { versionForProject } from '../scripts/git-version' const testAliases: {} | { 'file-saver': string } = process.env.CYPRESS === '1' @@ -15,53 +16,59 @@ const testAliases: {} | { 'file-saver': string } = } : {} -export default defineConfig({ - // this makes imports relative rather than absolute - base: '', - build: { - // Relative to the root - outDir: 'dist', - }, - plugins: [ - react({ - include: '**/*.tsx', - babel: { - // Use babel.config.js files - configFile: true, +export default defineConfig( + async (): Promise => { + const OT_PD_VERSION = await versionForProject('protocol-designer') + const OT_PD_BUILD_DATE = new Date().toUTCString() + return { + // this makes imports relative rather than absolute + base: '', + build: { + // Relative to the root + outDir: 'dist', }, - }), - ], - optimizeDeps: { - esbuildOptions: { - target: 'es2020', - }, - }, - css: { - postcss: { plugins: [ - postCssImport({ root: 'src/' }), - postCssApply(), - postColorModFunction(), - postCssPresetEnv({ stage: 0 }), - lostCss(), + react({ + include: '**/*.tsx', + babel: { + // Use babel.config.js files + configFile: true, + }, + }), ], - }, - }, - define: { - 'process.env': process.env, - global: 'globalThis', - }, - resolve: { - alias: { - '@opentrons/components/styles': path.resolve( - '../components/src/index.module.css' - ), - '@opentrons/components': path.resolve('../components/src/index.ts'), - '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), - '@opentrons/step-generation': path.resolve( - '../step-generation/src/index.ts' - ), - ...testAliases, - }, - }, -}) + optimizeDeps: { + esbuildOptions: { + target: 'es2020', + }, + }, + css: { + postcss: { + plugins: [ + postCssImport({ root: 'src/' }), + postCssApply(), + postColorModFunction(), + postCssPresetEnv({ stage: 0 }), + lostCss(), + ], + }, + }, + define: { + 'process.env': { ...process.env, OT_PD_VERSION, OT_PD_BUILD_DATE }, + global: 'globalThis', + }, + resolve: { + alias: { + '@opentrons/components/styles': path.resolve( + '../components/src/index.module.css' + ), + '@opentrons/components': path.resolve('../components/src/index.ts'), + '@opentrons/shared-data': path.resolve('../shared-data/js/index.ts'), + '@opentrons/step-generation': path.resolve( + '../step-generation/src/index.ts' + ), + ...testAliases, + }, + }, + } + } +) diff --git a/protocol-designer/webpack.config.js b/protocol-designer/webpack.config.js deleted file mode 100644 index d987a780b31..00000000000 --- a/protocol-designer/webpack.config.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' - -const path = require('path') -const webpack = require('webpack') -const merge = require('webpack-merge') -const HtmlWebpackPlugin = require('html-webpack-plugin') -const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin') -const WorkerPlugin = require('worker-plugin') -const { versionForProject } = require('../scripts/git-version') - -const { DEV_MODE, baseConfig } = require('@opentrons/webpack-config') -const { productName: title, description, author } = require('./package.json') -const PROTOCOL_DESIGNER_ENV_VAR_PREFIX = 'OT_PD_' -const PASS_THROUGH_ENV_VARS = Object.keys(process.env) - .filter(v => v.startsWith(PROTOCOL_DESIGNER_ENV_VAR_PREFIX)) - .concat(['NODE_ENV', 'CYPRESS']) - -const OT_PD_BUILD_DATE = new Date().toUTCString() - -const JS_ENTRY = path.join(__dirname, 'src/index.tsx') -const HTML_ENTRY = path.join(__dirname, 'src/index.hbs') -const ERROR_HTML = path.join(__dirname, 'src/error.html') - -const OUTPUT_PATH = path.join(__dirname, 'dist') -const PUBLIC_PATH = DEV_MODE ? '' : './' - -const testAliases = - process.env.CYPRESS === '1' - ? { - 'file-saver': path.resolve(__dirname, 'cypress/mocks/file-saver.js'), - } - : {} - -module.exports = async () => { - const OT_PD_VERSION = await versionForProject('protocol-designer') - - const envVarsWithDefaults = { - OT_PD_VERSION, - OT_PD_BUILD_DATE, - } - - const envVars = PASS_THROUGH_ENV_VARS.reduce( - (acc, envVar) => ({ [envVar]: '', ...acc }), - { ...envVarsWithDefaults } - ) - console.log(`PD version: ${OT_PD_VERSION || 'UNKNOWN!'}`) - return merge(baseConfig, { - entry: [JS_ENTRY], - - output: Object.assign( - { - path: OUTPUT_PATH, - publicPath: PUBLIC_PATH, - }, - // workaround for worker-plugin HMR - // see https://github.com/GoogleChromeLabs/worker-plugin#globalobject-string--false - DEV_MODE ? { globalObject: 'this' } : {} - ), - - plugins: [ - new webpack.EnvironmentPlugin(envVars), - new WorkerPlugin({ - // disable warnings about HMR when we're in prod - globalObject: DEV_MODE ? 'self' : false, - // add required JS plugins to child compiler - plugins: ['EnvironmentPlugin'], - }), - new HtmlWebpackPlugin({ - title, - description, - author, - template: HTML_ENTRY, - favicon: './src/images/favicon.ico', - }), - new HtmlWebpackPlugin({ - filename: 'error.html', - inject: false, - template: ERROR_HTML, - }), - new ScriptExtHtmlWebpackPlugin({ defaultAttribute: 'defer' }), - ], - - resolve: { - alias: testAliases, - }, - }) -} diff --git a/react-api-client/src/maintenance_runs/__fixtures__/index.ts b/react-api-client/src/maintenance_runs/__fixtures__/index.ts index 9d54ea798d4..94e6de036af 100644 --- a/react-api-client/src/maintenance_runs/__fixtures__/index.ts +++ b/react-api-client/src/maintenance_runs/__fixtures__/index.ts @@ -1,3 +1,2 @@ -export * from './maintenanceRunActions' export * from './maintenanceCommands' export * from './maintenanceRuns' diff --git a/react-api-client/src/maintenance_runs/__fixtures__/maintenanceRunActions.ts b/react-api-client/src/maintenance_runs/__fixtures__/maintenanceRunActions.ts deleted file mode 100644 index ceff8b09e5b..00000000000 --- a/react-api-client/src/maintenance_runs/__fixtures__/maintenanceRunActions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - MaintenanceRunAction, - MAINTENANCE_RUN_ACTION_TYPE_PLAY, - MAINTENANCE_RUN_ACTION_TYPE_PAUSE, - MAINTENANCE_RUN_ACTION_TYPE_STOP, -} from '@opentrons/api-client' - -export const mockPlayMaintenanceRunAction: MaintenanceRunAction = { - id: '1', - createdAt: '2021-10-25T13:23:31.366581+00:00', - actionType: MAINTENANCE_RUN_ACTION_TYPE_PLAY, -} - -export const mockPauseMaintenanceRunAction: MaintenanceRunAction = { - id: '2', - createdAt: '2021-10-25T13:23:31.366581+00:00', - actionType: MAINTENANCE_RUN_ACTION_TYPE_PAUSE, -} - -export const mockStopMaintenanceRunAction: MaintenanceRunAction = { - id: '3', - createdAt: '2021-10-25T13:23:31.366581+00:00', - actionType: MAINTENANCE_RUN_ACTION_TYPE_STOP, -} diff --git a/react-api-client/src/maintenance_runs/__fixtures__/maintenanceRuns.ts b/react-api-client/src/maintenance_runs/__fixtures__/maintenanceRuns.ts index aecfc87c68d..ae7ae65b433 100644 --- a/react-api-client/src/maintenance_runs/__fixtures__/maintenanceRuns.ts +++ b/react-api-client/src/maintenance_runs/__fixtures__/maintenanceRuns.ts @@ -1,4 +1,4 @@ -import { MAINTENANCE_RUN_ACTION_TYPE_PLAY } from '@opentrons/api-client' +import { RUN_ACTION_TYPE_PLAY } from '@opentrons/api-client' import type { MaintenanceRun, MaintenanceRunData } from '@opentrons/api-client' export const MAINTENANCE_RUN_ID = '1' @@ -12,7 +12,7 @@ export const mockRunningMaintenanceRun: MaintenanceRunData = { { id: '1', createdAt: '2021-10-25T12:54:53.366581+00:00', - actionType: MAINTENANCE_RUN_ACTION_TYPE_PLAY, + actionType: RUN_ACTION_TYPE_PLAY, }, ], errors: [], diff --git a/react-api-client/src/maintenance_runs/__tests__/usePlayMaintenanceRunMutation.test.tsx b/react-api-client/src/maintenance_runs/__tests__/usePlayMaintenanceRunMutation.test.tsx deleted file mode 100644 index 0f3f7c33f51..00000000000 --- a/react-api-client/src/maintenance_runs/__tests__/usePlayMaintenanceRunMutation.test.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import * as React from 'react' -import { describe, it, expect, beforeEach, vi } from 'vitest' -import { QueryClient, QueryClientProvider } from 'react-query' -import { act, renderHook, waitFor } from '@testing-library/react' -import { createRunAction } from '@opentrons/api-client' -import { useHost } from '../../api' -import { usePlayMaintenanceRunMutation } from '..' - -import { - MAINTENANCE_RUN_ID, - mockPlayMaintenanceRunAction, -} from '../__fixtures__' - -import type { HostConfig, Response, RunAction } from '@opentrons/api-client' -import type { UsePlayMaintenanceRunMutationOptions } from '../usePlayMaintenanceRunMutation' - -vi.mock('@opentrons/api-client') -vi.mock('../../api/useHost') - -const HOST_CONFIG: HostConfig = { hostname: 'localhost' } - -describe('usePlayMaintenanceRunMutation hook', () => { - let wrapper: React.FunctionComponent< - { children: React.ReactNode } & UsePlayMaintenanceRunMutationOptions - > - - beforeEach(() => { - const queryClient = new QueryClient() - const clientProvider: React.FunctionComponent< - { children: React.ReactNode } & UsePlayMaintenanceRunMutationOptions - > = ({ children }) => ( - {children} - ) - wrapper = clientProvider - }) - - it('should return no data when calling playRun if the request fails', async () => { - vi.mocked(useHost).mockReturnValue(HOST_CONFIG) - vi.mocked(createRunAction).mockRejectedValue('oh no') - - const { result } = renderHook(usePlayMaintenanceRunMutation, { - wrapper, - }) - - expect(result.current.data).toBeUndefined() - act(() => result.current.playMaintenanceRun(MAINTENANCE_RUN_ID)) - await waitFor(() => { - expect(result.current.data).toBeUndefined() - }) - }) - - it('should create a play run action when calling the playRun callback', async () => { - vi.mocked(useHost).mockReturnValue(HOST_CONFIG) - vi.mocked(createRunAction).mockResolvedValue({ - data: mockPlayMaintenanceRunAction, - } as Response) - - const { result } = renderHook(usePlayMaintenanceRunMutation, { - wrapper, - }) - act(() => result.current.playMaintenanceRun(MAINTENANCE_RUN_ID)) - - await waitFor(() => { - expect(result.current.data).toEqual(mockPlayMaintenanceRunAction) - }) - }) -}) diff --git a/react-api-client/src/maintenance_runs/index.ts b/react-api-client/src/maintenance_runs/index.ts index 4d6d89d9c33..87cf36b0bdc 100644 --- a/react-api-client/src/maintenance_runs/index.ts +++ b/react-api-client/src/maintenance_runs/index.ts @@ -3,5 +3,4 @@ export { useMaintenanceRunQuery } from './useMaintenanceRunQuery' export { useCreateMaintenanceCommandMutation } from './useCreateMaintenanceCommandMutation' export { useCreateMaintenanceRunLabwareDefinitionMutation } from './useCreateMaintenanceRunLabwareDefinitionMutation' export { useDeleteMaintenanceRunMutation } from './useDeleteMaintenanceRunMutation' -export { usePlayMaintenanceRunMutation } from './usePlayMaintenanceRunMutation' export { useCurrentMaintenanceRun } from './useCurrentMaintenanceRun' diff --git a/react-api-client/src/maintenance_runs/usePlayMaintenanceRunMutation.ts b/react-api-client/src/maintenance_runs/usePlayMaintenanceRunMutation.ts deleted file mode 100644 index 72fba1978e8..00000000000 --- a/react-api-client/src/maintenance_runs/usePlayMaintenanceRunMutation.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { - HostConfig, - RunAction, - MAINTENANCE_RUN_ACTION_TYPE_PLAY, - createRunAction, -} from '@opentrons/api-client' -import { - UseMutationResult, - useMutation, - UseMutateFunction, - UseMutationOptions, -} from 'react-query' -import { useHost } from '../api' - -import type { AxiosError } from 'axios' - -export type UsePlayMaintenanceRunMutationResult = UseMutationResult< - RunAction, - AxiosError, - string -> & { - playMaintenanceRun: UseMutateFunction -} - -export type UsePlayMaintenanceRunMutationOptions = UseMutationOptions< - RunAction, - AxiosError, - string -> - -export const usePlayMaintenanceRunMutation = ( - options: UsePlayMaintenanceRunMutationOptions = {} -): UsePlayMaintenanceRunMutationResult => { - const host = useHost() - const mutation = useMutation( - [host, 'maintenance_runs', MAINTENANCE_RUN_ACTION_TYPE_PLAY], - (runId: string) => - createRunAction(host as HostConfig, runId, { - actionType: MAINTENANCE_RUN_ACTION_TYPE_PLAY, - }) - .then(response => response.data) - .catch(e => { - throw e - }), - options - ) - return { - ...mutation, - playMaintenanceRun: mutation.mutate, - } -} diff --git a/robot-server/robot_server/commands/router.py b/robot-server/robot_server/commands/router.py index 9a06f9a7171..0d617e38a5a 100644 --- a/robot-server/robot_server/commands/router.py +++ b/robot-server/robot_server/commands/router.py @@ -140,7 +140,7 @@ async def get_commands_list( description=( "The starting index of the desired first command in the list." " If unspecified, a cursor will be selected automatically" - " based on the next queued or more recently executed command." + " based on the currently running or most recently executed command." ), ), pageLength: int = Query( diff --git a/robot-server/robot_server/protocols/analysis_models.py b/robot-server/robot_server/protocols/analysis_models.py index 2053d8ee3e4..0a3c64c9db0 100644 --- a/robot-server/robot_server/protocols/analysis_models.py +++ b/robot-server/robot_server/protocols/analysis_models.py @@ -1,6 +1,8 @@ """Response models for protocol analysis.""" # TODO(mc, 2021-08-25): add modules to simulation result from enum import Enum + +from opentrons.protocol_engine.types import RunTimeParameter from opentrons_shared_data.robot.dev_types import RobotType from pydantic import BaseModel, Field from typing import List, Optional, Union @@ -102,6 +104,16 @@ class CompletedAnalysis(BaseModel): " in analyses that were originally created on older versions." ), ) + runTimeParameters: List[RunTimeParameter] = Field( + default_factory=list, + description=( + "Run time parameters used during analysis." + " These are the parameters that are defined in the protocol, with values" + " specified either in the protocol creation request or reanalysis request" + " (whichever started this analysis), or default values from the protocol" + " if none are specified in the request." + ), + ) commands: List[Command] = Field( ..., description="The protocol commands the run is expected to produce", diff --git a/robot-server/robot_server/protocols/analysis_store.py b/robot-server/robot_server/protocols/analysis_store.py index 1af59788ed2..f59fed7176f 100644 --- a/robot-server/robot_server/protocols/analysis_store.py +++ b/robot-server/robot_server/protocols/analysis_store.py @@ -4,6 +4,8 @@ from logging import getLogger from typing import Dict, List, Optional + +from opentrons.protocol_engine.types import RunTimeParameter from typing_extensions import Final from opentrons_shared_data.robot.dev_types import RobotType @@ -122,6 +124,7 @@ async def update( self, analysis_id: str, robot_type: RobotType, + run_time_parameters: List[RunTimeParameter], commands: List[Command], labware: List[LoadedLabware], modules: List[LoadedModule], @@ -135,6 +138,7 @@ async def update( analysis_id: The ID of the analysis to promote. Must point to a valid pending analysis. robot_type: See `CompletedAnalysis.robotType`. + run_time_parameters: See `CompletedAnalysis.runTimeParameters`. commands: See `CompletedAnalysis.commands`. labware: See `CompletedAnalysis.labware`. modules: See `CompletedAnalysis.modules`. @@ -161,6 +165,7 @@ async def update( result=result, robotType=robot_type, status=AnalysisStatus.COMPLETED, + runTimeParameters=run_time_parameters, commands=commands, labware=labware, modules=modules, diff --git a/robot-server/robot_server/protocols/protocol_analyzer.py b/robot-server/robot_server/protocols/protocol_analyzer.py index 49457d864f9..8ae6cb0c647 100644 --- a/robot-server/robot_server/protocols/protocol_analyzer.py +++ b/robot-server/robot_server/protocols/protocol_analyzer.py @@ -42,6 +42,9 @@ async def analyze( await self._analysis_store.update( analysis_id=analysis_id, robot_type=protocol_resource.source.robot_type, + # TODO (spp, 2024-03-12): populate the RTP field if we decide to have + # parameter parsing and validation in protocol reader itself. + run_time_parameters=[], commands=[], labware=[], modules=[], @@ -64,6 +67,9 @@ async def analyze( await self._analysis_store.update( analysis_id=analysis_id, robot_type=protocol_resource.source.robot_type, + # TODO(spp, 2024-03-12): update this once protocol reader/ runner can parse + # and report the runTimeParameters + run_time_parameters=[], commands=result.commands, labware=result.state_summary.labware, modules=result.state_summary.modules, diff --git a/robot-server/robot_server/protocols/protocol_models.py b/robot-server/robot_server/protocols/protocol_models.py index 0e902d60034..3ce1d52443c 100644 --- a/robot-server/robot_server/protocols/protocol_models.py +++ b/robot-server/robot_server/protocols/protocol_models.py @@ -1,7 +1,7 @@ """Protocol file models.""" from datetime import datetime from pydantic import BaseModel, Extra, Field -from typing import Any, List, Optional +from typing import Any, List, Optional, Dict, Union from opentrons.protocol_reader import ( ProtocolType as ProtocolType, @@ -109,3 +109,6 @@ class Protocol(ResourceModel): " See `POST /protocols`." ), ) + + +RunTimeParameterDict = Dict[str, Union[str, int, float, bool]] diff --git a/robot-server/robot_server/protocols/router.py b/robot-server/robot_server/protocols/router.py index a64990cf27c..e71be06864f 100644 --- a/robot-server/robot_server/protocols/router.py +++ b/robot-server/robot_server/protocols/router.py @@ -238,6 +238,7 @@ async def create_protocol( ) try: + # Can make the passed in RTPs as part of protocolSource returned here source = await protocol_reader.save( files=buffered_files, directory=protocol_directory / protocol_id, diff --git a/robot-server/robot_server/runs/run_store.py b/robot-server/robot_server/runs/run_store.py index 38df8e064c6..a6da6942a11 100644 --- a/robot-server/robot_server/runs/run_store.py +++ b/robot-server/robot_server/runs/run_store.py @@ -131,7 +131,7 @@ def update_run_state( action_rows = transaction.execute(select_actions).all() self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) return _convert_row_to_run(row=run_row, action_rows=action_rows) def insert_action(self, run_id: str, action: RunAction) -> None: @@ -154,7 +154,7 @@ def insert_action(self, run_id: str, action: RunAction) -> None: transaction.execute(insert) self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) def insert( self, @@ -196,7 +196,7 @@ def insert( raise ProtocolNotFoundError(protocol_id=run.protocol_id) self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_refetch(run_id=run_id) return run @lru_cache(maxsize=_CACHE_ENTRIES) @@ -417,7 +417,7 @@ def remove(self, run_id: str) -> None: raise RunNotFoundError(run_id) self._clear_caches() - self._runs_publisher.publish_runs(run_id=run_id) + self._runs_publisher.publish_runs_advise_unsubscribe(run_id=run_id) def _run_exists( self, run_id: str, connection: sqlalchemy.engine.Connection diff --git a/robot-server/robot_server/service/json_api/__init__.py b/robot-server/robot_server/service/json_api/__init__.py index 8966763cb53..2680c99049f 100644 --- a/robot-server/robot_server/service/json_api/__init__.py +++ b/robot-server/robot_server/service/json_api/__init__.py @@ -16,6 +16,7 @@ PydanticResponse, ResponseList, NotifyRefetchBody, + NotifyUnsubscribeBody, ) @@ -46,4 +47,5 @@ "ResponseList", # notify models "NotifyRefetchBody", + "NotifyUnsubscribeBody", ] diff --git a/robot-server/robot_server/service/json_api/response.py b/robot-server/robot_server/service/json_api/response.py index dd2d0dc7b1d..9d2c2cb76b9 100644 --- a/robot-server/robot_server/service/json_api/response.py +++ b/robot-server/robot_server/service/json_api/response.py @@ -285,5 +285,15 @@ class ResponseList(BaseModel, Generic[ResponseDataT]): class NotifyRefetchBody(BaseResponseBody): - "A notification response that returns a flag for refetching via HTTP." + """A notification response that returns a flag for refetching via HTTP.""" + refetchUsingHTTP: bool = True + + +class NotifyUnsubscribeBody(BaseResponseBody): + """A notification response. + + Returns flags for unsubscribing from a topic. + """ + + unsubscribe: bool = True diff --git a/robot-server/robot_server/service/notifications/notification_client.py b/robot-server/robot_server/service/notifications/notification_client.py index 1ca2703d031..568d161cf53 100644 --- a/robot-server/robot_server/service/notifications/notification_client.py +++ b/robot-server/robot_server/service/notifications/notification_client.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Optional from enum import Enum -from ..json_api import NotifyRefetchBody +from ..json_api import NotifyRefetchBody, NotifyUnsubscribeBody from server_utils.fastapi_utils.app_state import ( AppState, AppStateAccessor, @@ -77,26 +77,50 @@ async def disconnect(self) -> None: self.client.loop_stop() await to_thread.run_sync(self.client.disconnect) - async def publish_async( - self, topic: str, message: NotifyRefetchBody = NotifyRefetchBody() + async def publish_advise_refetch_async(self, topic: str) -> None: + """Asynchronously publish a refetch message on a specific topic to the MQTT broker. + + Args: + topic: The topic to publish the message on. + """ + await to_thread.run_sync(self.publish_advise_refetch, topic) + + async def publish_advise_unsubscribe_async(self, topic: str) -> None: + """Asynchronously publish an unsubscribe message on a specific topic to the MQTT broker. + + Args: + topic: The topic to publish the message on. + """ + await to_thread.run_sync(self.publish_advise_unsubscribe, topic) + + def publish_advise_refetch( + self, + topic: str, ) -> None: - """Asynchronously Publish a message on a specific topic to the MQTT broker. + """Publish a refetch message on a specific topic to the MQTT broker. Args: topic: The topic to publish the message on. - message: The message to be published, in the format of NotifyRefetchBody. """ - await to_thread.run_sync(self.publish, topic, message) + message = NotifyRefetchBody.construct() + payload = message.json() + self.client.publish( + topic=topic, + payload=payload, + qos=self._default_qos, + retain=self._retain_message, + ) - def publish( - self, topic: str, message: NotifyRefetchBody = NotifyRefetchBody() + def publish_advise_unsubscribe( + self, + topic: str, ) -> None: - """Publish a message on a specific topic to the MQTT broker. + """Publish an unsubscribe message on a specific topic to the MQTT broker. Args: topic: The topic to publish the message on. - message: The message to be published. """ + message = NotifyUnsubscribeBody.construct() payload = message.json() self.client.publish( topic=topic, diff --git a/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py index f6f146e11e4..8ef07fd7eac 100644 --- a/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/maintenance_runs_publisher.py @@ -20,7 +20,9 @@ async def publish_current_maintenance_run( self, ) -> None: """Publishes the equivalent of GET /maintenance_run/current_run""" - await self._client.publish_async(topic=Topics.MAINTENANCE_RUNS_CURRENT_RUN) + await self._client.publish_advise_refetch_async( + topic=Topics.MAINTENANCE_RUNS_CURRENT_RUN + ) _maintenance_runs_publisher_accessor: AppStateAccessor[ diff --git a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py index 11222005b05..94aed694e8f 100644 --- a/robot-server/robot_server/service/notifications/publishers/runs_publisher.py +++ b/robot-server/robot_server/service/notifications/publishers/runs_publisher.py @@ -1,5 +1,6 @@ from fastapi import Depends import asyncio +import logging from typing import Union, Callable, Optional from opentrons.protocol_engine import CurrentCommand, StateSummary, EngineStatus @@ -13,6 +14,11 @@ from ..topics import Topics +log: logging.Logger = logging.getLogger(__name__) + +POLL_INTERVAL = 1 + + class RunsPublisher: """Publishes protocol runs topics.""" @@ -34,7 +40,8 @@ async def begin_polling_engine_store( """Continuously poll the engine store for the current_command. Args: - current_command: The currently executing command, if any. + get_current_command: Callback to get the currently executing command, if any. + get_state_summary: Callback to get the current run's state summary, if any. run_id: ID of the current run. """ if self._poller is None: @@ -56,24 +63,28 @@ async def begin_polling_engine_store( ) async def stop_polling_engine_store(self) -> None: - """Stops polling the engine store.""" + """Stops polling the engine store. Run-related topics will publish as the poller is cancelled.""" if self._poller is not None: self._run_data_manager_polling.set() self._poller.cancel() - self._poller = None - self._run_data_manager_polling.clear() - self._previous_current_command = None - self._previous_state_summary_status = None - await self._client.publish_async(topic=Topics.RUNS_CURRENT_COMMAND) - def publish_runs(self, run_id: str) -> None: + def publish_runs_advise_refetch(self, run_id: str) -> None: + """Publishes the equivalent of GET /runs and GET /runs/:runId. + + Args: + run_id: ID of the current run. + """ + self._client.publish_advise_refetch(topic=Topics.RUNS) + self._client.publish_advise_refetch(topic=f"{Topics.RUNS}/{run_id}") + + def publish_runs_advise_unsubscribe(self, run_id: str) -> None: """Publishes the equivalent of GET /runs and GET /runs/:runId. Args: run_id: ID of the current run. """ - self._client.publish(topic=Topics.RUNS) - self._client.publish(topic=f"{Topics.RUNS}/{run_id}") + self._client.publish_advise_unsubscribe(topic=Topics.RUNS) + self._client.publish_advise_unsubscribe(topic=f"{Topics.RUNS}/{run_id}") async def _poll_engine_store( self, @@ -85,8 +96,38 @@ async def _poll_engine_store( Args: get_current_command: Retrieves the engine store's current command. + get_state_summary: Retrieves the engine store's state summary. run_id: ID of the current run. """ + try: + await self._poll_for_run_id_info( + get_current_command=get_current_command, + get_state_summary=get_state_summary, + run_id=run_id, + ) + except asyncio.CancelledError: + self._clean_up_poller() + await self._publish_runs_advise_unsubscribe_async(run_id=run_id) + await self._client.publish_advise_refetch_async( + topic=Topics.RUNS_CURRENT_COMMAND + ) + except Exception as e: + log.error(f"Error within run data manager poller: {e}") + + async def _poll_for_run_id_info( + self, + get_current_command: Callable[[str], Optional[CurrentCommand]], + get_state_summary: Callable[[str], Optional[StateSummary]], + run_id: str, + ): + """Poll the engine store for a specific run's state while the poll is active. + + Args: + get_current_command: Retrieves the engine store's current command. + get_state_summary: Retrieves the engine store's state summary. + run_id: ID of the current run. + """ + while not self._run_data_manager_polling.is_set(): current_command = get_current_command(run_id) current_state_summary = get_state_summary(run_id) @@ -99,24 +140,44 @@ async def _poll_engine_store( self._previous_current_command = current_command if self._previous_state_summary_status != current_state_summary_status: - await self._publish_runs_async(run_id=run_id) + await self._publish_runs_advise_refetch_async(run_id=run_id) self._previous_state_summary_status = current_state_summary_status - await asyncio.sleep(1) + await asyncio.sleep(POLL_INTERVAL) async def _publish_current_command( self, ) -> None: """Publishes the equivalent of GET /runs/:runId/commands?cursor=null&pageLength=1.""" - await self._client.publish_async(topic=Topics.RUNS_CURRENT_COMMAND) + await self._client.publish_advise_refetch_async( + topic=Topics.RUNS_CURRENT_COMMAND + ) + + async def _publish_runs_advise_refetch_async(self, run_id: str) -> None: + """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via a refetch message. + + Args: + run_id: ID of the current run. + """ + await self._client.publish_advise_refetch_async(topic=Topics.RUNS) + await self._client.publish_advise_refetch_async(topic=f"{Topics.RUNS}/{run_id}") - async def _publish_runs_async(self, run_id: str) -> None: - """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId. + async def _publish_runs_advise_unsubscribe_async(self, run_id: str) -> None: + """Asynchronously publishes the equivalent of GET /runs and GET /runs/:runId via an unsubscribe message. Args: run_id: ID of the current run. """ - await self._client.publish_async(topic=Topics.RUNS) - await self._client.publish_async(topic=f"{Topics.RUNS}/{run_id}") + await self._client.publish_advise_unsubscribe_async(topic=Topics.RUNS) + await self._client.publish_advise_unsubscribe_async( + topic=f"{Topics.RUNS}/{run_id}" + ) + + def _clean_up_poller(self) -> None: + """Cleans up the runs data manager poller.""" + self._poller = None + self._run_data_manager_polling.clear() + self._previous_current_command = None + self._previous_state_summary_status = None _runs_publisher_accessor: AppStateAccessor[RunsPublisher] = AppStateAccessor[ diff --git a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml index 13af0f78d84..f2d17aff265 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml @@ -85,6 +85,7 @@ stages: status: completed result: ok robotType: OT-2 Standard + runTimeParameters: [] pipettes: - id: pipetteId pipetteName: p10_single diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml index 636cd055090..a592d757baf 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -86,6 +86,7 @@ stages: status: completed result: ok robotType: OT-3 Standard + runTimeParameters: [] pipettes: - id: pipetteId pipetteName: p1000_96 diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml index 48fb8200d61..afc1644afbd 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml @@ -85,6 +85,7 @@ stages: status: completed result: ok robotType: OT-2 Standard + runTimeParameters: [] pipettes: - id: pipetteId pipetteName: p10_single diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml index e468c8de84a..65929b5c9be 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -329,7 +329,14 @@ stages: status_code: 200 json: links: - current: !anydict + current: + href: !anystr + meta: + runId: !anystr + commandId: !anystr + index: 14 + key: !anystr + createdAt: !anystr meta: cursor: 0 totalLength: 15 @@ -564,7 +571,14 @@ stages: status_code: 200 json: links: - current: !anydict + current: + href: !anystr + meta: + runId: !anystr + commandId: !anystr + index: 14 + key: !anystr + createdAt: !anystr meta: cursor: 5 totalLength: 15 diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml index db35113b5ca..d9266dff9b0 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml @@ -81,7 +81,14 @@ stages: status_code: 200 json: links: - current: !anydict + current: + href: !anystr + meta: + runId: !anystr + commandId: !anystr + index: 4 + key: !anystr + createdAt: !anystr meta: cursor: 3 totalLength: 5 diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index bd11483d511..580feda6597 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -329,7 +329,14 @@ stages: status_code: 200 json: links: - current: !anydict + current: + href: !anystr + meta: + runId: !anystr + commandId: !anystr + index: 14 + key: !anystr + createdAt: !anystr meta: cursor: 0 totalLength: 15 diff --git a/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml index 443767c27fc..f7f54b8ac3e 100644 --- a/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml @@ -82,7 +82,14 @@ stages: status_code: 200 json: links: - current: !anydict + current: + href: !anystr + meta: + runId: !anystr + commandId: !anystr + index: 3 + key: !anystr + createdAt: !anystr meta: cursor: 3 totalLength: 4 diff --git a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml index e0521f3e655..ddac99be771 100644 --- a/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_protocol_run.tavern.yaml @@ -151,7 +151,14 @@ stages: status_code: 200 json: links: - current: !anydict + current: + href: !anystr + meta: + runId: !anystr + commandId: !anystr + index: 1 + key: !anystr + createdAt: !anystr meta: cursor: 0 totalLength: 2 diff --git a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml index 3434f210bd0..31de3799870 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_queued_protocol_commands.tavern.yaml @@ -139,7 +139,14 @@ stages: status_code: 200 json: links: - current: !anydict + current: + href: !anystr + meta: + runId: !anystr + commandId: !anystr + index: 3 + key: !anystr + createdAt: !anystr meta: cursor: 0 totalLength: 4 diff --git a/robot-server/tests/protocols/test_analysis_store.py b/robot-server/tests/protocols/test_analysis_store.py index 7207e15ff60..b9c2dcccdac 100644 --- a/robot-server/tests/protocols/test_analysis_store.py +++ b/robot-server/tests/protocols/test_analysis_store.py @@ -127,6 +127,7 @@ async def test_returned_in_order_added( await subject.update( analysis_id=analysis_id, robot_type="OT-2 Standard", + run_time_parameters=[], labware=[], modules=[], pipettes=[], @@ -175,6 +176,7 @@ async def test_update_adds_details_and_completes_analysis( await subject.update( analysis_id="analysis-id", robot_type="OT-2 Standard", + run_time_parameters=[], labware=[labware], pipettes=[pipette], # TODO(mm, 2022-10-21): Give the subject some commands, errors, and liquids here @@ -193,6 +195,7 @@ async def test_update_adds_details_and_completes_analysis( status=AnalysisStatus.COMPLETED, result=AnalysisResult.OK, robotType="OT-2 Standard", + runTimeParameters=[], labware=[labware], pipettes=[pipette], modules=[], @@ -206,6 +209,7 @@ async def test_update_adds_details_and_completes_analysis( "result": "ok", "status": "completed", "robotType": "OT-2 Standard", + "runTimeParameters": [], "labware": [ { "id": "labware-id", @@ -276,6 +280,7 @@ async def test_update_infers_status_from_errors( await subject.update( analysis_id="analysis-id", robot_type="OT-2 Standard", + run_time_parameters=[], commands=commands, errors=errors, labware=[], diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 4b76386acd4..8339460cf66 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -159,13 +159,13 @@ async def test_get_by_analysis_id_as_document( "id": "analysis-id", "result": "ok", "status": "completed", + "runTimeParameters": [], "commands": [], "errors": [], "labware": [], "liquids": [], "modules": [], "pipettes": [], - "result": "ok", } diff --git a/robot-server/tests/protocols/test_protocol_analyzer.py b/robot-server/tests/protocols/test_protocol_analyzer.py index 5f53452b7a2..77146333669 100644 --- a/robot-server/tests/protocols/test_protocol_analyzer.py +++ b/robot-server/tests/protocols/test_protocol_analyzer.py @@ -158,6 +158,7 @@ async def test_analyze( await analysis_store.update( analysis_id="analysis-id", robot_type=robot_type, + run_time_parameters=[], commands=[analysis_command], labware=[analysis_labware], modules=[], @@ -237,6 +238,7 @@ async def test_analyze_updates_pending_on_error( await analysis_store.update( analysis_id="analysis-id", robot_type=robot_type, + run_time_parameters=[], commands=[], labware=[], modules=[], diff --git a/robot-server/tests/runs/router/test_commands_router.py b/robot-server/tests/runs/router/test_commands_router.py index cc06ddd621f..fa5e47ada9a 100644 --- a/robot-server/tests/runs/router/test_commands_router.py +++ b/robot-server/tests/runs/router/test_commands_router.py @@ -8,6 +8,7 @@ CommandSlice, CurrentCommand, ProtocolEngine, + CommandNote, commands as pe_commands, errors as pe_errors, ) @@ -249,7 +250,7 @@ async def test_get_run_commands( decoy: Decoy, mock_run_data_manager: RunDataManager ) -> None: """It should return a list of all commands in a run.""" - long_note = pe_commands.CommandNote( + long_note = CommandNote( noteKind="warning", shortMessage="this is a warning.", longMessage=""" @@ -261,7 +262,7 @@ async def test_get_run_commands( """, source="test", ) - unenumed_note = pe_commands.CommandNote( + unenumed_note = CommandNote( noteKind="lahsdlasd", shortMessage="Oh no", longMessage="its a notekind not in the enum", diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index b807cbf1e18..8c696426c76 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -5,6 +5,7 @@ import pytest from decoy import Decoy from sqlalchemy.engine import Engine +from unittest import mock from opentrons_shared_data.pipette.dev_types import PipetteNameType @@ -162,6 +163,7 @@ def test_update_run_state( subject: RunStore, state_summary: StateSummary, protocol_commands: List[pe_commands.Command], + mock_runs_publisher: mock.Mock, ) -> None: """It should be able to update a run state to the store.""" action = RunAction( @@ -197,6 +199,9 @@ def test_update_run_state( ) assert run_summary_result == state_summary assert commands_result.commands == protocol_commands + mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( + run_id="run-id" + ) def test_update_state_run_not_found( @@ -372,7 +377,7 @@ def test_get_all_runs( assert result == expected_result -def test_remove_run(subject: RunStore) -> None: +def test_remove_run(subject: RunStore, mock_runs_publisher: mock.Mock) -> None: """It can remove a previously stored run entry.""" action = RunAction( actionType=RunActionType.PLAY, @@ -389,6 +394,9 @@ def test_remove_run(subject: RunStore) -> None: subject.remove(run_id="run-id") assert subject.get_all(length=20) == [] + mock_runs_publisher.publish_runs_advise_unsubscribe.assert_called_once_with( + run_id="run-id" + ) def test_remove_run_missing_id(subject: RunStore) -> None: @@ -409,7 +417,9 @@ def test_insert_actions_no_run(subject: RunStore) -> None: subject.insert_action(run_id="run-id-996", action=action) -def test_get_state_summary(subject: RunStore, state_summary: StateSummary) -> None: +def test_get_state_summary( + subject: RunStore, state_summary: StateSummary, mock_runs_publisher: mock.Mock +) -> None: """It should be able to get store run data.""" subject.insert( run_id="run-id", @@ -419,6 +429,9 @@ def test_get_state_summary(subject: RunStore, state_summary: StateSummary) -> No subject.update_run_state(run_id="run-id", summary=state_summary, commands=[]) result = subject.get_state_summary(run_id="run-id") assert result == state_summary + mock_runs_publisher.publish_runs_advise_refetch.assert_called_once_with( + run_id="run-id" + ) def test_get_state_summary_failure( diff --git a/robot-server/tests/service/json_api/test_response.py b/robot-server/tests/service/json_api/test_response.py index 4424774140a..1429d88b5e0 100644 --- a/robot-server/tests/service/json_api/test_response.py +++ b/robot-server/tests/service/json_api/test_response.py @@ -13,6 +13,7 @@ MultiBody, MultiBodyMeta, NotifyRefetchBody, + NotifyUnsubscribeBody, DeprecatedResponseModel, DeprecatedMultiResponseModel, ) @@ -116,6 +117,10 @@ class ResponseSpec(NamedTuple): }, ), ResponseSpec(subject=NotifyRefetchBody(), expected={"refetchUsingHTTP": True}), + ResponseSpec( + subject=NotifyUnsubscribeBody(), + expected={"unsubscribe": True}, + ), ] diff --git a/shared-data/command/schemas/8.json b/shared-data/command/schemas/8.json index c2eb0a0e2a8..a17be9ee690 100644 --- a/shared-data/command/schemas/8.json +++ b/shared-data/command/schemas/8.json @@ -612,7 +612,7 @@ "frontRightNozzle": { "title": "Frontrightnozzle", "description": "The front right nozzle in your configuration.", - "pattern": "[A-Z][0-100]", + "pattern": "[A-Z]\\d{1,2}", "type": "string" } }, diff --git a/shared-data/js/__tests__/labwareDefSchemaV2.test.ts b/shared-data/js/__tests__/labwareDefSchemaV2.test.ts index f95e663eabd..0f063a0bfa6 100644 --- a/shared-data/js/__tests__/labwareDefSchemaV2.test.ts +++ b/shared-data/js/__tests__/labwareDefSchemaV2.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/consistent-test-it */ import path from 'path' import glob from 'glob' import Ajv from 'ajv' @@ -169,7 +168,6 @@ const expectGroupsFollowConvention = ( if (noGroupsMetadataAllowed) { labwareDef.groups.forEach(group => { - /* eslint-disable jest/no-conditional-expect */ expect(group.brand).toBe(undefined) expect(group.metadata.displayName).toBe(undefined) expect(group.metadata.displayCategory).toBe(undefined) diff --git a/shared-data/js/__tests__/pipettes.test.ts b/shared-data/js/__tests__/pipettes.test.ts index c5f3e4ddd4b..0fe60334c3f 100644 --- a/shared-data/js/__tests__/pipettes.test.ts +++ b/shared-data/js/__tests__/pipettes.test.ts @@ -1,6 +1,11 @@ // tests for pipette info accessors in `shared-data/js/pipettes.js` import { describe, expect, it } from 'vitest' -import { getPipetteNameSpecs, getPipetteModelSpecs } from '../pipettes' +import { + getPipetteSpecsV2, + getPipetteNameSpecs, + getPipetteModelSpecs, +} from '../pipettes' +import type { PipetteV2LiquidSpecs, PipetteV2Specs } from '../types' const PIPETTE_NAMES = [ 'p10_single', @@ -56,4 +61,181 @@ describe('pipette data accessors', () => { expect(getPipetteModelSpecs(model)).toMatchSnapshot()) ) }) + + describe('getPipetteSpecsV2', () => { + it('returns the correct info for p1000_single_flex', () => { + const mockP1000Specs = { + $otSharedSchema: '#/pipette/schemas/2/pipetteGeometrySchema.json', + availableSensors: { + sensors: ['pressure', 'capacitive', 'environment'], + capacitive: { count: 1 }, + environment: { count: 1 }, + pressure: { count: 1 }, + }, + backCompatNames: [], + backlashDistance: 0.1, + channels: 1, + displayCategory: 'FLEX', + displayName: 'Flex 1-Channel 1000 μL', + dropTipConfigurations: { plungerEject: { current: 1, speed: 10 } }, + liquids: { + default: { + $otSharedSchema: + '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', + defaultTipOverlapDictionary: { + default: 10.5, + 'opentrons/opentrons_flex_96_tiprack_1000ul/1': 10.5, + 'opentrons/opentrons_flex_96_tiprack_200ul/1': 10.5, + 'opentrons/opentrons_flex_96_tiprack_50ul/1': 10.5, + }, + defaultTipracks: [ + 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + 'opentrons/opentrons_flex_96_tiprack_200ul/1', + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + ], + minVolume: 5, + maxVolume: 1000, + supportedTips: expect.anything(), + }, + }, + model: 'p1000', + nozzleMap: expect.anything(), + pathTo3D: + 'pipette/definitions/2/geometry/single_channel/p1000/placeholder.gltf', + pickUpTipConfigurations: { + pressFit: { + speedByTipCount: expect.anything(), + presses: 1, + increment: 0, + distanceByTipCount: expect.anything(), + currentByTipCount: expect.anything(), + }, + }, + partialTipConfigurations: { + availableConfigurations: null, + partialTipSupported: false, + }, + plungerHomingConfigurations: { current: 1, speed: 30 }, + plungerMotorConfigurations: { idle: 0.3, run: 1 }, + plungerPositionsConfigurations: { + default: { blowout: 76.5, bottom: 71.5, drop: 90.5, top: 0.5 }, + }, + quirks: [], + shaftDiameter: 4.5, + shaftULperMM: 15.904, + nozzleOffset: [-8, -22, -259.15], + orderedColumns: expect.anything(), + orderedRows: expect.anything(), + pipetteBoundingBoxOffsets: { + backLeftCorner: [-8, -22, -259.15], + frontRightCorner: [-8, -22, -259.15], + }, + } as PipetteV2Specs + expect(getPipetteSpecsV2('p1000_single_flex')).toStrictEqual( + mockP1000Specs + ) + }) + }) + it('returns the correct liquid info for a p50 pipette with default and lowVolume', () => { + const tiprack50uL = 'opentrons/opentrons_flex_96_tiprack_50ul/1' + const mockLiquidDefault = { + $otSharedSchema: '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', + defaultTipOverlapDictionary: { + default: 10.5, + [tiprack50uL]: 10.5, + }, + defaultTipracks: [tiprack50uL], + maxVolume: 50, + minVolume: 5, + supportedTips: { + t50: { + aspirate: { + default: { + 1: expect.anything(), + }, + }, + defaultAspirateFlowRate: { + default: 35, + valuesByApiLevel: { + '2.14': 35, + }, + }, + defaultBlowOutFlowRate: { + default: 57, + valuesByApiLevel: { + '2.14': 57, + }, + }, + defaultDispenseFlowRate: { + default: 57, + valuesByApiLevel: { + '2.14': 57, + }, + }, + defaultFlowAcceleration: 1200, + defaultPushOutVolume: 2, + defaultReturnTipHeight: 0.71, + defaultTipLength: 57.9, + dispense: { + default: { + 1: expect.anything(), + }, + }, + }, + }, + } as PipetteV2LiquidSpecs + const mockLiquidLowVolume = { + $otSharedSchema: '#/pipette/schemas/2/pipetteLiquidPropertiesSchema.json', + defaultTipOverlapDictionary: { + default: 10.5, + [tiprack50uL]: 10.5, + }, + defaultTipracks: [tiprack50uL], + maxVolume: 30, + minVolume: 1, + supportedTips: { + t50: { + aspirate: { + default: { + 1: expect.anything(), + }, + }, + defaultAspirateFlowRate: { + default: 35, + valuesByApiLevel: { + 2.14: 35, + }, + }, + defaultBlowOutFlowRate: { + default: 57, + valuesByApiLevel: { + 2.14: 57, + }, + }, + defaultDispenseFlowRate: { + default: 57, + valuesByApiLevel: { + 2.14: 57, + }, + }, + defaultFlowAcceleration: 1200, + defaultPushOutVolume: 7, + defaultReturnTipHeight: 0.71, + defaultTipLength: 57.9, + dispense: { + default: { + 1: expect.anything(), + }, + }, + }, + }, + } as PipetteV2LiquidSpecs + const mockLiquids: Record = { + default: mockLiquidDefault, + lowVolumeDefault: mockLiquidLowVolume, + } + expect(getPipetteSpecsV2('p50_single_v3.5')?.liquids).toStrictEqual( + mockLiquids + ) + }) }) diff --git a/shared-data/js/pipettes.ts b/shared-data/js/pipettes.ts index 12bc00a6a08..5a9fc1a67c9 100644 --- a/shared-data/js/pipettes.ts +++ b/shared-data/js/pipettes.ts @@ -1,9 +1,38 @@ import pipetteNameSpecs from '../pipette/definitions/1/pipetteNameSpecs.json' import pipetteModelSpecs from '../pipette/definitions/1/pipetteModelSpecs.json' import { OT3_PIPETTES } from './constants' +import type { + PipetteV2Specs, + PipetteV2GeneralSpecs, + PipetteV2GeometrySpecs, + PipetteV2LiquidSpecs, + PipetteNameSpecs, + PipetteModelSpecs, +} from './types' -import type { PipetteNameSpecs, PipetteModelSpecs } from './types' +type GeneralGeometricModules = PipetteV2GeneralSpecs | PipetteV2GeometrySpecs +interface GeneralGeometricSpecs { + default: GeneralGeometricModules +} +interface LiquidSpecs { + default: PipetteV2LiquidSpecs +} + +const generalGeometric: Record< + string, + GeneralGeometricSpecs +> = import.meta.glob('../pipette/definitions/2/*/*/*/*.json', { eager: true }) + +const liquid: Record = import.meta.glob( + '../pipette/definitions/2/liquid/*/*/*/*.json', + { + eager: true, + } +) +type PipChannelString = 'single' | 'multi' | '96' +type Channels = 'eight_channel' | 'single_channel' | 'ninety_six_channel' +type Gen = 'gen1' | 'gen2' | 'gen3' | 'flex' type SortableProps = 'maxVolume' | 'channels' // TODO(mc, 2021-04-30): use these types, pulled directly from the JSON, @@ -89,3 +118,115 @@ export const getIncompatiblePipetteNames = ( } export * from '../pipette/fixtures/name' + +const getChannelsFromString = ( + pipChannelString: PipChannelString +): Channels | null => { + switch (pipChannelString) { + case 'single': { + return 'single_channel' + } + case 'multi': { + return 'eight_channel' + } + case '96': { + return 'ninety_six_channel' + } + default: { + console.error(`invalid number of channels from ${pipChannelString}`) + return null + } + } +} +const getVersionFromGen = (gen: Gen): string | null => { + switch (gen) { + case 'gen1': { + return '1_0' + } + case 'gen2': { + return '2_0' + } + case 'gen3': + case 'flex': { + return '3_0' + } + default: { + return null + } + } +} + +const V2_DEFINITION_TYPES = ['general', 'geometry'] + +/* takes in pipetteName such as 'p300_single' or 'p300_single_gen1' +or PipetteModel such as 'p300_single_v1.3' and converts it to channels, +model, and version in order to return the correct pipette schema v2 json files. +**/ +export const getPipetteSpecsV2 = ( + name: PipetteName | PipetteModel +): PipetteV2Specs | null => { + const nameSplit = name.split('_') + const pipetteModel = nameSplit[0] // ex: p300 + const channels = getChannelsFromString(nameSplit[1] as PipChannelString) // ex: single -> single_channel + const gen = getVersionFromGen(nameSplit[2] as Gen) + + let version: string + // the first 2 conditions are to accommodate version from the pipetteName + if (nameSplit.length === 2) { + version = '1_0' + } else if (gen != null) { + version = gen // ex: gen1 -> 1_0 + // the 'else' is to accommodate the exact version if PipetteModel was added + } else { + const versionNumber = nameSplit[2].split('v')[1] + if (versionNumber.includes('.')) { + version = versionNumber.replace('.', '_') // ex: 1.0 -> 1_0 + } else { + version = `${versionNumber}_0` // ex: 1 -> 1_0 + } + } + + const generalGeometricMatchingJsons = Object.entries(generalGeometric).reduce( + (genericGeometricModules: GeneralGeometricModules[], [path, module]) => { + V2_DEFINITION_TYPES.forEach(type => { + if ( + `../pipette/definitions/2/${type}/${channels}/${pipetteModel}/${version}.json` === + path + ) { + genericGeometricModules.push(module.default) + } + }) + return genericGeometricModules + }, + [] + ) + + const liquidTypes: string[] = [] + const liquidMatchingJsons: { + liquids: Record + } = { liquids: {} } + + Object.entries(liquid).forEach(([path, module]) => { + const type = path.split('/')[7] + // dynamically check the different liquid types and store unique types + // into an array to parse through + if (!liquidTypes.includes(type)) { + liquidTypes.push(type) + } + if ( + `../pipette/definitions/2/liquid/${channels}/${pipetteModel}/${type}/${version}.json` === + path + ) { + const index = liquidTypes.indexOf(type) + const newKeyName = index !== -1 ? liquidTypes[index] : path + liquidMatchingJsons.liquids[newKeyName] = module.default + } + }) + + const pipetteV2Specs: PipetteV2Specs = { + ...Object.assign({}, ...generalGeometricMatchingJsons), + ...liquidMatchingJsons, + } + + return pipetteV2Specs +} diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 82fd65fb87d..45bcee05ff8 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -394,6 +394,124 @@ export interface FlowRateSpec { max: number } +export interface PipetteV2GeneralSpecs { + displayName: string + model: string + displayCategory: PipetteDisplayCategory + pickUpTipConfigurations: { + pressFit: { + speedByTipCount: Record + presses: number + increment: number + distanceByTipCount: Record + currentByTipCount: Record + } + } + dropTipConfigurations: { + plungerEject: { + current: number + speed: number + } + } + plungerMotorConfigurations: { + idle: number + run: number + } + plungerPositionsConfigurations: { + default: { + top: number + bottom: number + blowout: number + drop: number + } + } + availableSensors: { + sensors: string[] + capacitive?: { count: number } + environment?: { count: number } + pressure?: { count: number } + } + partialTipConfigurations: { + partialTipSupported: boolean + availableConfigurations: number[] | null + } + channels: number + shaftDiameter: number + shaftULperMM: number + backCompatNames: string[] + backlashDistance: number + quirks: string[] + plungerHomingConfigurations: { + current: number + speed: number + } +} + +interface NozzleInfo { + key: string + orderedNozzles: string[] +} +export interface PipetteV2GeometrySpecs { + nozzleOffset: number[] + pipetteBoundingBoxOffsets: { + backLeftCorner: number[] + frontRightCorner: number[] + } + pathTo3D: string + orderedRows: Record + orderedColumns: Record + nozzleMap: Record +} + +type TipData = [number, number, number] +interface SupportedTips { + [tipType: string]: { + aspirate: { + default: { + 1: TipData + } + } + defaultAspirateFlowRate: { + default: number + valuesByApiLevel: Record + } + defaultBlowOutFlowRate: { + default: number + valuesByApiLevel: Record + } + defaultDispenseFlowRate: { + default: number + valuesByApiLevel: Record + } + defaultFlowAcceleration: number + defaultPushOutVolume: number + defaultReturnTipHeight: number + defaultTipLength: number + dispense: { + default: { + 1: TipData + } + } + } +} + +export interface PipetteV2LiquidSpecs { + $otSharedSchema: string + supportedTips: SupportedTips + defaultTipOverlapDictionary: Record + maxVolume: number + minVolume: number + defaultTipracks: string[] +} + +export type GenericAndGeometrySpecs = PipetteV2GeneralSpecs & + PipetteV2GeometrySpecs + +export interface PipetteV2Specs extends GenericAndGeometrySpecs { + $otSharedSchema: string + liquids: Record +} + export interface PipetteNameSpecs { name: string displayName: string @@ -475,6 +593,39 @@ export interface AnalysisError { createdAt: string } +interface IntParameter { + min: number + max: number + default: number +} + +interface Choice { + displayName: string + value: unknown +} + +interface ChoiceParameter { + choices: Choice[] + default: string +} + +interface BooleanParameter { + default: boolean +} + +type RunTimeParameterType = 'int' | 'float' | 'str' | 'boolean' + +type ParameterType = IntParameter | ChoiceParameter | BooleanParameter +interface BaseRunTimeParameter { + displayName: string + variableName: string + description: string + type: RunTimeParameterType + suffix?: string +} + +export type RunTimeParameter = BaseRunTimeParameter & ParameterType + // TODO(BC, 10/25/2023): this type (and others in this file) probably belong in api-client, not here export interface CompletedProtocolAnalysis { id: string @@ -487,6 +638,7 @@ export interface CompletedProtocolAnalysis { commands: RunTimeCommand[] errors: AnalysisError[] robotType?: RobotType | null + runTimeParameters?: RunTimeParameter[] } export interface ResourceFile { diff --git a/step-generation/src/__tests__/transfer.test.ts b/step-generation/src/__tests__/transfer.test.ts index eb5f38d40d5..533167ef61e 100644 --- a/step-generation/src/__tests__/transfer.test.ts +++ b/step-generation/src/__tests__/transfer.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/consistent-test-it */ import { beforeEach, describe, it, expect, test } from 'vitest' import { ONE_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, diff --git a/step-generation/src/commandCreators/atomic/disengageMagnet.ts b/step-generation/src/commandCreators/atomic/disengageMagnet.ts index 567fb6d2c29..4a4b56f9587 100644 --- a/step-generation/src/commandCreators/atomic/disengageMagnet.ts +++ b/step-generation/src/commandCreators/atomic/disengageMagnet.ts @@ -13,7 +13,7 @@ export const disengageMagnet: CommandCreator = ( const { module: moduleId } = args const commandType = 'magneticModule/disengage' - if (module === null) { + if (moduleId === null) { return { errors: [errorCreators.missingModuleError()], } diff --git a/step-generation/src/commandCreators/atomic/engageMagnet.ts b/step-generation/src/commandCreators/atomic/engageMagnet.ts index 6d1a0070d14..da5f8af11b7 100644 --- a/step-generation/src/commandCreators/atomic/engageMagnet.ts +++ b/step-generation/src/commandCreators/atomic/engageMagnet.ts @@ -13,7 +13,7 @@ export const engageMagnet: CommandCreator = ( const { module: moduleId, engageHeight } = args const commandType = 'magneticModule/engage' - if (module === null) { + if (moduleId === null) { return { errors: [errorCreators.missingModuleError()], } diff --git a/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts b/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts index 58c9af666b0..24721a2967d 100644 --- a/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts +++ b/step-generation/src/commandCreators/atomic/heaterShakerSetTargetShakeSpeed.ts @@ -10,7 +10,7 @@ export const heaterShakerSetTargetShakeSpeed: CommandCreator ) => { const { moduleId, rpm } = args - if (module === null) { + if (moduleId === null) { return { errors: [errorCreators.missingModuleError()], } diff --git a/step-generation/src/robotStateSelectors.ts b/step-generation/src/robotStateSelectors.ts index 3b13aecf86d..0fcf26e6675 100644 --- a/step-generation/src/robotStateSelectors.ts +++ b/step-generation/src/robotStateSelectors.ts @@ -1,7 +1,6 @@ import assert from 'assert' // TODO: Ian 2019-04-18 move orderWells somewhere more general -- shared-data util? import min from 'lodash/min' -import sortBy from 'lodash/sortBy' import { getTiprackVolume, THERMOCYCLER_MODULE_TYPE, @@ -10,19 +9,40 @@ import { COLUMN, ALL, } from '@opentrons/shared-data' +import { COLUMN_4_SLOTS } from './constants' import type { InvariantContext, ModuleTemporalProperties, RobotState, ThermocyclerModuleState, -} from './' +} from './types' + export function sortLabwareBySlot( labwareState: RobotState['labware'] ): string[] { - return sortBy(Object.keys(labwareState), (id: string) => - parseInt(labwareState[id].slot) + const sortedLabware = Object.keys(labwareState).sort( + (idA: string, idB: string) => { + const slotA = parseInt(labwareState[idA].slot) + const slotB = parseInt(labwareState[idB].slot) + if ( + COLUMN_4_SLOTS.includes(labwareState[idA].slot) && + COLUMN_4_SLOTS.includes(labwareState[idB].slot) + ) { + return idA.localeCompare(idB) + } + if (COLUMN_4_SLOTS.includes(labwareState[idA].slot)) { + return 1 + } + if (COLUMN_4_SLOTS.includes(labwareState[idB].slot)) { + return -1 + } + return slotA - slotB + } ) + + return sortedLabware } + export function _getNextTip(args: { pipetteId: string tiprackId: string diff --git a/yarn.lock b/yarn.lock index 83683ae2007..fe7459d12e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,7 +50,7 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== -"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.20.12", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.3", "@babel/core@^7.23.5", "@babel/core@^7.7.5": +"@babel/core@>=7.2.2", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.20.12", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.3", "@babel/core@^7.23.5": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.0.tgz#56cbda6b185ae9d9bed369816a8f4423c5f2ff1b" integrity sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw== @@ -320,14 +320,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": +"@babel/plugin-syntax-class-properties@^7.12.13": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== @@ -376,7 +369,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": +"@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== @@ -397,7 +390,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== @@ -411,7 +404,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": +"@babel/plugin-syntax-numeric-separator@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== @@ -446,7 +439,7 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" -"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": +"@babel/plugin-syntax-top-level-await@^7.14.5": version "7.14.5" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== @@ -1034,7 +1027,7 @@ dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": +"@babel/template@^7.22.15", "@babel/template@^7.24.0": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" integrity sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA== @@ -1043,7 +1036,7 @@ "@babel/parser" "^7.24.0" "@babel/types" "^7.24.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.24.0", "@babel/traverse@^7.4.5", "@babel/traverse@^7.8.3": +"@babel/traverse@^7.18.9", "@babel/traverse@^7.23.2", "@babel/traverse@^7.24.0", "@babel/traverse@^7.4.5", "@babel/traverse@^7.8.3": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.0.tgz#4a408fbf364ff73135c714a2ab46a5eab2831b1e" integrity sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw== @@ -1059,7 +1052,7 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.24.0", "@babel/types@^7.4.4": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.0.tgz#3b951f435a92e7333eba05b7566fd297960ea1bf" integrity sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w== @@ -1078,14 +1071,6 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@cnakazawa/watch@^1.0.3": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" - integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== - dependencies: - exec-sh "^0.3.2" - minimist "^1.2.0" - "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -2071,115 +2056,6 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== -"@jest/console@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" - integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== - dependencies: - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^26.6.2" - jest-util "^26.6.2" - slash "^3.0.0" - -"@jest/core@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" - integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== - dependencies: - "@jest/console" "^26.6.2" - "@jest/reporters" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-changed-files "^26.6.2" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-resolve-dependencies "^26.6.3" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - jest-watcher "^26.6.2" - micromatch "^4.0.2" - p-each-series "^2.1.0" - rimraf "^3.0.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - -"@jest/environment@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" - integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== - dependencies: - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - jest-mock "^26.6.2" - -"@jest/fake-timers@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" - integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== - dependencies: - "@jest/types" "^26.6.2" - "@sinonjs/fake-timers" "^6.0.1" - "@types/node" "*" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-util "^26.6.2" - -"@jest/globals@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" - integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== - dependencies: - "@jest/environment" "^26.6.2" - "@jest/types" "^26.6.2" - expect "^26.6.2" - -"@jest/reporters@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" - integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.2" - graceful-fs "^4.2.4" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^4.0.3" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.0.2" - jest-haste-map "^26.6.2" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" - slash "^3.0.0" - source-map "^0.6.0" - string-length "^4.0.1" - terminal-link "^2.0.0" - v8-to-istanbul "^7.0.0" - optionalDependencies: - node-notifier "^8.0.0" - "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" @@ -2187,57 +2063,6 @@ dependencies: "@sinclair/typebox" "^0.27.8" -"@jest/source-map@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" - integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== - dependencies: - callsites "^3.0.0" - graceful-fs "^4.2.4" - source-map "^0.6.0" - -"@jest/test-result@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" - integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== - dependencies: - "@jest/console" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-sequencer@^26.6.3": - version "26.6.3" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" - integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== - dependencies: - "@jest/test-result" "^26.6.2" - graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-runner "^26.6.3" - jest-runtime "^26.6.3" - -"@jest/transform@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" - integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== - dependencies: - "@babel/core" "^7.1.0" - "@jest/types" "^26.6.2" - babel-plugin-istanbul "^6.0.0" - chalk "^4.0.0" - convert-source-map "^1.4.0" - fast-json-stable-stringify "^2.0.0" - graceful-fs "^4.2.4" - jest-haste-map "^26.6.2" - jest-regex-util "^26.0.0" - jest-util "^26.6.2" - micromatch "^4.0.2" - pirates "^4.0.1" - slash "^3.0.0" - source-map "^0.6.1" - write-file-atomic "^3.0.0" - "@jest/transform@^29.3.1": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" @@ -2259,17 +2084,6 @@ slash "^3.0.0" write-file-atomic "^4.0.2" -"@jest/types@^26.6.2": - version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" - integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" - "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" @@ -3285,20 +3099,6 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== -"@sinonjs/commons@^1.7.0": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.6.tgz#80c516a4dc264c2a69115e7578d62581ff455ed9" - integrity sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ== - dependencies: - type-detect "4.0.8" - -"@sinonjs/fake-timers@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== - dependencies: - "@sinonjs/commons" "^1.7.0" - "@storybook/addon-actions@7.6.17", "@storybook/addon-actions@^7.6.16": version "7.6.17" resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-7.6.17.tgz#b1be5ab28b22b4a50c6aa0cd0a3671ca5b6f5f71" @@ -3993,7 +3793,7 @@ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== -"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7", "@types/babel__core@^7.18.0", "@types/babel__core@^7.20.4", "@types/babel__core@^7.20.5": +"@types/babel__core@^7.0.0", "@types/babel__core@^7.18.0", "@types/babel__core@^7.20.4", "@types/babel__core@^7.20.5": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== @@ -4019,7 +3819,7 @@ "@babel/parser" "^7.1.0" "@babel/types" "^7.0.0" -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.4", "@types/babel__traverse@^7.0.6", "@types/babel__traverse@^7.18.0": +"@types/babel__traverse@*", "@types/babel__traverse@^7.18.0": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.5.tgz#7b7502be0aa80cc4ef22978846b983edaafcd4dd" integrity sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ== @@ -4178,7 +3978,7 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/graceful-fs@^4.1.2", "@types/graceful-fs@^4.1.3": +"@types/graceful-fs@^4.1.3": version "4.1.9" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== @@ -4227,14 +4027,6 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.20": - version "26.0.24" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" - integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== - dependencies: - jest-diff "^26.0.0" - pretty-format "^26.0.0" - "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -4361,11 +4153,6 @@ "@types/node" "*" xmlbuilder ">=11.0.1" -"@types/prettier@^2.0.0": - version "2.7.3" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" - integrity sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA== - "@types/pretty-hrtime@^1.0.0": version "1.0.3" resolved "https://registry.yarnpkg.com/@types/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#ee1bd8c9f7a01b3445786aad0ef23aba5f511a44" @@ -4557,11 +4344,6 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.8.tgz#518609aefb797da19bf222feb199e8f653ff7627" integrity sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg== -"@types/stack-utils@^2.0.0": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" - integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== - "@types/styled-components@^5.1.26": version "5.1.34" resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.34.tgz#4107df8ef8a7eaba4fa6b05f78f93fba4daf0300" @@ -4654,13 +4436,6 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^15.0.0": - version "15.0.19" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.19.tgz#328fb89e46109ecbdb70c295d96ff2f46dfd01b9" - integrity sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA== - dependencies: - "@types/yargs-parser" "*" - "@types/yauzl@^2.9.1": version "2.10.3" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" @@ -4790,7 +4565,7 @@ "@typescript-eslint/typescript-estree" "6.21.0" semver "^7.5.4" -"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0", "@typescript-eslint/utils@^5.62.0": +"@typescript-eslint/utils@^5.58.0", "@typescript-eslint/utils@^5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== @@ -5270,13 +5045,6 @@ ansi-escapes@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - ansi-html-community@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" @@ -5297,7 +5065,7 @@ ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== -ansi-regex@^5.0.0, ansi-regex@^5.0.1: +ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -5861,20 +5629,6 @@ babel-core@^7.0.0-bridge.0: resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== -babel-jest@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" - integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== - dependencies: - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/babel__core" "^7.1.7" - babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.6.2" - chalk "^4.0.0" - graceful-fs "^4.2.4" - slash "^3.0.0" - babel-loader@^8.2.2: version "8.3.0" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" @@ -5885,7 +5639,7 @@ babel-loader@^8.2.2: make-dir "^3.1.0" schema-utils "^2.6.5" -babel-plugin-istanbul@^6.0.0, babel-plugin-istanbul@^6.1.1: +babel-plugin-istanbul@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== @@ -5896,16 +5650,6 @@ babel-plugin-istanbul@^6.0.0, babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" - integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.0.0" - "@types/babel__traverse" "^7.0.6" - babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -5971,32 +5715,6 @@ babel-plugin-unassert@^3.0.1: resolved "https://registry.yarnpkg.com/babel-plugin-unassert/-/babel-plugin-unassert-3.2.0.tgz#4ea8f65709905cc540627baf4ce4c837281a317d" integrity sha512-dNeuFtaJ1zNDr59r24NjjIm4SsXXm409iNOVMIERp6ePciII+rTrdwsWcHDqDFUKpOoBNT4ZS63nPEbrANW7DQ== -babel-preset-current-node-syntax@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" - integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.8.3" - "@babel/plugin-syntax-import-meta" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - -babel-preset-jest@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" - integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== - dependencies: - babel-plugin-jest-hoist "^26.6.2" - babel-preset-current-node-syntax "^1.0.0" - babel-runtime@6.x.x: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" @@ -6733,11 +6451,6 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - camelize@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" @@ -6758,13 +6471,6 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001587, can resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001593.tgz#7cda1d9e5b0cad6ebab4133b1f239d4ea44fe659" integrity sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ== -capture-exit@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" - integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== - dependencies: - rsvp "^4.8.4" - caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" @@ -6824,11 +6530,6 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - character-entities-html4@^1.0.0: version "1.1.4" resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-1.1.4.tgz#0e64b0a3753ddbf1fdc044c5fd01d0199a02e125" @@ -6962,11 +6663,6 @@ citty@^0.1.5, citty@^0.1.6: dependencies: consola "^3.2.3" -cjs-module-lexer@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" - integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== - class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" @@ -7123,11 +6819,6 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" @@ -7147,11 +6838,6 @@ collapse-white-space@^1.0.0, collapse-white-space@^1.0.2: resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.6.tgz#e63629c0016665792060dbbeb79c42239d2c5287" integrity sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ== -collect-v8-coverage@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" - integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== - collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -7582,7 +7268,7 @@ conventional-commits-parser@^3.2.0: split2 "^3.0.0" through2 "^4.0.0" -convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0: +convert-source-map@^1.5.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== @@ -8569,11 +8255,6 @@ detect-libc@^2.0.1: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d" integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw== -detect-newline@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - detect-node-es@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" @@ -8691,11 +8372,6 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff-sequences@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" - integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== - diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -9205,11 +8881,6 @@ elliptic@^6.5.3, elliptic@^6.5.4: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" -emittery@^0.7.1: - version "0.7.2" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.2.tgz#25595908e13af0f5674ab419396e2fb394cdfa82" - integrity sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ== - emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -9559,11 +9230,6 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1 resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -9663,13 +9329,6 @@ eslint-plugin-import@^2.29.1: semver "^6.3.1" tsconfig-paths "^3.15.0" -eslint-plugin-jest@^27.6.3: - version "27.9.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz#7c98a33605e1d8b8442ace092b60e9919730000b" - integrity sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug== - dependencies: - "@typescript-eslint/utils" "^5.10.0" - eslint-plugin-json@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eslint-plugin-json/-/eslint-plugin-json-3.1.0.tgz#251108ba1681c332e0a442ef9513bd293619de67" @@ -9937,11 +9596,6 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: md5.js "^1.3.4" safe-buffer "^5.1.1" -exec-sh@^0.3.2: - version "0.3.6" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.6.tgz#ff264f9e325519a60cb5e273692943483cca63bc" - integrity sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w== - execa@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.0.0.tgz#7f37d6ec17f09e6b8fc53288611695b6d12b9daf" @@ -9970,7 +9624,7 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.0, execa@^4.0.2: +execa@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== @@ -10034,11 +9688,6 @@ exit-hook@^1.0.0: resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" integrity sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg== -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" @@ -10059,18 +9708,6 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" -expect@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" - integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== - dependencies: - "@jest/types" "^26.6.2" - ansi-styles "^4.0.0" - jest-get-type "^26.3.0" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-regex-util "^26.0.0" - exponential-backoff@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" @@ -10854,7 +10491,7 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" -fsevents@^2.1.2, fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -11149,7 +10786,7 @@ glob@^10.0.0, glob@^10.3.10: minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-scurry "^1.10.1" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.2.0: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -11373,11 +11010,6 @@ graphviz@0.0.9, graphviz@^0.0.9: dependencies: temp "~0.4.0" -growly@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" - integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== - gud@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0" @@ -12122,14 +11754,6 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -12510,11 +12134,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - is-generator-function@^1.0.10, is-generator-function@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" @@ -12913,16 +12532,6 @@ istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0, istanbul-lib-coverag resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== -istanbul-lib-instrument@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz#873c6fff897450118222774696a3f28902d77c1d" - integrity sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ== - dependencies: - "@babel/core" "^7.7.5" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.0.0" - semver "^6.3.0" - istanbul-lib-instrument@^5.0.4: version "5.2.1" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" @@ -12943,7 +12552,7 @@ istanbul-lib-report@^3.0.0, istanbul-lib-report@^3.0.1: make-dir "^4.0.0" supports-color "^7.1.0" -istanbul-lib-source-maps@^4.0.0, istanbul-lib-source-maps@^4.0.1: +istanbul-lib-source-maps@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== @@ -12952,7 +12561,7 @@ istanbul-lib-source-maps@^4.0.0, istanbul-lib-source-maps@^4.0.1: istanbul-lib-coverage "^3.0.0" source-map "^0.6.1" -istanbul-reports@^3.0.2, istanbul-reports@^3.1.6: +istanbul-reports@^3.1.6: version "3.1.7" resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== @@ -12998,137 +12607,6 @@ jake@^10.8.5: filelist "^1.0.4" minimatch "^3.1.2" -jest-changed-files@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" - integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== - dependencies: - "@jest/types" "^26.6.2" - execa "^4.0.0" - throat "^5.0.0" - -jest-cli@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" - integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== - dependencies: - "@jest/core" "^26.6.3" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.4" - import-local "^3.0.2" - is-ci "^2.0.0" - jest-config "^26.6.3" - jest-util "^26.6.2" - jest-validate "^26.6.2" - prompts "^2.0.1" - yargs "^15.4.1" - -jest-config@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" - integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== - dependencies: - "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.6.3" - "@jest/types" "^26.6.2" - babel-jest "^26.6.3" - chalk "^4.0.0" - deepmerge "^4.2.2" - glob "^7.1.1" - graceful-fs "^4.2.4" - jest-environment-jsdom "^26.6.2" - jest-environment-node "^26.6.2" - jest-get-type "^26.3.0" - jest-jasmine2 "^26.6.3" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - micromatch "^4.0.2" - pretty-format "^26.6.2" - -jest-diff@^26.0.0, jest-diff@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" - integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== - dependencies: - chalk "^4.0.0" - diff-sequences "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" - -jest-docblock@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" - integrity sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w== - dependencies: - detect-newline "^3.0.0" - -jest-each@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" - integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== - dependencies: - "@jest/types" "^26.6.2" - chalk "^4.0.0" - jest-get-type "^26.3.0" - jest-util "^26.6.2" - pretty-format "^26.6.2" - -jest-environment-jsdom@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" - integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== - dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" - jsdom "^16.4.0" - -jest-environment-node@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" - integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== - dependencies: - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - jest-mock "^26.6.2" - jest-util "^26.6.2" - -jest-get-type@^26.3.0: - version "26.3.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" - integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== - -jest-haste-map@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" - integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== - dependencies: - "@jest/types" "^26.6.2" - "@types/graceful-fs" "^4.1.2" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.4" - jest-regex-util "^26.0.0" - jest-serializer "^26.6.2" - jest-util "^26.6.2" - jest-worker "^26.6.2" - micromatch "^4.0.2" - sane "^4.0.3" - walker "^1.0.7" - optionalDependencies: - fsevents "^2.1.2" - jest-haste-map@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" @@ -13148,210 +12626,11 @@ jest-haste-map@^29.7.0: optionalDependencies: fsevents "^2.3.2" -jest-jasmine2@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" - integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== - dependencies: - "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - expect "^26.6.2" - is-generator-fn "^2.0.0" - jest-each "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-runtime "^26.6.3" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - pretty-format "^26.6.2" - throat "^5.0.0" - -jest-leak-detector@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" - integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== - dependencies: - jest-get-type "^26.3.0" - pretty-format "^26.6.2" - -jest-matcher-utils@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" - integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== - dependencies: - chalk "^4.0.0" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - pretty-format "^26.6.2" - -jest-message-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" - integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== - dependencies: - "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.6.2" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.4" - micromatch "^4.0.2" - pretty-format "^26.6.2" - slash "^3.0.0" - stack-utils "^2.0.2" - -jest-mock@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" - integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== - dependencies: - "@jest/types" "^26.6.2" - "@types/node" "*" - -jest-pnp-resolver@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== - -jest-regex-util@^26.0.0: - version "26.0.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" - integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== - jest-regex-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== -jest-resolve-dependencies@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" - integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== - dependencies: - "@jest/types" "^26.6.2" - jest-regex-util "^26.0.0" - jest-snapshot "^26.6.2" - -jest-resolve@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" - integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== - dependencies: - "@jest/types" "^26.6.2" - chalk "^4.0.0" - graceful-fs "^4.2.4" - jest-pnp-resolver "^1.2.2" - jest-util "^26.6.2" - read-pkg-up "^7.0.1" - resolve "^1.18.1" - slash "^3.0.0" - -jest-runner@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" - integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== - dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.7.1" - exit "^0.1.2" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-docblock "^26.0.0" - jest-haste-map "^26.6.2" - jest-leak-detector "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" - jest-runtime "^26.6.3" - jest-util "^26.6.2" - jest-worker "^26.6.2" - source-map-support "^0.5.6" - throat "^5.0.0" - -jest-runtime@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" - integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== - dependencies: - "@jest/console" "^26.6.2" - "@jest/environment" "^26.6.2" - "@jest/fake-timers" "^26.6.2" - "@jest/globals" "^26.6.2" - "@jest/source-map" "^26.6.2" - "@jest/test-result" "^26.6.2" - "@jest/transform" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/yargs" "^15.0.0" - chalk "^4.0.0" - cjs-module-lexer "^0.6.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.4" - jest-config "^26.6.3" - jest-haste-map "^26.6.2" - jest-message-util "^26.6.2" - jest-mock "^26.6.2" - jest-regex-util "^26.0.0" - jest-resolve "^26.6.2" - jest-snapshot "^26.6.2" - jest-util "^26.6.2" - jest-validate "^26.6.2" - slash "^3.0.0" - strip-bom "^4.0.0" - yargs "^15.4.1" - -jest-serializer@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" - integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== - dependencies: - "@types/node" "*" - graceful-fs "^4.2.4" - -jest-snapshot@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" - integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== - dependencies: - "@babel/types" "^7.0.0" - "@jest/types" "^26.6.2" - "@types/babel__traverse" "^7.0.4" - "@types/prettier" "^2.0.0" - chalk "^4.0.0" - expect "^26.6.2" - graceful-fs "^4.2.4" - jest-diff "^26.6.2" - jest-get-type "^26.3.0" - jest-haste-map "^26.6.2" - jest-matcher-utils "^26.6.2" - jest-message-util "^26.6.2" - jest-resolve "^26.6.2" - natural-compare "^1.4.0" - pretty-format "^26.6.2" - semver "^7.3.2" - -jest-util@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" - integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== - dependencies: - "@jest/types" "^26.6.2" - "@types/node" "*" - chalk "^4.0.0" - graceful-fs "^4.2.4" - is-ci "^2.0.0" - micromatch "^4.0.2" - jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" @@ -13364,31 +12643,6 @@ jest-util@^29.7.0: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" - integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== - dependencies: - "@jest/types" "^26.6.2" - camelcase "^6.0.0" - chalk "^4.0.0" - jest-get-type "^26.3.0" - leven "^3.1.0" - pretty-format "^26.6.2" - -jest-watcher@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" - integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== - dependencies: - "@jest/test-result" "^26.6.2" - "@jest/types" "^26.6.2" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - jest-util "^26.6.2" - string-length "^4.0.1" - jest-worker@^25.4.0: version "25.5.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.5.0.tgz#2611d071b79cea0f43ee57a3d118593ac1547db1" @@ -13397,7 +12651,7 @@ jest-worker@^25.4.0: merge-stream "^2.0.0" supports-color "^7.0.0" -jest-worker@^26.2.1, jest-worker@^26.6.2: +jest-worker@^26.2.1: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== @@ -13416,15 +12670,6 @@ jest-worker@^29.7.0: merge-stream "^2.0.0" supports-color "^8.0.0" -jest@^26.6.3: - version "26.6.3" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" - integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== - dependencies: - "@jest/core" "^26.6.3" - import-local "^3.0.2" - jest-cli "^26.6.3" - jmespath@0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" @@ -14691,7 +13936,7 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q== -minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -15173,18 +14418,6 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-notifier@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-8.0.2.tgz#f3167a38ef0d2c8a866a83e318c1ba0efeb702c5" - integrity sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg== - dependencies: - growly "^1.3.0" - is-wsl "^2.2.0" - semver "^7.3.2" - shellwords "^0.1.1" - uuid "^8.3.0" - which "^2.0.2" - node-releases@^2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" @@ -15635,11 +14868,6 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== -p-each-series@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" - integrity sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA== - p-event@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/p-event/-/p-event-2.3.1.tgz#596279ef169ab2c3e0cae88c1cfbb08079993ef6" @@ -16036,7 +15264,7 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== -pirates@^4.0.1, pirates@^4.0.4, pirates@^4.0.6: +pirates@^4.0.4, pirates@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== @@ -16048,7 +15276,7 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" -pkg-dir@^4.1.0, pkg-dir@^4.2.0: +pkg-dir@^4.1.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== @@ -16907,16 +16135,6 @@ pretty-error@^2.0.2: lodash "^4.17.20" renderkid "^2.0.4" -pretty-format@^26.0.0, pretty-format@^26.6.2: - version "26.6.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" - integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== - dependencies: - "@jest/types" "^26.6.2" - ansi-regex "^5.0.0" - ansi-styles "^4.0.0" - react-is "^17.0.1" - pretty-format@^27.0.2: version "27.5.1" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" @@ -16982,7 +16200,7 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prompts@^2.0.1, prompts@^2.4.0: +prompts@^2.4.0: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== @@ -18185,13 +17403,6 @@ resolve-cwd@^2.0.0: dependencies: resolve-from "^3.0.0" -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - resolve-dependency-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-dependency-path/-/resolve-dependency-path-2.0.0.tgz#11700e340717b865d216c66cabeb4a2a3c696736" @@ -18240,7 +17451,7 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== -resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.2, resolve@^1.22.4: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.22.1, resolve@^1.22.2, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -18338,7 +17549,7 @@ rimraf@2.6.3, rimraf@~2.6.2: dependencies: glob "^7.1.3" -rimraf@3.0.2, rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@3.0.2, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -18423,11 +17634,6 @@ rollup@^4.2.0: "@rollup/rollup-win32-x64-msvc" "4.12.0" fsevents "~2.3.2" -rsvp@^4.8.4: - version "4.8.5" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" - integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -18502,21 +17708,6 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" - integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== - dependencies: - "@cnakazawa/watch" "^1.0.3" - anymatch "^2.0.0" - capture-exit "^2.0.0" - exec-sh "^0.3.2" - execa "^1.0.0" - fb-watchman "^2.0.0" - micromatch "^3.1.4" - minimist "^1.1.1" - walker "~1.0.5" - sanitize-filename@^1.6.3: version "1.6.3" resolved "https://registry.yarnpkg.com/sanitize-filename/-/sanitize-filename-1.6.3.tgz#755ebd752045931977e30b2025d340d7c9090378" @@ -18886,11 +18077,6 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -shellwords@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" - integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== - shx@^0.3.3: version "0.3.4" resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" @@ -19122,11 +18308,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== -source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - sourcemap-codec@^1.4.8: version "1.4.8" resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" @@ -19283,13 +18464,6 @@ stack-trace@0.0.x: resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg== -stack-utils@^2.0.2: - version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" - integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== - dependencies: - escape-string-regexp "^2.0.0" - stackback@0.0.2: version "0.0.2" resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" @@ -19421,14 +18595,6 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -string-length@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - "string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -19604,11 +18770,6 @@ strip-bom@^3.0.0: resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - strip-dirs@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" @@ -19853,14 +19014,6 @@ supports-color@^8.0.0, supports-color@^8.1.1: dependencies: has-flag "^4.0.0" -supports-hyperlinks@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz#3943544347c1ff90b15effb03fc14ae45ec10624" - integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== - dependencies: - has-flag "^4.0.0" - supports-color "^7.0.0" - supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" @@ -20009,14 +19162,6 @@ tempy@1.0.1, tempy@^1.0.1: type-fest "^0.16.0" unique-string "^2.0.0" -terminal-link@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" - integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== - dependencies: - ansi-escapes "^4.2.1" - supports-hyperlinks "^2.0.0" - terser-webpack-plugin@^1.4.3: version "1.4.5" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" @@ -20090,11 +19235,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -throat@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" - integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== - throttleit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5" @@ -20416,7 +19556,7 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: +type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -20441,11 +19581,6 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -21092,7 +20227,7 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0, uuid@^8.3.2: +uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -21107,15 +20242,6 @@ v8-compile-cache@^2.1.0, v8-compile-cache@^2.1.1: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== -v8-to-istanbul@^7.0.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1" - integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow== - dependencies: - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - source-map "^0.7.3" - v8-to-istanbul@^9.2.0: version "9.2.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz#2ed7644a245cddd83d4e087b9b33b3e62dfd10ad" @@ -21377,7 +20503,7 @@ walkdir@^0.4.1: resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== -walker@^1.0.7, walker@^1.0.8, walker@~1.0.5: +walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== @@ -22025,23 +21151,6 @@ yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^15.4.1: - version "15.4.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" - integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== - dependencies: - cliui "^6.0.0" - decamelize "^1.2.0" - find-up "^4.1.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^4.2.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^18.1.2" - yargs@^16.2.0: version "16.2.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"