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

Add Queue History #19

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 26 additions & 14 deletions apps/spu-ui/src/app/_components/experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
setSamples as setServerSamples,
} from "../actions/samples";
import { Console } from "./console";
import { History } from "./history";
import { Queue } from "./queue";
import { SampleItem } from "./sample";
import { Tray } from "./tray";
Expand Down Expand Up @@ -136,7 +137,19 @@ export default function Experiment({
return (
<DndContext onDragEnd={handleDragEnd} onDragStart={handleDragStart}>
<div className="flex h-[calc(100vh-64px)] items-start justify-center gap-4 px-4 pt-4">
<Tabs defaultValue="tray1">
<Tabs className="space-y-2" defaultValue="tray1">
<TabsContent value="tray1">
<Tray
activeId={activeId}
samples={samples.filter((sample) => sample.tray === TRAY1)}
/>
</TabsContent>
<TabsContent value="tray2">
<Tray
activeId={activeId}
samples={samples.filter((sample) => sample.tray === TRAY2)}
/>
</TabsContent>
<div className="flex items-center gap-2">
<TabsList>
<TabsTrigger value="tray1">Tray 1</TabsTrigger>
Expand All @@ -151,21 +164,20 @@ export default function Experiment({
Clear Samples
</Button>
</div>
<TabsContent value="tray1">
<Tray
activeId={activeId}
samples={samples.filter((sample) => sample.tray === TRAY1)}
/>
</TabsContent>
<TabsContent value="tray2">
<Tray
activeId={activeId}
samples={samples.filter((sample) => sample.tray === TRAY2)}
/>
</TabsContent>
</Tabs>
<div className="flex w-2/3 flex-col gap-4">
<Queue />
<Tabs className="space-y-2" defaultValue="queue">
<TabsContent value="queue">
<Queue />
</TabsContent>
<TabsContent value="history">
<History />
</TabsContent>
<TabsList>
<TabsTrigger value="queue">Queue</TabsTrigger>
<TabsTrigger value="history">History</TabsTrigger>
</TabsList>
</Tabs>
<Console />
</div>
</div>
Expand Down
115 changes: 115 additions & 0 deletions apps/spu-ui/src/app/_components/history.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks good as a starting point. We should discuss, sorting and filtering the history items, as well as showing the times (start and stop) and other information.

Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"use client";

import { Trash2Icon } from "lucide-react";
import { cn } from "@sophys-web/ui";
import { Badge } from "@sophys-web/ui/badge";
import { Button } from "@sophys-web/ui/button";
import { ScrollArea } from "@sophys-web/ui/scroll-area";
import { toast } from "@sophys-web/ui/sonner";
import type { HistoryItemProps } from "../../lib/types";
import { kwargsResponseSchema } from "../../lib/schemas/acquisition";
import { api } from "../../trpc/react";

function HistoryItem({ props }: { props: HistoryItemProps }) {
const { data: planParams } = kwargsResponseSchema.safeParse(props.kwargs);
const status = function () {
// how can I get the running item?
if (!props.result) {
return "oops";
}
if (props.result.traceback) {
return "failed";
}
return "completed";
};

return (
<li className="w-fill flex select-none items-center justify-between space-x-2 rounded-md bg-gray-50 p-3 shadow-md">
<div className="flex flex-grow items-center space-x-3">
<div
className={cn(
"relative flex h-10 w-10 items-center justify-center rounded-full font-bold text-white",
{
"bg-gray-500": !planParams,
"bg-emerald-500": planParams?.sampleType === "sample",
"bg-sky-500": planParams?.sampleType === "buffer",
},
)}
>
{planParams ? `${planParams.col}${planParams.row}` : "N/A"}
<span className="absolute bottom-0 right-0 flex h-3 w-3 items-center justify-center rounded-full bg-white text-xs text-black">
{planParams ? planParams.sampleType.toUpperCase()[0] : "-"}
</span>
</div>
<div className="flex items-center space-x-2">
{planParams ? (
<div className="flex flex-col">
<p className="font-bold">{planParams.sampleTag}</p>
<p className="text-sm text-muted-foreground">
{`${planParams.tray} |`}
{planParams.sampleType !== "buffer" &&
` buffer: ${planParams.bufferTag} |`}
{` user: ${props.user}`}
</p>
</div>
) : (
<div className="flex flex-col">
<p className="font-bold">{`${props.name}`}</p>
<p className="text-sm text-muted-foreground">
{` user: ${props.user}`}
</p>
</div>
)}
</div>
<Badge variant="outline">{status()}</Badge>
</div>
</li>
);
}

export function History() {
const utils = api.useUtils();
const { data } = api.history.get.useQuery(undefined, {
refetchOnMount: "always",
});
const { mutate } = api.history.clear.useMutation({
onSuccess: async () => {
toast.success("Queue cleared.");
await utils.history.invalidate();
},
});

return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-center gap-2">
<h1 className="mr-auto text-lg font-medium">Experiment History</h1>

<Button
disabled={data?.items.length === 0}
onClick={() => {
mutate();
}}
variant="outline"
>
<Trash2Icon className="mr-2 h-4 w-4" />
Clear History
</Button>
</div>
<div className="relative flex h-fit w-full items-center justify-center rounded-lg border-2 border-muted p-4 font-medium">
<ScrollArea className="flex h-[calc(100vh-480px)] w-full flex-col">
{data?.items.length === 0 ? (
<p className="text-center text-muted-foreground">
History is empty.
</p>
) : (
<ul className="space-y-2">
{data?.items.map((item) => (
<HistoryItem key={item.itemUid} props={item} />
))}
</ul>
)}
</ScrollArea>
</div>
</div>
);
}
3 changes: 3 additions & 0 deletions apps/spu-ui/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ type QueueResponse = z.infer<typeof queueResponseSchema>;
export type QueueItemProps =
| QueueResponse["items"][number]
| QueueResponse["runningItem"];

type HistoryResponse = z.infer<typeof schemas.history.getResponseSchema>;
export type HistoryItemProps = HistoryResponse["items"][number];
2 changes: 2 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AppRouter } from "./root";
import { appRouter } from "./root";
import common from "./schemas/common";
import devices from "./schemas/devices";
import history from "./schemas/history";
import instructions from "./schemas/instructions";
import item from "./schemas/item";
import plans from "./schemas/plans";
Expand All @@ -16,6 +17,7 @@ const schemas = {
plans,
queue,
item,
history,
};

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/root.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { consoleOutputRouter } from "./router/console-output";
import { devicesRouter } from "./router/devices";
import { environmentRouter } from "./router/environment";
import { historyRouter } from "./router/history";
import { plansRouter } from "./router/plans";
import { postRouter } from "./router/post";
import { queueRouter } from "./router/queue";
Expand All @@ -15,6 +16,7 @@ export const appRouter = createTRPCRouter({
environment: environmentRouter,
status: statusRouter,
consoleOutput: consoleOutputRouter,
history: historyRouter,
});

// export type definition of API
Expand Down
50 changes: 50 additions & 0 deletions packages/api/src/router/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createZodFetcher } from "zod-fetch";
import { env } from "../../env";
import commonSchemas from "../schemas/common";
import history from "../schemas/history";
import { protectedProcedure } from "../trpc";

export const historyRouter = {
get: protectedProcedure.query(async ({ ctx }) => {
const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/history/get`;
const fetchWithZod = createZodFetcher();
try {
const res = await fetchWithZod(history.getResponseSchema, fetchURL, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`,
},
body: undefined,
});
return res;
} catch (e) {
if (e instanceof Error) {
console.error(e);
throw new Error(e.message);
}
throw new Error("Unknown error");
}
}),
clear: protectedProcedure.mutation(async ({ ctx }) => {
const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/history/clear`;
const fetchWithZod = createZodFetcher();
try {
const res = await fetchWithZod(commonSchemas.response, fetchURL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`,
},
body: undefined,
});
return res;
} catch (e) {
if (e instanceof Error) {
console.error(e);
throw new Error(e.message);
}
throw new Error("Unknown error");
}
}),
} as const;
70 changes: 70 additions & 0 deletions packages/api/src/schemas/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { z } from "zod";

