Skip to content

Commit 87bf426

Browse files
authored
fix(app): show InfoScreen under RunPreview for a run canceled before start (#15179)
Show a 'Run was never started' InfoScreen under RunPreview for a terminal run that was never started (has 0 commands). Ensure we only fetch once to eliminate flickering. Closes RQA-2717
1 parent ff46f3a commit 87bf426

File tree

3 files changed

+141
-102
lines changed

3 files changed

+141
-102
lines changed

app/src/assets/localization/en/run_details.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"load_module_protocol_setup": "Load {{module}} in Slot {{slot_name}}",
6060
"load_pipette_protocol_setup": "Load {{pipette_name}} in {{mount_name}} Mount",
6161
"loading_protocol": "Loading Protocol",
62+
"loading_data": "Loading data...",
6263
"location": "location",
6364
"module_controls": "Module Controls",
6465
"module_slot_number": "Slot {{slot_number}}",

app/src/organisms/RunPreview/index.tsx

Lines changed: 119 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
DISPLAY_FLEX,
1313
DISPLAY_NONE,
1414
Flex,
15+
InfoScreen,
1516
POSITION_FIXED,
1617
PrimaryButton,
1718
SPACING,
@@ -58,17 +59,19 @@ export const RunPreviewComponent = (
5859
? (RUN_STATUSES_TERMINAL as RunStatus[]).includes(runStatus)
5960
: false
6061
// we only ever want one request done for terminal runs because this is a heavy request
61-
const commandsFromQuery = useNotifyAllCommandsAsPreSerializedList(
62+
const {
63+
data: commandsFromQueryResponse,
64+
isLoading: isRunCommandDataLoading,
65+
} = useNotifyAllCommandsAsPreSerializedList(
6266
runId,
6367
{ cursor: 0, pageLength: MAX_COMMANDS },
6468
{
6569
staleTime: Infinity,
6670
cacheTime: Infinity,
6771
enabled: isRunTerminal,
6872
}
69-
).data?.data
70-
const nullCheckedCommandsFromQuery =
71-
commandsFromQuery == null ? robotSideAnalysis?.commands : commandsFromQuery
73+
)
74+
const commandsFromQuery = commandsFromQueryResponse?.data
7275
const viewPortRef = React.useRef<HTMLDivElement | null>(null)
7376
const currentRunCommandKey = useNotifyLastRunCommand(runId, {
7477
refetchInterval: LIVE_RUN_COMMANDS_POLL_MS,
@@ -78,10 +81,9 @@ export const RunPreviewComponent = (
7881
setIsCurrentCommandVisible,
7982
] = React.useState<boolean>(true)
8083
if (robotSideAnalysis == null) return null
81-
const commands =
82-
(isRunTerminal
83-
? nullCheckedCommandsFromQuery
84-
: robotSideAnalysis.commands) ?? []
84+
const commands = isRunTerminal
85+
? commandsFromQuery
86+
: robotSideAnalysis.commands
8587
// pass relevant data from run rather than analysis so that CommandText utilities can properly hash the entities' IDs
8688
// TODO (nd:05/02/2024, AUTH-380): update name and types for CommandText (and children/utilities) use of analysis.
8789
// We should ideally pass only subset of analysis/run data required by these children and utilities
@@ -93,14 +95,28 @@ export const RunPreviewComponent = (
9395
modules: runRecord.data.modules ?? [],
9496
pipettes: runRecord.data.pipettes ?? [],
9597
liquids: runRecord.data.liquids ?? [],
96-
commands: commands,
98+
commands: commands ?? [],
9799
}
98100
: robotSideAnalysis
99-
const currentRunCommandIndex = commands.findIndex(
100-
c => c.key === currentRunCommandKey
101-
)
101+
const currentRunCommandIndex =
102+
commands != null
103+
? commands.findIndex(c => c.key === currentRunCommandKey)
104+
: 0
102105

103-
return (
106+
if (isRunCommandDataLoading || commands == null) {
107+
return (
108+
<Flex flexDirection={DIRECTION_COLUMN} padding={SPACING.spacing16}>
109+
<StyledText alignSelf={ALIGN_CENTER} color={COLORS.grey50}>
110+
{t('protocol_setup:loading_data')}
111+
</StyledText>
112+
</Flex>
113+
)
114+
}
115+
return commands.length === 0 ? (
116+
<Flex flexDirection={DIRECTION_COLUMN} padding={SPACING.spacing16}>
117+
<InfoScreen contentType="runNotStarted" />
118+
</Flex>
119+
) : (
104120
<Flex
105121
ref={viewPortRef}
106122
flexDirection={DIRECTION_COLUMN}
@@ -110,99 +126,101 @@ export const RunPreviewComponent = (
110126
gridGap={SPACING.spacing8}
111127
padding={SPACING.spacing16}
112128
>
113-
<Flex gridGap={SPACING.spacing8} alignItems={ALIGN_CENTER}>
114-
<StyledText as="h3" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
115-
{t('run_preview')}
116-
</StyledText>
117-
<StyledText as="label" color={COLORS.grey50}>
118-
{t('steps_total', { count: commands.length })}
129+
<>
130+
<Flex gridGap={SPACING.spacing8} alignItems={ALIGN_CENTER}>
131+
<StyledText as="h3" fontWeight={TYPOGRAPHY.fontWeightSemiBold}>
132+
{t('run_preview')}
133+
</StyledText>
134+
<StyledText as="label" color={COLORS.grey50}>
135+
{t('steps_total', { count: commands.length })}
136+
</StyledText>
137+
</Flex>
138+
<StyledText as="p" marginBottom={SPACING.spacing8}>
139+
{t('preview_of_protocol_steps')}
119140
</StyledText>
120-
</Flex>
121-
<StyledText as="p" marginBottom={SPACING.spacing8}>
122-
{t('preview_of_protocol_steps')}
123-
</StyledText>
124-
<Divider marginX={`calc(-1 * ${SPACING.spacing16})`} />
125-
<ViewportList
126-
viewportRef={viewPortRef}
127-
ref={ref}
128-
items={commands}
129-
onViewportIndexesChange={([
130-
lowestVisibleIndex,
131-
highestVisibleIndex,
132-
]) => {
133-
if (currentRunCommandIndex >= 0) {
134-
setIsCurrentCommandVisible(
135-
currentRunCommandIndex >= lowestVisibleIndex &&
136-
currentRunCommandIndex <= highestVisibleIndex
137-
)
138-
}
139-
}}
140-
initialIndex={currentRunCommandIndex}
141-
>
142-
{(command, index) => {
143-
const isCurrent = index === currentRunCommandIndex
144-
const backgroundColor = isCurrent ? COLORS.blue30 : COLORS.grey20
145-
const iconColor = isCurrent ? COLORS.blue60 : COLORS.grey50
146-
return (
147-
<Flex
148-
key={command.id}
149-
alignItems={ALIGN_CENTER}
150-
gridGap={SPACING.spacing8}
151-
>
152-
<StyledText
153-
minWidth={SPACING.spacing16}
154-
fontSize={TYPOGRAPHY.fontSizeCaption}
155-
>
156-
{index + 1}
157-
</StyledText>
141+
<Divider marginX={`calc(-1 * ${SPACING.spacing16})`} />
142+
<ViewportList
143+
viewportRef={viewPortRef}
144+
ref={ref}
145+
items={commands}
146+
onViewportIndexesChange={([
147+
lowestVisibleIndex,
148+
highestVisibleIndex,
149+
]) => {
150+
if (currentRunCommandIndex >= 0) {
151+
setIsCurrentCommandVisible(
152+
currentRunCommandIndex >= lowestVisibleIndex &&
153+
currentRunCommandIndex <= highestVisibleIndex
154+
)
155+
}
156+
}}
157+
initialIndex={currentRunCommandIndex}
158+
>
159+
{(command, index) => {
160+
const isCurrent = index === currentRunCommandIndex
161+
const backgroundColor = isCurrent ? COLORS.blue30 : COLORS.grey20
162+
const iconColor = isCurrent ? COLORS.blue60 : COLORS.grey50
163+
return (
158164
<Flex
159-
flexDirection={DIRECTION_COLUMN}
160-
gridGap={SPACING.spacing4}
161-
width="100%"
162-
backgroundColor={
163-
index === jumpedIndex ? '#F5E3FF' : backgroundColor
164-
}
165-
color={COLORS.black90}
166-
borderRadius={BORDERS.borderRadius4}
167-
padding={SPACING.spacing8}
168-
css={css`
169-
transition: background-color ${COLOR_FADE_MS}ms ease-out,
170-
border-color ${COLOR_FADE_MS}ms ease-out;
171-
`}
165+
key={command.id}
166+
alignItems={ALIGN_CENTER}
167+
gridGap={SPACING.spacing8}
172168
>
173-
<Flex alignItems={ALIGN_CENTER} gridGap={SPACING.spacing8}>
174-
<CommandIcon command={command} color={iconColor} />
175-
<CommandText
176-
command={command}
177-
robotSideAnalysis={protocolDataFromAnalysisOrRun}
178-
robotType={robotType}
179-
color={COLORS.black90}
180-
/>
169+
<StyledText
170+
minWidth={SPACING.spacing16}
171+
fontSize={TYPOGRAPHY.fontSizeCaption}
172+
>
173+
{index + 1}
174+
</StyledText>
175+
<Flex
176+
flexDirection={DIRECTION_COLUMN}
177+
gridGap={SPACING.spacing4}
178+
width="100%"
179+
backgroundColor={
180+
index === jumpedIndex ? '#F5E3FF' : backgroundColor
181+
}
182+
color={COLORS.black90}
183+
borderRadius={BORDERS.borderRadius4}
184+
padding={SPACING.spacing8}
185+
css={css`
186+
transition: background-color ${COLOR_FADE_MS}ms ease-out,
187+
border-color ${COLOR_FADE_MS}ms ease-out;
188+
`}
189+
>
190+
<Flex alignItems={ALIGN_CENTER} gridGap={SPACING.spacing8}>
191+
<CommandIcon command={command} color={iconColor} />
192+
<CommandText
193+
command={command}
194+
robotSideAnalysis={protocolDataFromAnalysisOrRun}
195+
robotType={robotType}
196+
color={COLORS.black90}
197+
/>
198+
</Flex>
181199
</Flex>
182200
</Flex>
183-
</Flex>
184-
)
185-
}}
186-
</ViewportList>
187-
{currentRunCommandIndex >= 0 ? (
188-
<PrimaryButton
189-
position={POSITION_FIXED}
190-
bottom={SPACING.spacing40}
191-
left={`calc(calc(100% + ${NAV_BAR_WIDTH})/2)`} // add width of half of nav bar to center within run tab
192-
transform="translate(-50%)"
193-
borderRadius={SPACING.spacing32}
194-
display={isCurrentCommandVisible ? DISPLAY_NONE : DISPLAY_FLEX}
195-
onClick={makeHandleScrollToStep(currentRunCommandIndex)}
196-
id="RunLog_jumpToCurrentStep"
197-
>
198-
{t('view_current_step')}
199-
</PrimaryButton>
200-
) : null}
201-
{currentRunCommandIndex === commands.length - 1 ? (
202-
<StyledText as="h6" color={COLORS.grey60}>
203-
{t('end_of_protocol')}
204-
</StyledText>
205-
) : null}
201+
)
202+
}}
203+
</ViewportList>
204+
{currentRunCommandIndex >= 0 ? (
205+
<PrimaryButton
206+
position={POSITION_FIXED}
207+
bottom={SPACING.spacing40}
208+
left={`calc(calc(100% + ${NAV_BAR_WIDTH})/2)`} // add width of half of nav bar to center within run tab
209+
transform="translate(-50%)"
210+
borderRadius={SPACING.spacing32}
211+
display={isCurrentCommandVisible ? DISPLAY_NONE : DISPLAY_FLEX}
212+
onClick={makeHandleScrollToStep(currentRunCommandIndex)}
213+
id="RunLog_jumpToCurrentStep"
214+
>
215+
{t('view_current_step')}
216+
</PrimaryButton>
217+
) : null}
218+
{currentRunCommandIndex === commands.length - 1 ? (
219+
<StyledText as="h6" color={COLORS.grey60}>
220+
{t('end_of_protocol')}
221+
</StyledText>
222+
) : null}
223+
</>
206224
</Flex>
207225
)
208226
}

react-api-client/src/runs/useAllCommandsAsPreSerializedList.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,28 @@ export function useAllCommandsAsPreSerializedList<TError = Error>(
2828
enabled: host !== null && runId != null && options.enabled !== false,
2929
}
3030
const { cursor, pageLength } = nullCheckedParams
31+
// reduce hostKey into a new object to make nullish values play nicely with react-query key hash
32+
const hostKey =
33+
host != null
34+
? Object.entries(host).reduce<Object>((acc, current) => {
35+
const [key, val] = current
36+
if (val != null) {
37+
return { ...acc, [key]: val }
38+
} else {
39+
return { ...acc, [key]: 'no value' }
40+
}
41+
}, {})
42+
: {}
43+
3144
const query = useQuery<CommandsData, TError>(
32-
[host, 'runs', runId, 'getCommandsAsPreSerializedList', cursor, pageLength],
45+
[
46+
hostKey,
47+
'runs',
48+
runId,
49+
'getCommandsAsPreSerializedList',
50+
cursor,
51+
pageLength,
52+
],
3353
() => {
3454
return getCommandsAsPreSerializedList(
3555
host as HostConfig,

0 commit comments

Comments
 (0)