Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(app): Refactor LPC flows in preparation for LPC redesign #17279

Open
wants to merge 28 commits into
base: edge
Choose a base branch
from

Conversation

mjhuff
Copy link
Contributor

@mjhuff mjhuff commented Jan 15, 2025

Closes EXEC-1070

Overview

This PR effectively rewrites LabwarePositionCheck, separating the data layer from the presentation layer and re-architecting both layers to be more in line with the direction Design is heading with the LPC Redesign.

Some Disclaimers

First and foremost, this PR intentionally separates the OT-2 from the Flex LPC flows, and it effectively does not touch the OT-2 flows. All changes to LPC are made for the Flex flows entirely. There is one very very small change to the Legacy flows, see "Risk assessment".

Note that no functional changes are intended to be introduced, but in practice, a few bugs I noticed in the old code were fixed along the way as a side effect of rewriting the code. Also, this PR is not meant to polish all the existing LPC logic or be a final architectural product. Some old decisions are kept that should definitely change (ex, the role of steps in the old/this version of LPC), but it's probably not worth investing the time until designs are more concrete. As a last note, there are intentionally no tests (yet!!), because the presentation layer is very likely to change, and I'm not yet convinced the data layer is solidified enough to add testing. It's a lot of time to invest for little payoff.

High Level Overview of Changes

To best reason about the new LPC, it's probably best to start with LPCFlows, the touch point for of the new LPC API. Below are the high-level, TLDR major changes followed by some additional explanation for the more salient changes/thoughts.

On a high-level, here are the major ways the new code differs from the old code:

  • LPC is split into the OT-2 flows, LegacyLabwarePositionCheck, and the Flex flows, LabwarePositionCheck. Effectively no changes are made to the OT-2 flows.

  • All state and commands are injected into LPC components.

  • Almost all state, including all initial state, is contained in Redux.

  • We get labware definitions from an HTTP resource as opposed to scanning commands and building it ourselves.

  • Protocol command scanning is eliminated except for one instance.

  • Component render control flow is consolidated, and no component returns null now.

  • Each LPC step view renders what's on the tin. There's no more conditional rendering of different views in each step.

  • N A M E S P A C I N G.

  • Increased encapsulation around components.

  • An introduction of encapsulation for commands and module commands. This seems important, because we need a clear pattern when it comes to supporting new modules in LPC.

  • Lots of removed cruft. No more unused variables, local utilities that duplicate utilities in local-resources, redeclaration of variables/subroutines for functionality that exists elsewhere, and miscellaneous duplicated logic.

  • Error handling is consolidated.

  • Jogging is now debounced! No more breaking labware by overjogging!

  • More comments on things that I felt needed comments.

  • Some React Fiber/general rendering optimizations.

  • Eliminated all robot commands that could occur as side effects.

  • A couple bug fixes:

  1. Not attaching your probe when you said you attached it doesn't cause LPC to sometimes fail anymore.
  2. The old logic some assumptions about the state of robot homing, and when performing certain command sequences, it was possible to error out of LPC.

Some Things Worth Mentioning in More Detail

Everything Goes into Redux

One of the nice and also not nice things about React is that it's very unopinionated, especially around issues of state management. We have Redux in the app, we also have React context, we have local useState, we have prop drilling, we have hooks, and we have hook wrappers around React Query. While there are some quasi-objective instances in which one state management solution is more correct than another, that isn't always the case.

One thing that worked well in Error Recovery and Drop Tip wizard was the use of a single object to pass around state. However, there's a good argument that if you probably want Redux for some of that state, should all your state go into Redux? There's arguments on both sides, but I think doing so for LPC does make working the API a bit simpler (see next paragraph), and keeping things memoized becomes the job of a third party library instead of needing to roll our own solution (and potentially introduce odd bugs because of dependency lists including/missing certain things). This PR does not add the LPC store to the app Redux store, but it could, see review comments.

The major advantage of using the store will become more apparent with the redesign: it will be simpler to promote labware to first-class citizens, storing, updating, and deriving all sorts of data associated with a specific labware URI. Additionally, debugging will be made significantly easier.

No Commands Emitted as Side-Effects

Commands emitted as side effects often lead to unexpected interactions, especially once flows start to get more complicated. When looking at other wizard flows, the ones that execute commands purely through CTAs lead to less unexpected behavior, especially as the flows evolve, since keeping on top of interactions is much more straightforward. This does lead to the commands API being a bit more complex than the alternative (state driven conditional logic is more of the responsibility of the commands themselves than the presentation layer), but I think the benefits are worth it.