const getResponseSchema = z
.object({
success: z.boolean(),
msg: z.string(),
items: z.array(
z.object({
name: z.string(),
args: z.array(z.any()).optional().nullable(),
kwargs: z.record(z.any()).optional().nullable(),
item_type: z.string(),
user: z.string(),
user_group: z.string(),
item_uid: z.string().uuid(),
result: z
.object({
exit_status: z.string().optional(),
run_uids: z.array(z.string()),
scan_ids: z.array(z.string()),
time_start: z.number(),
time_stop: z.number(),
msg: z.string().optional(),
traceback: z.string().optional(),
})
.optional()
.nullable(),
}),
),
plan_history_uid: z.string().uuid(),
})
.transform((data) => {
const {
success,
msg,
items: unprocessedItems,
plan_history_uid,
...unchanged
} = data;

const items = unprocessedItems.map((item) => ({
name: item.name,
args: item.args,
kwargs: item.kwargs,
itemType: item.item_type,
user: item.user,
userGroup: item.user_group,
itemUid: item.item_uid,
result: item.result
? {
exitStatus: item.result.exit_status,
runUids: item.result.run_uids,
scanIds: item.result.scan_ids,
timeStart: item.result.time_start,
timeStop: item.result.time_stop,
msg: item.result.msg,
traceback: item.result.traceback,
}
: null,
}));
return {
success,
msg,
items,
planHistoryUid: plan_history_uid,
...unchanged,
};
});

export default { getResponseSchema };