Reimplement GWT assay plate designer in React#7623
Reimplement GWT assay plate designer in React#7623labkey-jeckels wants to merge 14 commits intodevelopfrom
Conversation
|
👍 |
labkey-alan
left a comment
There was a problem hiding this comment.
I did not do the most thorough review on the React code, since I was only asked to take a cursory look at the code, and the GitHub outage is preventing me from viewing all of the files at this time. I do have two pieces of feedback though:
- Many of the components have really large chunks of TSX code (e.g. GroupTypesPanel). Lots of nested loops and conditional statements. This is a readability mess, and it also makes it significantly harder to test, since we can't test in smaller units of functionality. My recommendation is to split these large chunks of TSX into smaller components.
- There are zero unit tests. For a set of components this size, and this complex, we really should have as much unit test coverage as is reasonable.
Additionally I do have a concern about introducing this code more generally. While I understand the motivation to get rid of the last GWT component, and I would love to see us accomplish that goal, I am not sure that this is the best path forward. We already have a lot of plate related code in our apps, and while this code helps us get rid of GWT, it does not change the fact that we have two different implementations of a UI that is meant to accomplish the same thing. I think we would benefit most from having one UI for our plate code, not two different implementations that are both made with React.
I can take a more thorough look at the code later if wanted.
|
|
|
Manual testing looks mostly good. Just some problems with the buttons at the top.
|
Save & Close has typically been enabled even when the page isn't dirty. That's retained from GWT. Other button issues should be fixed - I matched the normal SDMS styling. |
|
The button behavior doesn't seem to have changed at all. The "Save & Close" and "Cancel" buttons still alert you about unsaved changes. |
Strange, I don't know why I couldn't repro the Save & Close before. I removed the prompt from both buttons. The GWT designer confirmed on Cancel from a dirty page, but from spot checking our other React UIs we aren't doing it elsewhere. |
labkey-alan
left a comment
There was a problem hiding this comment.
I've left some feedback. Overall it looks pretty good, but there are several changes that I think we should make.
One general piece of feedback is that function components should be typed as FC, and they should all have their displayName set. Most (all?) components are defined as function MyComponent(props: MyPropsType): JSX.Element, which is not a pattern we use at LabKey, and I think is worse than the const MyComponent: FC<MyPropsType> = () => {} pattern.
Another piece of feedback is that there is very little consistency in how we handle Events. Sometimes useCallback is used, but even then sometimes it's used in a way that doesn't matter (it's wrapped in a function that is regenerated on every render). Other times we create named functions for event handlers, but they're not memoized with useCallback. Often we just have inline event handlers. While techincally speaking you don't need to memoize callbacks handed to native DOM elements, we tend to do that at LabKey. The consistency is nice, but also because refactors or new features happen reasonably often it tends to be safer to memoize it today, because tomorrow's usage may benefit from it. There's very little overhead to useCallback, and we've seen real performance issues from omitting it.
| } | ||
|
|
||
| /** | ||
| * Left-hand panel that manages group types (tabs) and individual well groups. |
There was a problem hiding this comment.
This giant block of comments feels unnecessary. I'm sure it was useful for Claude to figure things out, but I am not sure we want a giant wall of text like this in the source file moving forward.
| panelId={`group-panel-${type}`} | ||
| isActive={type === activeTab} | ||
| baseClass="group-types-panel__tab" | ||
| onClick={() => onTabChange(type)} |
There was a problem hiding this comment.
The TabButton component here will re-render on every render cycle because we are always passing a fresh onClick function to it. Instead, you should pass onTabChange and type as props, then in TabButton you should define onClick as const onClick = useCallback(() => onTabChange(type), [onTabChange, type]). Then, React can skip rendering these TabButton components unless onTabChange or type changes, which is unlikely.
| <div | ||
| className="group-types-panel__tabs" | ||
| role="tablist" | ||
| onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => { |
There was a problem hiding this comment.
This component is incredibly confusing because it defines quite a few handlers above via useCallback, which is what I would recommend, but then in the TSX here and below we define a lot of inline event handlers. Generally speaking I recommend defining the event handlers above, with named functions. This accomplishes two things: There's less stuff in your TSX, and your functions are named, which can help increase readability.
| const isHighlighted = hoveredWellGroupId === group.rowId; | ||
| const isRenaming = renamingId === group.rowId; | ||
| return ( | ||
| <React.Fragment key={group.rowId}> |
There was a problem hiding this comment.
This whole React.Fragment smells like a component to me.
|
|
||
| test('shows a text <input> when no defaults are configured for the type', () => { | ||
| renderPanel({ plate: makePlate({ typesToDefaultGroups: {} }) }); | ||
| expect(screen.getByRole('textbox', { name: 'Group name' })).toBeInTheDocument(); |
There was a problem hiding this comment.
I feel like we should be asserting the combobox to be null here like we did in the other similar case.
| expect(screen.getByRole('textbox', { name: 'Group name' })).toBeInTheDocument(); | |
| expect(screen.getByRole('textbox', { name: 'Group name' })).toBeInTheDocument(); | |
| expect(screen.queryByRole('combobox', { name: 'Group name' })).toBeNull(); |
| export function WarningPanel({ warnings }: WarningPanelProps): JSX.Element { | ||
| return ( | ||
| <div className="warning-panel"> | ||
| {warnings.length === 0 ? ( | ||
| <div className="warning-panel__none">No warnings.</div> | ||
| ) : ( | ||
| <ul className="warning-panel__list"> | ||
| {warnings.map((w) => ( | ||
| <li key={w} className="warning-panel__item">{w}</li> | ||
| ))} | ||
| </ul> | ||
| )} | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
| export function WarningPanel({ warnings }: WarningPanelProps): JSX.Element { | |
| return ( | |
| <div className="warning-panel"> | |
| {warnings.length === 0 ? ( | |
| <div className="warning-panel__none">No warnings.</div> | |
| ) : ( | |
| <ul className="warning-panel__list"> | |
| {warnings.map((w) => ( | |
| <li key={w} className="warning-panel__item">{w}</li> | |
| ))} | |
| </ul> | |
| )} | |
| </div> | |
| ); | |
| } | |
| export const WarningPanel: FC<WarningPanelProps> = ({ warnings }) => ( | |
| <div className="warning-panel"> | |
| {warnings.length === 0 ? ( | |
| <div className="warning-panel__none">No warnings.</div> | |
| ) : ( | |
| <ul className="warning-panel__list"> | |
| {warnings.map((w) => ( | |
| <li key={w} className="warning-panel__item">{w}</li> | |
| ))} | |
| </ul> | |
| )} | |
| </div> | |
| ); | |
| WarningPanel.displayName = 'WarningPanel'; |
|
|
||
| /* | ||
| * ────────────────────────────────────────────────────────────────────────────── | ||
| * Overall layout (vertical stack): |
There was a problem hiding this comment.
This comment is unnecessary.
| /** | ||
| * Root component of the Plate Template Designer. | ||
| * | ||
| * ─── User workflow ────────────────────────────────────────────────────────────── |
There was a problem hiding this comment.
I'm not sure this comment is necessary.
| </div> | ||
| </GroupTypesPanel> | ||
| </div> | ||
| {/* Right panel: WellGroupProperties and (if enabled) a Warnings tab. |
There was a problem hiding this comment.
This comment feels unnecessary.
| className="plate-template-designer__name-input" | ||
| type="text" | ||
| value={plate.name} | ||
| onChange={e => handleNameChange(e.target.value)} |
There was a problem hiding this comment.
Instead of wrapping handleNameChange inline here I would make handleNameChange handle the event. Then you can just pass onChange={handleNameChange}




Rationale
Our assay plate designer is our final GWT UI. This replaces it with a React-based version.
Related Pull Requests
Changes
Tasks 📍