Steps are Part of the Old Data Model (and Make the Refactor Awkward)

It seems that the original intention of steps was to act as a top-level shared prop object, but it's at odds with the new approach to prop drilling. Steps treats the "step" as the first-class citizen, which conflicts with where we want to end up by the end of the redesign, with "labware" as the first class citizen. It would be a serious functional refactor (it's probably the most important change of the redesign from a FE perspective) to include it in this PR, so it's excluded.

All of this is to say that there's a lot of weird step injection, especially into commands, that feels awkward because it is. It's where the old LPC data model meets the new one, and a lot of this will feel much better when labware becomes the first-class citizen.

Test Plan and Hands on Testing

  • Smoke tested LPC on the desktop app & ODD, verifying behavior is consistent with the old flows. LPC data is still persisted, and the command flow is identical to the old flow.
  • Verified no crazy HTTP requests/failures/etc.

Review requests

  • How do we feel about the "throw everything into Redux approach"?
  • Should we make this a part of the app global Redux store? I didn't for historical reasons (we don't do this with Quick Transfer), and I don't know if doing so is just somewhat confusing given how/when we would access this slice of the global app store.

Risk assessment

very very low - see the diff for files in LegacyLPCFlows. It's just marking a couple props as optional, so the adapter that injects necessary props in the modern LPC flows component works.

Move redux out of the presentation layer and introduce more of the commonly used boilerplate. The
intention is to shift more and more state into redux.
A first rough pass at splitting data from presentation. This requires a good bit of typing clean up
as well. A couple params from legacy lpc flows are made optional.
…lows

Because the Flex/OT-2 flows are controlled outside of the LPC presentation layer, we don't need any
of the conditional logic here.
TerseOffsetTable is used in other places in the app outside of LPC, so it should be an organism.
We already have a global util that does the same thing.
We iterate over the same command set a lot to get labware definitions. We can just do this once.
About half the commands were injected into the presentation and half of them were not, so now we
inject all of them. All commands are injected via a singular hook, useLPCCommands. There's a good
bit of cleanup still left to do, but this commit is the first step.
Why do many when one does the trick?
The commands utilized by the LPC handlers are largely shared and should be consolidated and given
structure so it's easier to reason about them, since we'll continually need to support new modules
in LPC. This commit inadvertently fixes a bug in which LPC assumes certain axes were homed before
they may actually be in practice.
Some LPC steps contained pseudo components that really should be their own components. This helps
React Fiber properly optimize renders, too.
A couple components are now not shared in order to keep render control flow simpler. This just moves
them out of the shared dir and cleans up a bit of their CSS styling in the process.
There are no longer any base components that are shared between steps, so we can nix the directory.
General utils are no longer generally used and should be relocated appropriately for better
namespacing.
We don't actually use tip pickup offset data in the Flex LPC flows, since we use a probe (and don't
pick up tips).
Doing so gives us more granular control over when to show robot in motion views.
@mjhuff mjhuff requested a review from smb2268 January 15, 2025 18:40
@mjhuff mjhuff requested a review from a team as a code owner January 15, 2025 18:40
@mjhuff mjhuff force-pushed the app_split-data-presentation-lpc branch from f413fb5 to 47e1ac7 Compare January 15, 2025 18:41
@mjhuff mjhuff force-pushed the app_split-data-presentation-lpc branch from 47e1ac7 to 3ee2308 Compare January 15, 2025 18:42
Copy link
Member

@sfoster1 sfoster1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still reviewing, IMO definitely put the redux state in the global store though. Makes it much easier to look for.

@@ -77,7 +75,7 @@ export function ProtocolSetupOffsets({
const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets)
return (
<>
{isNewLpc ? null : LPCWizard}
{LPCWizard}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these seem like a weird couple lines, can we collapse this into either no logic or explicit alternation logic, like {LPCWizard ?? (...)}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, duh

export interface LPCWizardFlexProps extends Omit<LPCFlowsProps, 'robotType'> {}

export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element {
const initialState = useLPCInitialState({ ...props })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the debuggability that we're using redux for, IMO this really wants to go in the global store keyed by run id or something. Having this owned by the react component itself makes it a lot less useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants