Skip to content

Commit 108ef05

Browse files
i15-1: display basic feedback from blueapi (#44)
* Add a blueapi status card * Add abort button * Tidy up * Try adding a handler * Add a first snackbar * Get the msw handler to more or less show abort * Update the Trackable task from blueapi * Add a snackbar to the run plan button * Move snackbar back * Add more snackbar and try to mock * Fix the snackbar - now just need to poll * Try to see if it wait for sleep * An attempt at polling * Fix typing in test * Fix lint * Fix typecheck * Rename parameter * Pull idle and aborting into constants * Fix issues from bad merge in Robot * Put the abort button back in * Add more snackbars * Put the worker state back in * Get the polling to work * Fix the polling * Get mock for useBlueapi to work and add a test to check it * One more test * Remove some extra comments * And maybe remember to update test * Use correct utility * Use act for rendering * Try to fix it * Check if other tests actually pass * Check if other tests actually pass - part 2 * Try to fix broken tests one by one * Try to fix broken tests one by one - maybe mocking an actual missing response as it's not a rejection * See happy test * Tidy up * Add tests for abort button and tidy up * Fix lint * Try to disable warning since linter does not like the cast * Try removing the offending comment * Remove typo * Actually remove typo * Update log message * Try commenting out failing test * Bring it partially back * Tidy up --------- Co-authored-by: Jacob Williamson <jacob.williamson@diamond.ac.uk>
1 parent 2b6e311 commit 108ef05

7 files changed

Lines changed: 397 additions & 73 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { render, screen, userEvent } from "@atlas/vitest-conf";
2+
import { AbortPlanButton } from "./AbortPlanButton";
3+
import { useSetWorkerState } from "@atlas/blueapi-query";
4+
import type { WorkerState, WorkerStateRequest } from "@atlas/blueapi";
5+
import type { UseMutationResult } from "@tanstack/react-query";
6+
7+
describe("AbortPlanButton", () => {
8+
vi.mock("@atlas/blueapi-query");
9+
const mockedHook = vi.mocked(useSetWorkerState);
10+
11+
const mutate = vi.fn();
12+
13+
/* eslint-disable @typescript-eslint/no-explicit-any */
14+
mockedHook.mockReturnValue({ mutate } as any as UseMutationResult<
15+
WorkerState,
16+
Error,
17+
WorkerStateRequest
18+
>);
19+
20+
it("renders default Abort button", () => {
21+
render(<AbortPlanButton />);
22+
23+
expect(screen.getByText("Abort"));
24+
});
25+
26+
it("when abort clicked the worker state changes and alert comes on screen", async () => {
27+
const expectedRequest: WorkerStateRequest = {
28+
new_state: "ABORTING",
29+
reason: "Abort button pressed",
30+
};
31+
const user = userEvent.setup();
32+
render(<AbortPlanButton />);
33+
34+
const button = screen.getByText("Abort");
35+
await user.click(button);
36+
expect(mutate).toHaveBeenCalledWith(expectedRequest);
37+
expect(
38+
screen.findByTestId("Abort button pressed, will abort current plan ..."),
39+
);
40+
const alert = await screen.findByRole("alert");
41+
expect(alert).toBeVisible();
42+
});
43+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
Alert,
3+
Button,
4+
Snackbar,
5+
Tooltip,
6+
type SnackbarCloseReason,
7+
} from "@mui/material";
8+
9+
import type { WorkerStateRequest } from "@atlas/blueapi";
10+
import { useSetWorkerState } from "@atlas/blueapi-query";
11+
import React, { useState } from "react";
12+
13+
export function AbortPlanButton() {
14+
const workerState = useSetWorkerState();
15+
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
16+
17+
const abortPlan = async () => {
18+
const workerRequest: WorkerStateRequest = {
19+
new_state: "ABORTING",
20+
reason: "Abort button pressed",
21+
};
22+
workerState.mutate(workerRequest);
23+
};
24+
25+
const handleClick = async () => {
26+
setOpenSnackbar(true);
27+
await abortPlan();
28+
};
29+
30+
const handleSnackbarClose = (
31+
_event: React.SyntheticEvent | Event,
32+
reason?: SnackbarCloseReason,
33+
) => {
34+
if (reason === "clickaway") {
35+
return;
36+
}
37+
38+
setOpenSnackbar(false);
39+
};
40+
41+
return (
42+
<React.Fragment>
43+
<Tooltip title="Abort current blueapi operation" placement="bottom">
44+
<Button
45+
variant="contained"
46+
color="error"
47+
sx={{ width: "150px" }}
48+
onClick={handleClick}
49+
>
50+
Abort
51+
</Button>
52+
</Tooltip>
53+
<Snackbar
54+
open={openSnackbar}
55+
autoHideDuration={5000}
56+
onClose={handleSnackbarClose}
57+
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
58+
>
59+
<Alert onClose={handleSnackbarClose} severity="warning">
60+
Abort button pressed, will abort current plan ...
61+
</Alert>
62+
</Snackbar>
63+
</React.Fragment>
64+
);
65+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useGetWorkerState } from "@atlas/blueapi-query";
2+
import {
3+
Card,
4+
CardContent,
5+
Stack,
6+
Typography,
7+
useTheme,
8+
type Theme,
9+
} from "@mui/material";
10+
11+
function getStateColorMap(theme: Theme) {
12+
return {
13+
IDLE: theme.palette.info.main,
14+
RUNNING: theme.palette.success.main,
15+
PAUSING: theme.palette.warning.main,
16+
PAUSED: theme.palette.warning.main,
17+
HALTING: theme.palette.warning.main,
18+
STOPPING: theme.palette.error.main,
19+
ABORTING: theme.palette.error.main,
20+
SUSPENDING: theme.palette.error.main,
21+
PANICKED: theme.palette.error.main,
22+
UNKNOWN: theme.palette.background.paper,
23+
};
24+
}
25+
26+
export function BlueapiWorkerState() {
27+
const theme = useTheme();
28+
const workerState = useGetWorkerState();
29+
const stateMap = getStateColorMap(theme);
30+
31+
return (
32+
<Card
33+
variant="outlined"
34+
sx={{
35+
minWidth: 250,
36+
maxHeight: 200,
37+
bgcolor: theme.palette.background.paper,
38+
borderColor: theme.palette.text.primary,
39+
}}
40+
>
41+
<CardContent>
42+
<Stack direction={"column"} spacing={"1"}>
43+
<Typography
44+
variant="body1"
45+
sx={{
46+
fontSize: 16,
47+
fontStyle: "italic",
48+
fontWeight: "bold",
49+
}}
50+
>
51+
Blueapi worker state:{" "}
52+
</Typography>
53+
<Typography
54+
variant="body1"
55+
sx={{ fontSize: 18, fontWeight: "bold" }}
56+
color={stateMap[workerState.data ? workerState.data : "UNKNOWN"]}
57+
>
58+
{workerState.data}
59+
</Typography>
60+
</Stack>
61+
</CardContent>
62+
</Card>
63+
);
64+
}

apps/i15-1/src/mocks/handlers.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ const fakeExperiments = {
7979
},
8080
};
8181

82+
function setWorkerState(new_state: string) {
83+
workerStatus.status = new_state;
84+
}
85+
8286
const fakePvws = ws.link("wss://pvws.diamond.ac.uk/pvws/pv");
8387

8488
const fakeHistory = [
@@ -522,7 +526,7 @@ export const handlers = [
522526
: []),
523527

524528
http.put("/api/blueapi/worker/task", () => {
525-
workerStatus.status = "RUNNING";
529+
setWorkerState("RUNNING");
526530
return HttpResponse.json({
527531
task_id: fakeTaskId,
528532
});
@@ -534,8 +538,24 @@ export const handlers = [
534538
});
535539
}),
536540

537-
http.put("/api/blueapi/worker/state", () => {
538-
return HttpResponse.json("IDLE");
541+
http.get("/api/blueapi/tasks/:task_id", () => {
542+
return HttpResponse.json({
543+
task_id: fakeTaskId,
544+
task: { name: "fake-task", params: {}, metadata: {} },
545+
request_id: "00",
546+
is_complete: true,
547+
is_pending: false,
548+
errors: [],
549+
outcome: { outcome: "success", type: "str", result: null },
550+
});
551+
}),
552+
553+
http.put("/api/blueapi/worker/state", async ({ request }) => {
554+
const { new_state } = (await request.json()) as { new_state: string };
555+
if (new_state === "ABORTING") {
556+
setWorkerState(new_state);
557+
}
558+
return HttpResponse.json(workerStatus.status);
539559
}),
540560

541561
http.get("/oauth2/userinfo", () => {

apps/i15-1/src/routes/Robot.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import { NumberInput } from "../components/NumberInput";
55
import { RunPlanButton } from "@atlas/blueapi-ui";
66
import { ReadOnlyPv } from "@atlas/pvws-config";
77
import { StatusCard } from "../components/StatusCard";
8+
import { AbortPlanButton } from "../components/AbortPlanButton";
89
import { WebcamStreamFromPv } from "../components/Webcam";
10+
import { BlueapiWorkerState } from "../components/BlueapiWorkerState";
911

1012
type RobotSampleFormData = {
1113
puck: number;
@@ -53,6 +55,7 @@ function StatusSidebar() {
5355
pv="ca://BL15J-EA-LOC-01:SAMPLE:INDEX"
5456
/>
5557
</StatusCard>
58+
<BlueapiWorkerState />
5659
</Stack>
5760
</Box>
5861
);
@@ -96,17 +99,20 @@ function RobotControl() {
9699
}}
97100
/>
98101
</Stack>
99-
<RunPlanButton
100-
name="robot_load"
101-
params={formData}
102-
instrumentSession={instrumentSession}
103-
buttonText="Load Sample"
104-
/>
105-
<RunPlanButton
106-
name="robot_unload"
107-
instrumentSession={instrumentSession}
108-
buttonText="Unload Sample"
109-
/>
102+
<Stack direction={"row"} spacing={3} alignItems={"center"}>
103+
<RunPlanButton
104+
name="robot_load"
105+
params={formData}
106+
instrumentSession={instrumentSession}
107+
buttonText="Load Sample"
108+
/>
109+
<RunPlanButton
110+
name="robot_unload"
111+
instrumentSession={instrumentSession}
112+
buttonText="Unload Sample"
113+
/>
114+
</Stack>
115+
<AbortPlanButton />
110116
</Stack>
111117
</Box>
112118
);

0 commit comments

Comments
 (0)