-
Notifications
You must be signed in to change notification settings - Fork 4
i15-1: display basic feedback from blueapi #44
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
base: main
Are you sure you want to change the base?
Changes from 23 commits
e997c63
38ff3ed
cb83b97
bec4ed7
2e2e94d
d4b9912
28b869f
de12c13
85d7ab4
b717540
cb84630
78a005f
c5738a4
a2ede88
fd020b7
7a61e6a
9e4c89d
8a74ce6
40377f0
5093662
c21fba7
b7c65ec
38ceb2e
fbd0420
80630f9
f2c4e53
77b99cf
1dda9b3
b9e529f
59a9248
a373950
83cc6cb
94f04cf
0ffbf99
afd4c2e
3925bbd
9975ef5
89eccf6
4c0658a
0d8027b
b3880e9
2b45039
a2eb7af
d94080b
df555b5
4adf11d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { | ||
| Alert, | ||
| Button, | ||
| Snackbar, | ||
| Tooltip, | ||
| type SnackbarCloseReason, | ||
| } from "@mui/material"; | ||
|
|
||
| import type { WorkerStateRequest } from "@atlas/blueapi"; | ||
| import { useSetWorkerState } from "@atlas/blueapi-query"; | ||
| import React, { useState } from "react"; | ||
|
|
||
| export function AbortPlanButton() { | ||
| const workerState = useSetWorkerState(); | ||
| const [openSnackbar, setOpenSnackbar] = useState<boolean>(false); | ||
|
|
||
| const abortPlan = async () => { | ||
| const workerRequest: WorkerStateRequest = { | ||
| new_state: "ABORTING", | ||
| reason: "Abort button pressed", | ||
| }; | ||
| workerState.mutate(workerRequest); | ||
| }; | ||
|
|
||
| const handleClick = async () => { | ||
| setOpenSnackbar(true); | ||
| await abortPlan(); | ||
| }; | ||
|
|
||
| const handleSnackbarClose = ( | ||
| _event: React.SyntheticEvent | Event, | ||
| reason?: SnackbarCloseReason, | ||
| ) => { | ||
| if (reason === "clickaway") { | ||
| return; | ||
| } | ||
|
|
||
| setOpenSnackbar(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <React.Fragment> | ||
| <Tooltip title="Abort current blueapi operation" placement="bottom"> | ||
| <Button | ||
| variant="contained" | ||
| color="error" | ||
| sx={{ width: "150px" }} | ||
| onClick={handleClick} | ||
| > | ||
| Abort | ||
| </Button> | ||
| </Tooltip> | ||
| <Snackbar | ||
| open={openSnackbar} | ||
| autoHideDuration={5000} | ||
| onClose={handleSnackbarClose} | ||
| anchorOrigin={{ vertical: "bottom", horizontal: "right" }} | ||
| > | ||
| <Alert onClose={handleSnackbarClose} severity="warning"> | ||
| Abort button pressed, will abort current plan ... | ||
| </Alert> | ||
| </Snackbar> | ||
| </React.Fragment> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import { useGetWorkerState } from "@atlas/blueapi-query"; | ||
| import { | ||
| Card, | ||
| CardContent, | ||
| Stack, | ||
| Typography, | ||
| useTheme, | ||
| type Theme, | ||
| } from "@mui/material"; | ||
|
|
||
| function getStateColorMap(theme: Theme) { | ||
| return { | ||
| IDLE: theme.palette.info.main, | ||
| RUNNING: theme.palette.success.main, | ||
| PAUSING: theme.palette.warning.main, | ||
| PAUSED: theme.palette.warning.main, | ||
| HALTING: theme.palette.warning.main, | ||
| STOPPING: theme.palette.error.main, | ||
| ABORTING: theme.palette.error.main, | ||
| SUSPENDING: theme.palette.error.main, | ||
| PANICKED: theme.palette.error.main, | ||
| UNKNOWN: theme.palette.background.paper, | ||
| }; | ||
| } | ||
|
|
||
| export function BlueapiWorkerState() { | ||
| const theme = useTheme(); | ||
| const workerState = useGetWorkerState(); | ||
| const stateMap = getStateColorMap(theme); | ||
|
|
||
| return ( | ||
| <Card | ||
| variant="outlined" | ||
| sx={{ | ||
| minWidth: 250, | ||
| maxHeight: 100, | ||
| bgcolor: theme.palette.background.paper, | ||
| borderColor: theme.palette.text.primary, | ||
| }} | ||
| > | ||
| <CardContent> | ||
| <Stack direction={"column"} spacing={"1"}> | ||
| <Typography>Blueapi worker state: </Typography> | ||
| <Typography | ||
| variant="body1" | ||
| color={stateMap[workerState.data ? workerState.data : "UNKNOWN"]} | ||
| > | ||
| {workerState.data} | ||
| </Typography> | ||
| </Stack> | ||
| </CardContent> | ||
| </Card> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ | |
| import { RunPlanButton } from "@atlas/blueapi-ui"; | ||
| import { ReadOnlyPv } from "@atlas/pvws-config"; | ||
| import { StatusCard } from "../components/StatusCard"; | ||
| import { BlueapiWorkerState } from "../components/BlueapiWorkerState"; | ||
| // import { AbortPlanButton } from "../components/AbortPlanButton"; | ||
| import { WebcamStreamFromPv } from "../components/Webcam"; | ||
|
|
||
| type RobotSampleFormData = { | ||
|
|
@@ -16,6 +18,19 @@ | |
| const theme = useTheme(); | ||
| return ( | ||
| <Box sx={{ ml: 5 }}> | ||
| <Stack direction={"column"} spacing={2}> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Must: I think this is a duplicate of the existing card. Maybe a bad merge? |
||
| <BlueapiWorkerState /> | ||
| <StatusCard | ||
| title="Currently loaded" | ||
| bgColor={theme.palette.info.light} | ||
| cardColor={theme.palette.primary.main} | ||
| > | ||
| <ReadOnlyPv label="Puck" pv="ca://BL15J-EA-LOC-01:PUCK:INDEX" /> | ||
| <ReadOnlyPv | ||
| label="Sample Pin" | ||
| pv="ca://BL15J-EA-LOC-01:SAMPLE:INDEX" | ||
| /> | ||
| </StatusCard> | ||
| <Stack direction={"row"} spacing={2}> | ||
| <StatusCard | ||
| title="Ring status" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,18 @@ | ||
| import { Button } from "@mui/material"; | ||
| import { useState } from "react"; | ||
| import { | ||
| Alert, | ||
| Button, | ||
| Snackbar, | ||
| type SnackbarCloseReason, | ||
| } from "@mui/material"; | ||
| import React, { useState } from "react"; | ||
|
|
||
| import { | ||
| useBlueapi, | ||
| useGetWorkerState, | ||
| useSetActiveTask, | ||
| useSubmitTask, | ||
| } from "@atlas/blueapi-query"; | ||
| import type { TaskRequest } from "@atlas/blueapi"; | ||
| import type { TaskRequest, TaskResponse } from "@atlas/blueapi"; | ||
|
|
||
| export type RunPlanButtonProps = { | ||
| name: string; | ||
|
|
@@ -15,47 +21,125 @@ export type RunPlanButtonProps = { | |
| buttonText?: string; | ||
| }; | ||
|
|
||
| type SeverityLevel = "success" | "info" | "warning" | "error"; | ||
|
|
||
| const idleState = "IDLE"; | ||
| const abortState = "ABORTING"; | ||
|
|
||
| export function RunPlanButton({ | ||
| name, | ||
| params, | ||
| instrumentSession, | ||
| buttonText = "Run", | ||
| }: RunPlanButtonProps) { | ||
| const [openSnackbar, setOpenSnackbar] = useState<boolean>(false); | ||
| const [msg, setMsg] = useState<string>(`Running ${name} plan`); | ||
| const [severity, setSeverity] = useState<SeverityLevel>("info"); | ||
|
|
||
| const [loading, setLoading] = useState<boolean>(false); | ||
|
|
||
| const workerState = useGetWorkerState(); | ||
| const blueapi = useBlueapi(); | ||
|
|
||
| const submitTask = useSubmitTask(); | ||
| const startTask = useSetActiveTask(); | ||
| const submitAndRunTask = async (task: TaskRequest) => { | ||
| await submitTask | ||
| .mutateAsync(task) | ||
| .then((response) => startTask.mutateAsync(response.task_id)); | ||
|
|
||
| const waitForIdle = async (timeoutInMs: number): Promise<void> => { | ||
| return new Promise((res) => setTimeout(res, timeoutInMs)); | ||
| }; | ||
|
|
||
| const runTask = async (task_id: string) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should: Can we have some tests on the logic in this? |
||
| await startTask.mutateAsync(task_id).then(async (response) => { | ||
| if (response) { | ||
| let status = workerState.data; | ||
| while (status !== idleState && status !== abortState) { | ||
| await waitForIdle(10); | ||
| status = workerState.data; | ||
| } | ||
| const data = await blueapi.tasks.get(task_id); | ||
| if (data.is_complete) { | ||
| if (data.outcome?.outcome === "success") { | ||
| setSeverity("success"); | ||
| setMsg("Plan succeeded"); | ||
| } else if (data.outcome?.outcome === "error") { | ||
| throw new Error(`${data.errors[0]}`); | ||
| } | ||
| } | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| const submitAndRunTask = async ( | ||
| task: TaskRequest, | ||
| ): Promise<TaskResponse | void> => { | ||
| await submitTask.mutateAsync(task).then(async (response) => { | ||
| if (response) { | ||
| await runTask(response.task_id).catch((error) => { | ||
| throw new Error(error); | ||
| }); | ||
| } else { | ||
| throw new Error("Task couldn't be submitted"); | ||
| } | ||
| }); | ||
| }; | ||
|
|
||
| const [loading, setLoading] = useState<boolean>(false); | ||
| const handleClick = async () => { | ||
| setOpenSnackbar(true); | ||
| setLoading(true); | ||
| const taskRequest: TaskRequest = { | ||
| name: name, | ||
| params: params, | ||
| instrument_session: instrumentSession, | ||
| }; | ||
| setLoading(true); | ||
| await submitAndRunTask(taskRequest); | ||
| await submitAndRunTask(taskRequest).catch((error) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should: Can we have a test on this |
||
| setSeverity("error"); | ||
| setMsg( | ||
| `Failed to run plan ${name}, see console and blueapi logs for full error.`, | ||
| ); | ||
| console.log(`${msg}.\n Reason: ${error}`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like the first time a task fails, the snackbar shows the correct message: However, the console message shows the default message: I imagine this is something that may also be fixed by refactoring the logic outside the component? |
||
| }); | ||
| setLoading(false); | ||
| }; | ||
|
|
||
| const handleSnackbarClose = ( | ||
| _event: React.SyntheticEvent | Event, | ||
| reason?: SnackbarCloseReason, | ||
| ) => { | ||
| if (reason === "clickaway") { | ||
| return; | ||
| } | ||
|
|
||
| setOpenSnackbar(false); | ||
| }; | ||
|
|
||
| const isButtonDisabled = () => { | ||
| const workerState = useGetWorkerState(); | ||
| const disable = workerState.data !== "IDLE"; | ||
| const disable = workerState.data !== idleState; | ||
|
|
||
| return disable; | ||
| }; | ||
|
|
||
| return ( | ||
| <Button | ||
| variant="contained" | ||
| loading={loading} | ||
| sx={{ width: "150px" }} | ||
| onClick={handleClick} | ||
| disabled={isButtonDisabled()} | ||
| > | ||
| {buttonText} | ||
| </Button> | ||
| <React.Fragment> | ||
| <Button | ||
| variant="contained" | ||
| loading={loading} | ||
| sx={{ width: "150px" }} | ||
| onClick={handleClick} | ||
| disabled={isButtonDisabled()} | ||
| > | ||
| {buttonText} | ||
| </Button> | ||
| <Snackbar | ||
| open={openSnackbar} | ||
| autoHideDuration={10000} | ||
| onClose={handleSnackbarClose} | ||
| anchorOrigin={{ vertical: "bottom", horizontal: "right" }} | ||
| > | ||
| <Alert onClose={handleSnackbarClose} severity={severity}> | ||
| {msg} | ||
| </Alert> | ||
| </Snackbar> | ||
| </React.Fragment> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: This is no longer used? I think we want it here, right?