From e5db9ee439a1e8919d0094558066a1c2c0f2a8e1 Mon Sep 17 00:00:00 2001 From: Bruno Carlos <6951456+brnovasco@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:44:08 -0300 Subject: [PATCH 01/10] packages/api: Updating schemas with props transformations Changing existing schemas to enable transform props names from httpserver names to app names (generally snake case to camel case) accounting for the dicrection of the transformation depending on the schema purpose (app -> server, app <- server). Adding new schemas following the same pattern. --- packages/api/src/index.ts | 8 ++ packages/api/src/schemas/common.ts | 10 ++ packages/api/src/schemas/devices.ts | 86 ++++++++++------ packages/api/src/schemas/instructions.ts | 22 +++++ packages/api/src/schemas/item.ts | 53 ++++++++++ packages/api/src/schemas/plans.ts | 89 ++++++++++++----- packages/api/src/schemas/queue.ts | 119 +++++++++++++++++++++++ packages/api/src/schemas/status.ts | 96 ++++++++++++++++++ 8 files changed, 431 insertions(+), 52 deletions(-) create mode 100644 packages/api/src/schemas/common.ts create mode 100644 packages/api/src/schemas/instructions.ts create mode 100644 packages/api/src/schemas/item.ts create mode 100644 packages/api/src/schemas/queue.ts create mode 100644 packages/api/src/schemas/status.ts diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 39f1d38..0ba1222 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,11 +1,19 @@ import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import type { AppRouter } from "./root"; import { appRouter } from "./root"; +import common from "./schemas/common"; +import devices from "./schemas/devices"; +import instructions from "./schemas/instructions"; import plans from "./schemas/plans"; +import queue from "./schemas/queue"; import { createCallerFactory, createTRPCContext } from "./trpc"; const schemas = { + common, + devices, + instructions, plans, + queue, }; /** diff --git a/packages/api/src/schemas/common.ts b/packages/api/src/schemas/common.ts new file mode 100644 index 0000000..98b6dea --- /dev/null +++ b/packages/api/src/schemas/common.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; + +const response = z.object({ + success: z.boolean(), + msg: z.string().optional(), +}); + +export default { + response, +}; diff --git a/packages/api/src/schemas/devices.ts b/packages/api/src/schemas/devices.ts index 6986172..e285a8a 100644 --- a/packages/api/src/schemas/devices.ts +++ b/packages/api/src/schemas/devices.ts @@ -1,32 +1,62 @@ import { z } from "zod"; -const componentSchema = z.object({ - classname: z.string(), - is_flyable: z.boolean(), - is_movable: z.boolean(), - is_readable: z.boolean(), - long_name: z.string(), - module: z.string(), -}); +const devicesAllowedSchema = z.record( + z.object({ + classname: z.string(), + isFlyable: z.boolean(), + isMovable: z.boolean(), + isReadable: z.boolean(), + longName: z.string(), + module: z.string(), + components: z + .record( + z.object({ + classname: z.string(), + isFlyable: z.boolean(), + isMovable: z.boolean(), + isReadable: z.boolean(), + longName: z.string(), + module: z.string(), + }), + ) + .optional(), + }), +); -const deviceSchema = z.object({ - classname: z.string(), - is_flyable: z.boolean(), - is_movable: z.boolean(), - is_readable: z.boolean(), - long_name: z.string(), - module: z.string(), - components: z.record(componentSchema).optional(), -}); +const responseSchema = z + .object({ + success: z.boolean(), + msg: z.string(), + devices_allowed: z.record( + z.object({ + classname: z.string(), + is_flyable: z.boolean(), + is_movable: z.boolean(), + is_readable: z.boolean(), + long_name: z.string(), + module: z.string(), + components: z + .record( + z.object({ + classname: z.string(), + is_flyable: z.boolean(), + is_movable: z.boolean(), + is_readable: z.boolean(), + long_name: z.string(), + module: z.string(), + }), + ) + .optional(), + }), + ), + }) + .transform((data) => { + const { devices_allowed, ...unchanged } = data; + const devicesAllowed = devicesAllowedSchema.parse(devices_allowed); + return { + devicesAllowed, + ...unchanged, + }; + }); -const apiResponseSchema = z.object({ - success: z.boolean(), - msg: z.string(), - devices_allowed: z.record(deviceSchema), -}); - -export default { - device: deviceSchema, - devicesAllowed: z.record(deviceSchema), - devicesAllowedResponse: apiResponseSchema, -}; +export default responseSchema; diff --git a/packages/api/src/schemas/instructions.ts b/packages/api/src/schemas/instructions.ts new file mode 100644 index 0000000..b7ac25a --- /dev/null +++ b/packages/api/src/schemas/instructions.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +export const AVAILABLE_INSTRUCTIONS = ["queue_stop"] as const; + +const queueStop = z + .object({ + item_type: z.literal("instruction"), + instruction: z.literal("queue_stop"), + args: z.array(z.void()).length(0), + kwargs: z.record(z.void()), + }) + .transform((data) => { + const { item_type: itemType, ...unchanged } = data; + return { + itemType, + ...unchanged, + }; + }); + +export default { + queueStop, +}; diff --git a/packages/api/src/schemas/item.ts b/packages/api/src/schemas/item.ts new file mode 100644 index 0000000..887e538 --- /dev/null +++ b/packages/api/src/schemas/item.ts @@ -0,0 +1,53 @@ +import { z } from "zod"; + +const addSubmit = z + .object({ + pos: z.number().optional(), + item: z.object({ + name: z.string(), + args: z.array(z.any()), + kwargs: z.record(z.any()), + itemType: z.string(), + }), + }) + .transform((data) => { + const { + item: { itemType, ...unchanged }, + pos, + } = data; + return { + item: { + item_type: itemType, + ...unchanged, + }, + pos, + }; + }); + +const addResponse = z.discriminatedUnion("success", [ + z.object({ success: z.literal(true) }), + z.object({ + success: z.literal(false), + msg: z.string(), + qsize: z.number().nullable(), + item: z + .object({ + name: z.string(), + args: z.array(z.any()), + kwargs: z.record(z.any()), + item_type: z.string(), + }) + .transform((data) => { + const { item_type, ...unchanged } = data; + return { + itemType: item_type, + ...unchanged, + }; + }), + }), +]); + +export default { + addSubmit, + addResponse, +}; diff --git a/packages/api/src/schemas/plans.ts b/packages/api/src/schemas/plans.ts index 312a62d..1c4f256 100644 --- a/packages/api/src/schemas/plans.ts +++ b/packages/api/src/schemas/plans.ts @@ -9,19 +9,34 @@ const annotation = z.object({ type: z.string(), }); -const parameter = z.object({ - annotation: annotation.optional(), - convert_device_names: z.boolean().optional(), - description: z.string().optional(), - kind: kind, - name: z.string(), - default: z.any().optional(), - eval_expressions: z.boolean().optional(), -}); +const parameter = z + .object({ + annotation: annotation.optional(), + convert_device_names: z.boolean().optional(), + description: z.string().optional(), + kind: kind, + name: z.string(), + default: z.any().optional(), + eval_expressions: z.boolean().optional(), + }) + .transform((data) => { + const { + convert_device_names: convertDeviceNames, + eval_expressions: evalExpressions, + ...unchanged + } = data; + return { + convertDeviceNames, + evalExpressions, + ...unchanged, + }; + }); -const properties = z.object({ - is_generator: z.boolean(), -}); +const properties = z + .object({ + is_generator: z.boolean(), + }) + .transform((data) => ({ isGenerator: data.is_generator })); const plan = z.object({ description: z.string(), @@ -31,19 +46,45 @@ const plan = z.object({ properties: properties, }); -const allowed = z.object({ - success: z.boolean(), - msg: z.string(), - plans_allowed_uid: z.string(), - plans_allowed: z.record(plan), -}); +const allowed = z + .object({ + success: z.boolean(), + msg: z.string(), + plans_allowed_uid: z.string(), + plans_allowed: z.record(plan), + }) + .transform((data) => { + const { + plans_allowed_uid: plansAllowedUid, + plans_allowed: plansAllowed, + ...unchanged + } = data; + return { + plansAllowedUid, + plansAllowed, + ...unchanged, + }; + }); -const existing = z.object({ - success: z.boolean(), - msg: z.string(), - plans_existing_uid: z.string(), - plans_existing: z.record(plan), -}); +const existing = z + .object({ + success: z.boolean(), + msg: z.string(), + plans_existing_uid: z.string(), + plans_existing: z.record(plan), + }) + .transform((data) => { + const { + plans_existing_uid: plansExistingUid, + plans_existing: plansExisting, + ...unchanged + } = data; + return { + plansExistingUid, + plansExisting, + ...unchanged, + }; + }); export default { allowed, diff --git a/packages/api/src/schemas/queue.ts b/packages/api/src/schemas/queue.ts new file mode 100644 index 0000000..4a42ae2 --- /dev/null +++ b/packages/api/src/schemas/queue.ts @@ -0,0 +1,119 @@ +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()), + kwargs: z.record(z.any()), + 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(), + }), + ), + running_item: z + .object({ + name: z.string().optional(), + args: z.array(z.any()).optional(), + kwargs: z.object({}).passthrough().optional(), + item_type: z.string().optional(), + user: z.string().optional(), + user_group: z.string().optional(), + item_uid: z.string().optional(), + properties: z.object({}).passthrough().optional(), + result: z + .object({ + exit_status: z.string().optional(), + run_uids: z.array(z.string()).optional(), + scan_ids: z.array(z.string()).optional(), + time_start: z.number().optional(), + time_stop: z.number().optional(), + msg: z.string().optional(), + traceback: z.string().optional(), + }) + .optional() + .nullable(), + }) + .optional() + .nullable(), + plan_queue_uid: z.string().uuid(), + }) + .transform((data) => { + const { + success, + msg, + items: unprocessedItems, + running_item, + plan_queue_uid, + ...unchanged + } = data; + const runningItem = running_item + ? { + name: running_item.name, + args: running_item.args, + kwargs: running_item.kwargs, + itemType: running_item.item_type, + user: running_item.user, + userGroup: running_item.user_group, + itemUid: running_item.item_uid, + properties: running_item.properties, + result: running_item.result + ? { + exitStatus: running_item.result.exit_status, + runUids: running_item.result.run_uids, + scanIds: running_item.result.scan_ids, + timeStart: running_item.result.time_start, + timeStop: running_item.result.time_stop, + msg: running_item.result.msg, + traceback: running_item.result.traceback, + } + : null, + } + : null; + 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, + runningItem, + planQueueUid: plan_queue_uid, + ...unchanged, + }; + }); + +export default { getResponseSchema }; diff --git a/packages/api/src/schemas/status.ts b/packages/api/src/schemas/status.ts new file mode 100644 index 0000000..214dbf8 --- /dev/null +++ b/packages/api/src/schemas/status.ts @@ -0,0 +1,96 @@ +import { z } from "zod"; + +const getResponse = z + .object({ + msg: z.string().optional(), + items_in_queue: z.number(), + items_in_history: z.number(), + running_item_uid: z.coerce.string(), + manager_state: z.string(), + queue_stop_pending: z.boolean(), + queue_autostart_enabled: z.boolean(), + worker_environment_exists: z.boolean(), + worker_environment_state: z.string(), + worker_background_tasks: z.number(), + re_state: z.string().nullable(), + ip_kernel_state: z.string().nullable(), + ip_kernel_captured: z.boolean().nullable(), + pause_pending: z.boolean(), + run_list_uid: z.string().optional(), + plan_queue_uid: z.string().optional(), + plan_history_uid: z.string().optional(), + devices_existing_uid: z.string().optional(), + plans_existing_uid: z.string().optional(), + devices_allowed_uid: z.string().optional(), + plans_allowed_uid: z.string().optional(), + plan_queue_mode: z.object({ + loop: z.boolean(), + ignore_failures: z.boolean(), + }), + task_results_uid: z.string().optional(), + lock_info_uid: z.string().optional(), + lock: z.object({ + environment: z.boolean(), + queue: z.boolean(), + }), + }) + .transform((data) => { + const { + items_in_queue: itemsInQueue, + items_in_history: itemsInHistory, + running_item_uid: runningItemUid, + manager_state: managerState, + queue_stop_pending: queueStopPending, + queue_autostart_enabled: queueAutostartEnabled, + worker_environment_exists: workerEnvironmentExists, + worker_environment_state: workerEnvironmentState, + worker_background_tasks: workerBackgroundTasks, + re_state: reState, + ip_kernel_state: ipKernelState, + ip_kernel_captured: ipKernelCaptured, + pause_pending: pausePending, + run_list_uid: runListUid, + plan_queue_uid: planQueueUid, + plan_history_uid: planHistoryUid, + devices_existing_uid: devicesExistingUid, + plans_existing_uid: plansExistingUid, + devices_allowed_uid: devicesAllowedUid, + plans_allowed_uid: plansAllowedUid, + plan_queue_mode: planQueueMode, + task_results_uid: taskResultsUid, + lock_info_uid: lockInfoUid, + lock, + ...unchanged + } = data; + return { + itemsInQueue, + itemsInHistory, + runningItemUid, + managerState, + queueStopPending, + queueAutostartEnabled, + workerEnvironmentExists, + workerEnvironmentState, + workerBackgroundTasks, + reState, + ipKernelState, + ipKernelCaptured, + pausePending, + runListUid, + planQueueUid, + planHistoryUid, + devicesExistingUid, + plansExistingUid, + devicesAllowedUid, + plansAllowedUid, + planQueueMode, + taskResultsUid, + lockInfoUid, + lock, + ...unchanged, + }; + }); + +export default { + getResponse, +}; From 9535ee8cda1f9dcc40949244e79e71d1f051a599 Mon Sep 17 00:00:00 2001 From: Bruno Carlos <6951456+brnovasco@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:52:08 -0300 Subject: [PATCH 02/10] fix devices route with new schema --- packages/api/src/router/devices.ts | 58 ++++++++++++------------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/packages/api/src/router/devices.ts b/packages/api/src/router/devices.ts index 216f0ba..df12c00 100644 --- a/packages/api/src/router/devices.ts +++ b/packages/api/src/router/devices.ts @@ -1,41 +1,22 @@ -import { z } from "zod"; import { createZodFetcher } from "zod-fetch"; import { env } from "../../env"; -import schemas from "../schemas/devices"; +import devicesSchema from "../schemas/devices"; import { protectedProcedure } from "../trpc"; -function namedDevices(devices_allowed: z.infer) { - const flyables = Object.entries(devices_allowed) - .map(([_, value]) => (value.is_flyable ? value.long_name : null)) - .filter((x) => x !== null); - const movables = Object.entries(devices_allowed) - .map(([_, value]) => (value.is_movable ? value.long_name : null)) - .filter((x) => x !== null); - const readables = Object.entries(devices_allowed) - .map(([_, value]) => (value.is_readable ? value.long_name : null)) - .filter((x) => x !== null); - - return { - flyables, - movables, - readables, - }; -} - export const devicesRouter = { allowed: protectedProcedure.query(async ({ ctx }) => { const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/devices/allowed`; const fetchWithZod = createZodFetcher(); try { - const devices = await fetchWithZod(schemas.devicesAllowed, fetchURL, { + const devices = await fetchWithZod(devicesSchema, fetchURL, { method: "GET", headers: { - contentType: "application/json", + "Content-Type": "application/json", Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, }, body: undefined, }); - return devices.devices_allowed; + return devices.devicesAllowed; } catch (e) { if (e instanceof Error) { throw new Error(e.message); @@ -43,23 +24,28 @@ export const devicesRouter = { throw new Error("Unknown error"); } }), - allowedNamed: protectedProcedure.query(async ({ ctx }) => { + allowedNames: protectedProcedure.query(async ({ ctx }) => { const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/devices/allowed`; const fetchWithZod = createZodFetcher(); try { - const devices = await fetchWithZod( - schemas.devicesAllowedResponse, - fetchURL, - { - method: "GET", - headers: { - contentType: "application/json", - Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, - }, - body: undefined, + const devices = await fetchWithZod(devicesSchema, fetchURL, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, }, - ); - return namedDevices(devices.devices_allowed); + body: undefined, + }); + const flyables = Object.values(devices.devicesAllowed) + .filter((d) => d.isFlyable) + .map((d) => d.longName); + const movables = Object.values(devices.devicesAllowed) + .filter((d) => d.isMovable) + .map((d) => d.longName); + const readables = Object.values(devices.devicesAllowed) + .filter((d) => d.isReadable) + .map((d) => d.longName); + return { flyables, movables, readables }; } catch (e) { if (e instanceof Error) { throw new Error(e.message); From 8c6c8d6c8d182b575bec3867e1ef57516e99d598 Mon Sep 17 00:00:00 2001 From: Bruno Carlos <6951456+brnovasco@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:52:25 -0300 Subject: [PATCH 03/10] fix plans route with new schema --- packages/api/src/router/plans.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api/src/router/plans.ts b/packages/api/src/router/plans.ts index da6ed5e..b9ade2c 100644 --- a/packages/api/src/router/plans.ts +++ b/packages/api/src/router/plans.ts @@ -16,7 +16,7 @@ export const plansRouter = { }, body: undefined, }); - return plans.plans_allowed; + return plans.plansAllowed; } catch (e) { if (e instanceof Error) { throw new Error(e.message); @@ -36,7 +36,7 @@ export const plansRouter = { }, body: undefined, }); - return plans.plans_existing; + return plans.plansExisting; } catch (e) { if (e instanceof Error) { throw new Error(e.message); From 919ee69a349caba9c6a452b105027ea45da435dc Mon Sep 17 00:00:00 2001 From: Bruno Carlos <6951456+brnovasco@users.noreply.github.com> Date: Tue, 12 Nov 2024 13:51:01 -0300 Subject: [PATCH 04/10] apps/spu-ui: new schemas for default plans - schema for default acquisition plan - schema for an example cleaning plan - auxiliary types for queue response related objects --- apps/spu-ui/src/lib/schemas/acquisition.ts | 197 +++++++++++++++++++++ apps/spu-ui/src/lib/schemas/cleaning.ts | 21 +++ apps/spu-ui/src/lib/schemas/queue.ts | 8 + apps/spu-ui/src/lib/schemas/table-item.ts | 24 +++ 4 files changed, 250 insertions(+) create mode 100644 apps/spu-ui/src/lib/schemas/acquisition.ts create mode 100644 apps/spu-ui/src/lib/schemas/cleaning.ts create mode 100644 apps/spu-ui/src/lib/schemas/queue.ts create mode 100644 apps/spu-ui/src/lib/schemas/table-item.ts diff --git a/apps/spu-ui/src/lib/schemas/acquisition.ts b/apps/spu-ui/src/lib/schemas/acquisition.ts new file mode 100644 index 0000000..9391a6d --- /dev/null +++ b/apps/spu-ui/src/lib/schemas/acquisition.ts @@ -0,0 +1,197 @@ +import { z } from "zod"; + +export const trayColumns = [ + "01", + "02", + "03", + "04", + "05", + "06", + "07", + "08", + "09", + "10", + "11", + "12", +] as const; +export const trayRows = ["A", "B", "C", "D", "E", "F", "G", "H"] as const; +export const trayOptions = ["Tray1", "Tray2"] as const; +export const acquireTimeOptions = [ + 200, 100, 50, 25, 12.5, 6.25, 3.125, 1.5625, 0.5, +] as const; + +// export const planName = "setup1_load_and_acquire"; +export const planName = "load_and_acquire_sim"; + +export const info = { + sampleType: "Type of the sample to be measured (buffer or sample)", + sampleTag: "Tag (Identifier) of the sample to be measured.", + bufferTag: "Buffer tag to be linked for this experiment.", + tray: "Desired tray for collection", + row: "Sample row in the Tray", + col: "Sample column in the Tray", + volume: "Amount of the sample to be collected in uL.", + acquireTime: + "The time for the acquisition of one sample in milliseconds in both pimega and picolo.", + numExposures: "Number of acquisitions to be made.", + expUvTime: "Exposure time of the UV-Vis spectrum.", + measureUvNumber: "Number of measurements in the UV-Vis spectrum.", + proposal: "Proposal associated with the experiment being executed.", +}; + +function checkSampleType(sampleType: string): boolean { + return ["buffer", "sample"].includes(sampleType); +} + +function checkColumn(colNumber: number): boolean { + const colString = colNumber + .toString() + .padStart(2, "0") as (typeof trayColumns)[number]; + return trayColumns.includes(colString); +} + +function checkRow(row: string): boolean { + return trayRows.includes(row as (typeof trayRows)[number]); +} + +function checkTray(tray: string): boolean { + return trayOptions.includes(tray as (typeof trayOptions)[number]); +} + +function checkAcquireTime(acquireTime: number): boolean { + return acquireTimeOptions.includes( + acquireTime as (typeof acquireTimeOptions)[number], + ); +} + +export const appSchema = z.object({ + sampleType: z + .string() + .min(1, "Sample type is required") + .refine((type) => checkSampleType(type), { + message: "Sample type must be 'buffer' or 'sample'", + }), + sampleTag: z + .string() + .min(1, "Sample name or other form of identification is required") + .regex(/^[a-zA-Z0-9_-]+$/, { + message: + "Sample tag can only contain letters, numbers, dashes, and underscores", + }), + bufferTag: z.string(), // validated when the full list of samples is parsed + tray: z.string().refine((tray) => checkTray(tray), { + message: `Tray must be one of the following options ${trayOptions.join(", ")}`, + }), + row: z.string().refine((row) => checkRow(row), { + message: `Row must be one of the following options ${trayRows.join(", ")}`, + }), + col: z.coerce.number().refine((col) => checkColumn(col), { + message: `Column must be one of the following options ${trayColumns.join(", ")}`, + }), + volume: z.coerce.number().min(0, "Volume must be a positive number"), + acquireTime: z + .string() + .transform((val) => Number(`${val}`.replace(",", "."))) + .pipe( + z.number().refine((acquireTime) => checkAcquireTime(acquireTime), { + message: `Acquire time (ms) must be one of the following options ${acquireTimeOptions.join("; ")}`, + }), + ), + numExposures: z.coerce + .number() + .min(1, "Number of exposures must be at least 1"), + expUvTime: z.coerce.number({ + message: "Exposure UV time must be a number", + }), + measureUvNumber: z.coerce.number({ + message: "Measure UV number must be a number", + }), + proposal: z.string().min(1, "Proposal is required"), +}); + +export const apiResponseSchema = z + .object({ + sample_tag: z.string(), + sample_type: z.string(), + buffer_tag: z.string(), + tray: z.string(), + row: z.string(), + col: z.number(), + volume: z.number(), + acquire_time: z.number(), + num_exposures: z.number(), + exp_uv_time: z.number(), + measure_uv_number: z.number(), + proposal: z.string(), + }) + .transform((data) => { + const { + sample_tag: sampleTag, + sample_type: sampleType, + buffer_tag: bufferTag, + acquire_time: acquireTime, + num_exposures: numExposures, + exp_uv_time: expUvTime, + measure_uv_number: measureUvNumber, + ...rest + } = data; + return { + sampleTag, + sampleType, + bufferTag, + acquireTime, + numExposures, + expUvTime, + measureUvNumber, + ...rest, + }; + }); + +export const apiSubmitSchema = z + .object({ + sampleTag: z.string(), + sampleType: z.string(), + bufferTag: z.string(), + tray: z.string(), + row: z.string(), + col: z.number(), + volume: z.number(), + acquireTime: z.number(), + numExposures: z.number(), + expUvTime: z.number(), + measureUvNumber: z.number(), + proposal: z.string(), + }) + .transform((data) => { + const { + sampleType, + sampleTag, + bufferTag, + tray, + row, + col, + volume, + acquireTime, + numExposures, + expUvTime, + measureUvNumber, + proposal, + } = data; + return { + sample_tag: sampleTag, + sample_type: sampleType, + buffer_tag: bufferTag, + tray, + row, + col, + volume, + acquire_time: acquireTime, + num_exposures: numExposures, + exp_uv_time: expUvTime, + measure_uv_number: measureUvNumber, + proposal, + }; + }); + +// for the submit plan endpoint we need to add the user proposal field +export type PlanKwargs = z.infer; diff --git a/apps/spu-ui/src/lib/schemas/cleaning.ts b/apps/spu-ui/src/lib/schemas/cleaning.ts new file mode 100644 index 0000000..65de885 --- /dev/null +++ b/apps/spu-ui/src/lib/schemas/cleaning.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +export const schema = z.object({ + water1: z.coerce.number().min(0, "Water 1 must be a positive number"), + agent1: z.coerce.number().min(0, "Agent 1 must be a positive number"), + water2: z.coerce.number().min(0, "Water 2 must be a positive number"), + agent2: z.coerce.number().min(0, "Agent 2 must be a positive number"), + air: z.coerce.number().min(0, "Air must be a positive number"), + saveReference: z.boolean(), + compareReference: z.boolean(), +}); + +export const info = { + water1: "Time in seconds that water will be applied before the agent1.", + agent1: "Time in seconds that the agent1 will be applied.", + water2: "Time in seconds that water will be applied before the agent2.", + agent2: "Time in seconds that the agent2 will be applied.", + air: "Time in seconds that air will be applied.", + saveReference: "Save the current data as reference.", + compareReference: "Compare the current data with the reference data.", +}; diff --git a/apps/spu-ui/src/lib/schemas/queue.ts b/apps/spu-ui/src/lib/schemas/queue.ts new file mode 100644 index 0000000..d82a197 --- /dev/null +++ b/apps/spu-ui/src/lib/schemas/queue.ts @@ -0,0 +1,8 @@ +import type { z } from "zod"; +import { schemas } from "@sophys-web/api"; + +const responseSchema = schemas.queue.getResponseSchema; +type QueueResponse = z.infer; +export type QueueItem = + | QueueResponse["items"][number] + | QueueResponse["runningItem"]; diff --git a/apps/spu-ui/src/lib/schemas/table-item.ts b/apps/spu-ui/src/lib/schemas/table-item.ts new file mode 100644 index 0000000..64f96df --- /dev/null +++ b/apps/spu-ui/src/lib/schemas/table-item.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; +import { appSchema as acquisitionSchema } from "./acquisition"; + +// the sampleTableItemSchema is the schema expected for all lines of the CSV file +// so it extends the sampleSubmitSchema and adds the order field used for +// submitting the full list of samples +export const tableItemSchema = acquisitionSchema + .omit({ proposal: true }) + .extend({ + order: z.coerce.number(), + cleaningProcedure: z.string().optional(), + }) + .superRefine((data, ctx) => { + if (data.sampleType !== "buffer") { + if (!data.bufferTag) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Buffer tag is required for non-buffer samples.`, + }); + } + } + }); + +export type TableItem = z.infer; From f78bd3d26ca9e6a8c38dfc05a2231b8711edfa16 Mon Sep 17 00:00:00 2001 From: Bruno Carlos <6951456+brnovasco@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:12:01 -0300 Subject: [PATCH 05/10] packages/api: update routes - add environment, queue and status routes based on added schemas. --- packages/api/src/root.ts | 6 ++ packages/api/src/router/environment.ts | 87 +++++++++++++++++ packages/api/src/router/queue.ts | 123 +++++++++++++++++++++++++ packages/api/src/router/status.ts | 28 ++++++ 4 files changed, 244 insertions(+) create mode 100644 packages/api/src/router/environment.ts create mode 100644 packages/api/src/router/queue.ts create mode 100644 packages/api/src/router/status.ts diff --git a/packages/api/src/root.ts b/packages/api/src/root.ts index 932fbba..e722305 100644 --- a/packages/api/src/root.ts +++ b/packages/api/src/root.ts @@ -1,12 +1,18 @@ import { devicesRouter } from "./router/devices"; +import { environmentRouter } from "./router/environment"; import { plansRouter } from "./router/plans"; import { postRouter } from "./router/post"; +import { queueRouter } from "./router/queue"; +import { statusRouter } from "./router/status"; import { createTRPCRouter } from "./trpc"; export const appRouter = createTRPCRouter({ post: postRouter, plans: plansRouter, devices: devicesRouter, + queue: queueRouter, + environment: environmentRouter, + status: statusRouter, }); // export type definition of API diff --git a/packages/api/src/router/environment.ts b/packages/api/src/router/environment.ts new file mode 100644 index 0000000..a67e41c --- /dev/null +++ b/packages/api/src/router/environment.ts @@ -0,0 +1,87 @@ +import { createZodFetcher } from "zod-fetch"; +import { env } from "../../env"; +import commonSchemas from "../schemas/common"; +import { protectedProcedure } from "../trpc"; + +export const environmentRouter = { + open: protectedProcedure.mutation(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/environment/open`; + const fetchWithZod = createZodFetcher(); + try { + const response = await fetchWithZod(commonSchemas.response, fetchURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, + }, + body: "{}", + }); + return response; + } catch (e) { + if (e instanceof Error) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), + close: protectedProcedure.mutation(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/environment/close`; + const fetchWithZod = createZodFetcher(); + try { + const response = await fetchWithZod(commonSchemas.response, fetchURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, + }, + body: "{}", + }); + return response; + } catch (e) { + if (e instanceof Error) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), + update: protectedProcedure.mutation(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/environment/update`; + const fetchWithZod = createZodFetcher(); + try { + const response = await fetchWithZod(commonSchemas.response, fetchURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, + }, + body: "{}", + }); + return response; + } catch (e) { + if (e instanceof Error) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), + destroy: protectedProcedure.mutation(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/environment/destroy`; + const fetchWithZod = createZodFetcher(); + try { + const response = await fetchWithZod(commonSchemas.response, fetchURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, + }, + body: "{}", + }); + return response; + } catch (e) { + if (e instanceof Error) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), +} as const; diff --git a/packages/api/src/router/queue.ts b/packages/api/src/router/queue.ts new file mode 100644 index 0000000..b608d79 --- /dev/null +++ b/packages/api/src/router/queue.ts @@ -0,0 +1,123 @@ +import { createZodFetcher } from "zod-fetch"; +import { env } from "../../env"; +import commonSchemas from "../schemas/common"; +import item from "../schemas/item"; +import queue from "../schemas/queue"; +import { protectedProcedure } from "../trpc"; + +const itemRouter = { + add: protectedProcedure + .input(item.addSubmit) + .mutation(async ({ ctx, input }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/queue/item/add`; + const fetchWithZod = createZodFetcher(); + const body = JSON.stringify(input); + try { + const res = await fetchWithZod(item.addResponse, fetchURL, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, + }, + body, + }); + if (res.success === false) { + throw new Error(res.msg); + } + return res; + } catch (e) { + if (e instanceof Error) { + console.error(e.message); + throw new Error(e.message); + } + console.error("Unknown error", e); + throw new Error("Unknown error"); + } + }), +} as const; + +export const queueRouter = { + item: itemRouter, + get: protectedProcedure.query(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/queue/get`; + const fetchWithZod = createZodFetcher(); + try { + const res = await fetchWithZod(queue.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) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), + start: protectedProcedure.mutation(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/queue/start`; + 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) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), + stop: protectedProcedure.mutation(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/queue/stop`; + 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) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), + clear: protectedProcedure.mutation(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/queue/clear`; + const fetchWithZod = createZodFetcher(); + try { + const res = await fetchWithZod(commonSchemas.response, fetchURL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + Authorization: `Bearer ${ctx.session.user.blueskyAccessToken}`, + }, + body: undefined, + }); + return res; + } catch (e) { + if (e instanceof Error) { + throw new Error(e.message); + } + throw new Error("Unknown error"); + } + }), +} as const; diff --git a/packages/api/src/router/status.ts b/packages/api/src/router/status.ts new file mode 100644 index 0000000..45f2568 --- /dev/null +++ b/packages/api/src/router/status.ts @@ -0,0 +1,28 @@ +import { createZodFetcher } from "zod-fetch"; +import { env } from "../../env"; +import schemas from "../schemas/status"; +import { protectedProcedure } from "../trpc"; + +export const statusRouter = { + get: protectedProcedure.query(async ({ ctx }) => { + const fetchURL = `${env.BLUESKY_HTTPSERVER_URL}/api/status`; + const fetchWithZod = createZodFetcher(); + try { + const res = await fetchWithZod(schemas.getResponse, 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"); + } + }), +} as const; From 82f1aa0e50cf71a0d3320a16522260c45d2c9448 Mon Sep 17 00:00:00 2001 From: Bruno Carlos <6951456+brnovasco@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:13:58 -0300 Subject: [PATCH 06/10] apps/spu-ui: add component for control queue environment - view status - open and update environment --- apps/spu-ui/src/app/_components/env-menu.tsx | 90 ++++++++ .../spu-ui/src/app/_components/experiment.tsx | 6 +- packages/ui/package.json | 1 + packages/ui/src/dropdown-menu.tsx | 199 ++++++++++++++++++ pnpm-lock.yaml | 70 ++++++ 5 files changed, 364 insertions(+), 2 deletions(-) create mode 100644 apps/spu-ui/src/app/_components/env-menu.tsx create mode 100644 packages/ui/src/dropdown-menu.tsx diff --git a/apps/spu-ui/src/app/_components/env-menu.tsx b/apps/spu-ui/src/app/_components/env-menu.tsx new file mode 100644 index 0000000..92a2e7f --- /dev/null +++ b/apps/spu-ui/src/app/_components/env-menu.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { buttonVariants } from "@sophys-web/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@sophys-web/ui/dropdown-menu"; +import { api } from "../../trpc/react"; + +export function EnvMenu() { + const apiUtils = api.useUtils(); + + const { + data: status, + isError, + isLoading, + } = api.status.get.useQuery(undefined); + + const { mutate: envUpdate } = api.environment.update.useMutation({ + onSuccess: async () => { + await apiUtils.status.get.invalidate(); + }, + }); + + const { mutate: envOpen } = api.environment.open.useMutation({ + onSuccess: async () => { + await apiUtils.status.get.invalidate(); + }, + }); + + const statusMessage = () => { + if (isLoading) { + return "Loading..."; + } + if (isError) { + return "Error"; + } + return status?.reState || "Unknown"; + }; + + return ( + + { + await apiUtils.status.get.invalidate(); + }} + > + Status: {statusMessage()} + + + Env controls + { + envUpdate(); + }} + > + Update + + + { + envOpen(); + }} + > + Open + + + Full status +
+ {status + ? Object.entries(status).map(([key, value]) => ( +

+ {key}: {value?.toString()} +

+ )) + : null} +
+
+
+ ); +} diff --git a/apps/spu-ui/src/app/_components/experiment.tsx b/apps/spu-ui/src/app/_components/experiment.tsx index 629a124..e2c71ef 100644 --- a/apps/spu-ui/src/app/_components/experiment.tsx +++ b/apps/spu-ui/src/app/_components/experiment.tsx @@ -19,6 +19,7 @@ import { clearSamples as clearServerSamples, setSamples as setServerSamples, } from "../actions/samples"; +import { EnvMenu } from "./env-menu"; import { Queue } from "./queue"; import { SampleItem } from "./sample"; import { Tray } from "./tray"; @@ -237,8 +238,9 @@ export default function Experiment({
-

Experiment Queue

-
+
+

Experiment Queue

+