From 8821f0d72b5835d22faae5cbf530c52ae3c61e89 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 11:44:42 +0100 Subject: [PATCH 01/11] Queue in run table and filtering --- .../app/components/runs/v3/RunFilters.tsx | 189 +++++++++++++++++- .../app/components/runs/v3/TaskRunsTable.tsx | 21 +- .../app/presenters/RunFilters.server.ts | 7 +- .../v3/NextRunListPresenter.server.ts | 8 + .../app/services/runsRepository.server.ts | 11 + 5 files changed, 232 insertions(+), 4 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 403690aa11..19bd8bc4e7 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -3,6 +3,7 @@ import { CalendarIcon, ClockIcon, FingerPrintIcon, + RectangleStackIcon, Squares2X2Icon, TagIcon, XMarkIcon, @@ -41,9 +42,12 @@ import { TooltipProvider, TooltipTrigger, } from "~/components/primitives/Tooltip"; +import { useEnvironment } from "~/hooks/useEnvironment"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; +import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; +import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags"; import { Button } from "../../primitives/Buttons"; import { BulkActionTypeCombo } from "./BulkAction"; @@ -105,6 +109,7 @@ export const TaskRunListSearchFilters = z.object({ batchId: z.string().optional(), runId: StringOrStringArray, scheduleId: z.string().optional(), + queues: StringOrStringArray, }); export type TaskRunListSearchFilters = z.infer; @@ -138,6 +143,8 @@ export function filterTitle(filterKey: string) { return "Run ID"; case "scheduleId": return "Schedule ID"; + case "queues": + return "Queues"; default: return filterKey; } @@ -170,6 +177,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined { return ; case "scheduleId": return ; + case "queues": + return ; default: return undefined; } @@ -204,6 +213,10 @@ export function getRunFiltersFromSearchParams( : undefined, batchId: searchParams.get("batchId") ?? undefined, scheduleId: searchParams.get("scheduleId") ?? undefined, + queues: + searchParams.getAll("queues").filter((v) => v.length > 0).length > 0 + ? searchParams.getAll("queues") + : undefined, }; const parsed = TaskRunListSearchFilters.safeParse(params); @@ -237,7 +250,8 @@ export function RunsFilters(props: RunFiltersProps) { searchParams.has("tags") || searchParams.has("batchId") || searchParams.has("runId") || - searchParams.has("scheduleId"); + searchParams.has("scheduleId") || + searchParams.has("queues"); return (
@@ -265,6 +279,7 @@ const filterTypes = [ }, { name: "tasks", title: "Tasks", icon: }, { name: "tags", title: "Tags", icon: }, + { name: "queues", title: "Queues", icon: }, { name: "run", title: "Run ID", icon: }, { name: "batch", title: "Batch ID", icon: }, { name: "schedule", title: "Schedule ID", icon: }, @@ -315,6 +330,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) { + @@ -343,6 +359,8 @@ function Menu(props: MenuProps) { return props.setFilterType(undefined)} {...props} />; case "tags": return props.setFilterType(undefined)} {...props} />; + case "queues": + return props.setFilterType(undefined)} {...props} />; case "run": return props.setFilterType(undefined)} {...props} />; case "batch": @@ -806,6 +824,175 @@ function AppliedTagsFilter() { ); } +function QueuesDropdown({ + trigger, + clearSearchValue, + searchValue, + onClose, +}: { + trigger: ReactNode; + clearSearchValue: () => void; + searchValue: string; + onClose?: () => void; +}) { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const { values, replace } = useSearchParams(); + + const handleChange = (values: string[]) => { + clearSearchValue(); + replace({ + queues: values.length > 0 ? values : undefined, + cursor: undefined, + direction: undefined, + }); + }; + + const queueValues = values("queues").filter((v) => v !== ""); + const selected = queueValues.length > 0 ? queueValues : undefined; + + const fetcher = useFetcher(); + + useEffect(() => { + const searchParams = new URLSearchParams(); + searchParams.set("per_page", "25"); + if (searchValue) { + searchParams.set("query", encodeURIComponent(searchValue)); + } + fetcher.load( + `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${ + environment.slug + }/queues?${searchParams.toString()}` + ); + }, [searchValue]); + + const filtered = useMemo(() => { + console.log(fetcher.data); + let items: { name: string; type: "custom" | "task"; value: string }[] = []; + if (searchValue === "") { + // items = selected ?? []; + items = []; + } + + for (const queueName of selected ?? []) { + const queueItem = fetcher.data?.queues.find((q) => q.name === queueName); + if (!queueItem) { + if (queueName.startsWith("task/")) { + items.push({ + name: queueName.replace("task/", ""), + type: "task", + value: queueName, + }); + } else { + items.push({ + name: queueName, + type: "custom", + value: queueName, + }); + } + } + } + + if (fetcher.data === undefined) { + return matchSorter(items, searchValue); + } + + items.push( + ...fetcher.data.queues.map((q) => ({ + name: q.name, + type: q.type, + value: q.type === "task" ? `task/${q.name}` : q.name, + })) + ); + + return matchSorter(Array.from(new Set(items)), searchValue, { + keys: ["name"], + }); + }, [searchValue, fetcher.data]); + + return ( + + {trigger} + { + if (onClose) { + onClose(); + return false; + } + + return true; + }} + > + ( +
+ + {fetcher.state === "loading" && } +
+ )} + /> + + {filtered.length > 0 + ? filtered.map((queue) => ( + + ) : ( + + ) + } + > + {queue.name} + + )) + : null} + {filtered.length === 0 && fetcher.state !== "loading" && ( + No queues found + )} + +
+
+ ); +} + +function AppliedQueuesFilter() { + const { values, del } = useSearchParams(); + + const queues = values("queues"); + + if (queues.length === 0 || queues.every((v) => v === "")) { + return null; + } + + return ( + + {(search, setSearch) => ( + }> + v.replace("task/", "")))} + onRemove={() => del(["queues", "cursor", "direction"])} + variant="secondary/small" + /> + + } + searchValue={search} + clearSearchValue={() => setSearch("")} + /> + )} + + ); +} + function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { const { value, values, replace } = useSearchParams(); const searchValue = value("rootOnly"); diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 4a76cd18dd..ba87909492 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -59,6 +59,7 @@ import { import { MachineIcon } from "~/assets/icons/MachineIcon"; import { MachineLabelCombo } from "~/components/MachineLabelCombo"; import { MachineTooltipInfo } from "~/components/MachineTooltipInfo"; +import { TaskIconSmall } from "~/assets/icons/TaskIcon"; type RunsTableProps = { total: number; @@ -82,9 +83,8 @@ export function TaskRunsTable({ }: RunsTableProps) { const organization = useOrganization(); const project = useProject(); - const environment = useEnvironment(); const checkboxes = useRef<(HTMLInputElement | null)[]>([]); - const { selectedItems, has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection); + const { has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection); const { isManagedCloud } = useFeatures(); const showCompute = isManagedCloud; @@ -213,6 +213,7 @@ export function TaskRunsTable({ Test Created at + Queue @@ -394,6 +395,22 @@ export function TaskRunsTable({ {run.createdAt ? : "–"} + + + {run.queue.type === "task" ? ( + } + content={`This queue was automatically created from your "${run.queue.name}" task`} + /> + ) : ( + } + content={`This is a custom queue you added in your code.`} + /> + )} + {run.queue.name} + + {run.delayUntil ? : "–"} diff --git a/apps/webapp/app/presenters/RunFilters.server.ts b/apps/webapp/app/presenters/RunFilters.server.ts index 37a5a4755b..adc224a3ed 100644 --- a/apps/webapp/app/presenters/RunFilters.server.ts +++ b/apps/webapp/app/presenters/RunFilters.server.ts @@ -3,8 +3,11 @@ import { TaskRunListSearchFilters, } from "~/components/runs/v3/RunFilters"; import { getRootOnlyFilterPreference } from "~/services/preferences/uiPreferences.server"; +import { type ParsedRunFilters } from "~/services/runsRepository.server"; -export async function getRunFiltersFromRequest(request: Request) { +type FiltersFromRequest = ParsedRunFilters & Required>; + +export async function getRunFiltersFromRequest(request: Request): Promise { const url = new URL(request.url); let rootOnlyValue = false; if (url.searchParams.has("rootOnly")) { @@ -29,6 +32,7 @@ export async function getRunFiltersFromRequest(request: Request) { runId, batchId, scheduleId, + queues, } = TaskRunListSearchFilters.parse(s); return { @@ -46,5 +50,6 @@ export async function getRunFiltersFromRequest(request: Request) { rootOnly: rootOnlyValue, direction: direction, cursor: cursor, + queues, }; } diff --git a/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts b/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts index 9217c5039d..74e4ac2a0a 100644 --- a/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts @@ -30,6 +30,7 @@ export type RunListOptions = { rootOnly?: boolean; batchId?: string; runId?: string[]; + queues?: string[]; //pagination direction?: Direction; cursor?: string; @@ -65,6 +66,7 @@ export class NextRunListPresenter { rootOnly, batchId, runId, + queues, from, to, direction = "forward", @@ -90,6 +92,7 @@ export class NextRunListPresenter { (tags !== undefined && tags.length > 0) || batchId !== undefined || (runId !== undefined && runId.length > 0) || + (queues !== undefined && queues.length > 0) || typeof isTest === "boolean" || rootOnly === true || !time.isDefault; @@ -173,6 +176,7 @@ export class NextRunListPresenter { batchId, runId, bulkId, + queues, page: { size: pageSize, cursor, @@ -233,6 +237,10 @@ export class NextRunListPresenter { metadata: run.metadata, metadataType: run.metadataType, machinePreset: run.machinePreset ? machinePresetFromRun(run)?.name : undefined, + queue: { + name: run.queue.replace("task/", ""), + type: run.queue.startsWith("task/") ? "task" : "custom", + }, }; }), pagination: { diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts index acc3670206..db7e2f4b0c 100644 --- a/apps/webapp/app/services/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository.server.ts @@ -36,6 +36,7 @@ const RunListInputOptionsSchema = z.object({ batchId: z.string().optional(), runId: z.array(z.string()).optional(), bulkId: z.string().optional(), + queues: z.array(z.string()).optional(), }); export type RunListInputOptions = z.infer; @@ -44,6 +45,11 @@ export type RunListInputFilters = Omit< "organizationId" | "projectId" | "environmentId" >; +export type ParsedRunFilters = RunListInputFilters & { + cursor?: string; + direction?: "forward" | "backward"; +}; + type FilterRunsOptions = Omit & { period: number | undefined; }; @@ -170,6 +176,7 @@ export class RunsRepository { metadata: true, metadataType: true, machinePreset: true, + queue: true, }, }); @@ -353,6 +360,10 @@ function applyRunFiltersToQueryBuilder( runIds: options.runId.map((runId) => RunId.toFriendlyId(runId)), }); } + + if (options.queues && options.queues.length > 0) { + queryBuilder.where("queue IN {queues: Array(String)}", { queues: options.queues }); + } } export function parseRunListInputOptions(data: any): RunListInputOptions { From 91a944f26fd81c1922cc0ebf0dfa5a904a6141d2 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 12:33:35 +0100 Subject: [PATCH 02/11] Debounce the filter changes --- .../app/components/runs/v3/RunFilters.tsx | 29 +++++++++++-------- apps/webapp/app/hooks/useDebounce.ts | 24 ++++++++++++++- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 19bd8bc4e7..c89133efb5 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -60,6 +60,7 @@ import { TaskRunStatusCombo, } from "./TaskRunStatus"; import { TaskTriggerSourceIcon } from "./TaskTriggerSource"; +import { useDebounceEffect } from "~/hooks/useDebounce"; export const RunStatus = z.enum(allTaskRunStatuses); @@ -854,18 +855,22 @@ function QueuesDropdown({ const fetcher = useFetcher(); - useEffect(() => { - const searchParams = new URLSearchParams(); - searchParams.set("per_page", "25"); - if (searchValue) { - searchParams.set("query", encodeURIComponent(searchValue)); - } - fetcher.load( - `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${ - environment.slug - }/queues?${searchParams.toString()}` - ); - }, [searchValue]); + useDebounceEffect( + searchValue, + (s) => { + const searchParams = new URLSearchParams(); + searchParams.set("per_page", "25"); + if (searchValue) { + searchParams.set("query", encodeURIComponent(s)); + } + fetcher.load( + `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${ + environment.slug + }/queues?${searchParams.toString()}` + ); + }, + 250 + ); const filtered = useMemo(() => { console.log(fetcher.data); diff --git a/apps/webapp/app/hooks/useDebounce.ts b/apps/webapp/app/hooks/useDebounce.ts index a8670caf7f..da63330f2a 100644 --- a/apps/webapp/app/hooks/useDebounce.ts +++ b/apps/webapp/app/hooks/useDebounce.ts @@ -1,4 +1,4 @@ -import { useRef } from "react"; +import { useEffect, useRef } from "react"; /** * A function that you call with a debounce delay, the function will only be called after the delay has passed @@ -19,3 +19,25 @@ export function useDebounce any>(fn: T, delay: num }, delay); }; } + +/** + * A function that takes in a value, function, and delay. + * It will run the function with the debounced value, only if the value has changed. + * It should deal with the function being passed in not being a useCallback + */ +export function useDebounceEffect(value: T, fn: (value: T) => void, delay: number) { + const fnRef = useRef(fn); + + // Update the ref whenever the function changes + fnRef.current = fn; + + useEffect(() => { + const timeout = setTimeout(() => { + fnRef.current(value); + }, delay); + + return () => { + clearTimeout(timeout); + }; + }, [value, delay]); // Only depend on value and delay, not fn +} From de85cb281b43525192f14e800e7f1d7cdb184500 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 12:38:36 +0100 Subject: [PATCH 03/11] Remove console log --- apps/webapp/app/components/runs/v3/RunFilters.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index c89133efb5..040de3aac8 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -873,7 +873,6 @@ function QueuesDropdown({ ); const filtered = useMemo(() => { - console.log(fetcher.data); let items: { name: string; type: "custom" | "task"; value: string }[] = []; if (searchValue === "") { // items = selected ?? []; From ce6189a5561f2e37a4533ecfda0afed52619cf07 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 15:58:14 +0100 Subject: [PATCH 04/11] Added machine filtering --- apps/webapp/app/assets/icons/MachineIcon.tsx | 2 +- .../app/components/MachineLabelCombo.tsx | 4 +- .../app/components/runs/v3/RunFilters.tsx | 149 +++++++++++++++++- .../app/components/runs/v3/TaskRunsTable.tsx | 27 ++-- .../app/presenters/RunFilters.server.ts | 2 + .../v3/NextRunListPresenter.server.ts | 5 + .../webapp/app/services/platform.v3.server.ts | 1 - .../app/services/runsRepository.server.ts | 8 + apps/webapp/app/utils/cn.ts | 81 ++++++++++ 9 files changed, 252 insertions(+), 27 deletions(-) diff --git a/apps/webapp/app/assets/icons/MachineIcon.tsx b/apps/webapp/app/assets/icons/MachineIcon.tsx index a58023a283..f07e7467b0 100644 --- a/apps/webapp/app/assets/icons/MachineIcon.tsx +++ b/apps/webapp/app/assets/icons/MachineIcon.tsx @@ -27,7 +27,7 @@ export function MachineIcon({ preset, className }: { preset?: string; className? } } -function MachineDefaultIcon({ className }: { className?: string }) { +export function MachineDefaultIcon({ className }: { className?: string }) { return ( { return undefined; }, z.string().array().optional()); +export const MachinePresetOrMachinePresetArray = z.preprocess((value) => { + if (typeof value === "string") { + if (value.length > 0) { + const parsed = MachinePresetName.safeParse(value); + return parsed.success ? [parsed.data] : undefined; + } + + return undefined; + } + + if (Array.isArray(value)) { + return value + .filter((v) => typeof v === "string" && v.length > 0) + .map((v) => MachinePresetName.safeParse(v)) + .filter((result) => result.success) + .map((result) => result.data); + } + + return undefined; +}, MachinePresetName.array().optional()); + export const TaskRunListSearchFilters = z.object({ cursor: z.string().optional(), direction: z.enum(["forward", "backward"]).optional(), @@ -111,6 +139,7 @@ export const TaskRunListSearchFilters = z.object({ runId: StringOrStringArray, scheduleId: z.string().optional(), queues: StringOrStringArray, + machines: MachinePresetOrMachinePresetArray, }); export type TaskRunListSearchFilters = z.infer; @@ -146,6 +175,8 @@ export function filterTitle(filterKey: string) { return "Schedule ID"; case "queues": return "Queues"; + case "machines": + return "Machine"; default: return filterKey; } @@ -157,7 +188,7 @@ export function filterIcon(filterKey: string): ReactNode | undefined { case "direction": return undefined; case "statuses": - return ; + return ; case "tasks": return ; case "tags": @@ -180,6 +211,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined { return ; case "queues": return ; + case "machines": + return ; default: return undefined; } @@ -218,6 +251,10 @@ export function getRunFiltersFromSearchParams( searchParams.getAll("queues").filter((v) => v.length > 0).length > 0 ? searchParams.getAll("queues") : undefined, + machines: + searchParams.getAll("machines").filter((v) => v.length > 0).length > 0 + ? searchParams.getAll("machines") + : undefined, }; const parsed = TaskRunListSearchFilters.safeParse(params); @@ -252,7 +289,8 @@ export function RunsFilters(props: RunFiltersProps) { searchParams.has("batchId") || searchParams.has("runId") || searchParams.has("scheduleId") || - searchParams.has("queues"); + searchParams.has("queues") || + searchParams.has("machines"); return (
@@ -276,11 +314,12 @@ const filterTypes = [ { name: "statuses", title: "Status", - icon: , + icon: , }, { name: "tasks", title: "Tasks", icon: }, { name: "tags", title: "Tags", icon: }, { name: "queues", title: "Queues", icon: }, + { name: "machines", title: "Machines", icon: }, { name: "run", title: "Run ID", icon: }, { name: "batch", title: "Batch ID", icon: }, { name: "schedule", title: "Schedule ID", icon: }, @@ -332,6 +371,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) { + @@ -362,6 +402,8 @@ function Menu(props: MenuProps) { return props.setFilterType(undefined)} {...props} />; case "queues": return props.setFilterType(undefined)} {...props} />; + case "machines": + return props.setFilterType(undefined)} {...props} />; case "run": return props.setFilterType(undefined)} {...props} />; case "batch": @@ -874,10 +916,6 @@ function QueuesDropdown({ const filtered = useMemo(() => { let items: { name: string; type: "custom" | "task"; value: string }[] = []; - if (searchValue === "") { - // items = selected ?? []; - items = []; - } for (const queueName of selected ?? []) { const queueItem = fetcher.data?.queues.find((q) => q.name === queueName); @@ -997,6 +1035,101 @@ function AppliedQueuesFilter() { ); } +function MachinesDropdown({ + trigger, + clearSearchValue, + searchValue, + onClose, +}: { + trigger: ReactNode; + clearSearchValue: () => void; + searchValue: string; + onClose?: () => void; +}) { + const { values, replace } = useSearchParams(); + + const handleChange = (values: string[]) => { + clearSearchValue(); + replace({ machines: values, cursor: undefined, direction: undefined }); + }; + + const filtered = useMemo(() => { + if (searchValue === "") { + return machines; + } + return matchSorter(machines, searchValue); + }, [searchValue]); + + return ( + + {trigger} + { + if (onClose) { + onClose(); + return false; + } + + return true; + }} + > + + + {filtered.map((item, index) => ( + + + + ))} + + + + ); +} + +function AppliedMachinesFilter() { + const { values, del } = useSearchParams(); + const machines = values("machines"); + + if (machines.length === 0 || machines.every((v) => v === "")) { + return null; + } + + return ( + + {(search, setSearch) => ( + }> + { + const parsed = MachinePresetName.safeParse(v); + if (!parsed.success) { + return v; + } + return formatMachinePresetName(parsed.data); + }) + )} + onRemove={() => del(["machines", "cursor", "direction"])} + variant="secondary/small" + /> + + } + searchValue={search} + clearSearchValue={() => setSearch("")} + /> + )} + + ); +} + function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { const { value, values, replace } = useSearchParams(); const searchValue = value("rootOnly"); diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index ba87909492..8abca02eef 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -8,12 +8,11 @@ import { } from "@heroicons/react/20/solid"; import { BeakerIcon, BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid"; import { useLocation } from "@remix-run/react"; -import { - formatDuration, - formatDurationMilliseconds, - MachinePresetName, -} from "@trigger.dev/core/v3"; +import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3"; import { useCallback, useRef } from "react"; +import { TaskIconSmall } from "~/assets/icons/TaskIcon"; +import { MachineLabelCombo } from "~/components/MachineLabelCombo"; +import { MachineTooltipInfo } from "~/components/MachineTooltipInfo"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Checkbox } from "~/components/primitives/Checkbox"; @@ -56,10 +55,6 @@ import { filterableTaskRunStatuses, TaskRunStatusCombo, } from "./TaskRunStatus"; -import { MachineIcon } from "~/assets/icons/MachineIcon"; -import { MachineLabelCombo } from "~/components/MachineLabelCombo"; -import { MachineTooltipInfo } from "~/components/MachineTooltipInfo"; -import { TaskIconSmall } from "~/assets/icons/TaskIcon"; type RunsTableProps = { total: number; @@ -211,9 +206,9 @@ export function TaskRunsTable({ }> Machine + Queue Test Created at - Queue @@ -389,12 +384,6 @@ export function TaskRunsTable({ - - {run.isTest ? : "–"} - - - {run.createdAt ? : "–"} - {run.queue.type === "task" ? ( @@ -411,6 +400,12 @@ export function TaskRunsTable({ {run.queue.name} + + {run.isTest ? : "–"} + + + {run.createdAt ? : "–"} + {run.delayUntil ? : "–"} diff --git a/apps/webapp/app/presenters/RunFilters.server.ts b/apps/webapp/app/presenters/RunFilters.server.ts index adc224a3ed..db19e65656 100644 --- a/apps/webapp/app/presenters/RunFilters.server.ts +++ b/apps/webapp/app/presenters/RunFilters.server.ts @@ -33,6 +33,7 @@ export async function getRunFiltersFromRequest(request: Request): Promise 0) || (queues !== undefined && queues.length > 0) || + (machines !== undefined && machines.length > 0) || typeof isTest === "boolean" || rootOnly === true || !time.isDefault; @@ -177,6 +181,7 @@ export class NextRunListPresenter { runId, bulkId, queues, + machines, page: { size: pageSize, cursor, diff --git a/apps/webapp/app/services/platform.v3.server.ts b/apps/webapp/app/services/platform.v3.server.ts index 928ace6592..40d0c87bf4 100644 --- a/apps/webapp/app/services/platform.v3.server.ts +++ b/apps/webapp/app/services/platform.v3.server.ts @@ -12,7 +12,6 @@ import { import { createCache, DefaultStatefulContext, Namespace } from "@unkey/cache"; import { MemoryStore } from "@unkey/cache/stores"; import { redirect } from "remix-typedjson"; -import { $replica } from "~/db.server"; import { env } from "~/env.server"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { createEnvironment } from "~/models/organization.server"; diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts index db7e2f4b0c..b1e8205254 100644 --- a/apps/webapp/app/services/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository.server.ts @@ -7,6 +7,7 @@ import { timeFilters } from "~/components/runs/v3/SharedFilters"; import { type PrismaClient } from "~/db.server"; import { z } from "zod"; import { BulkActionId, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { MachinePresetName } from "@trigger.dev/core/v3"; export type RunsRepositoryOptions = { clickhouse: ClickHouse; @@ -37,6 +38,7 @@ const RunListInputOptionsSchema = z.object({ runId: z.array(z.string()).optional(), bulkId: z.string().optional(), queues: z.array(z.string()).optional(), + machines: MachinePresetName.array().optional(), }); export type RunListInputOptions = z.infer; @@ -364,6 +366,12 @@ function applyRunFiltersToQueryBuilder( if (options.queues && options.queues.length > 0) { queryBuilder.where("queue IN {queues: Array(String)}", { queues: options.queues }); } + + if (options.machines && options.machines.length > 0) { + queryBuilder.where("machine_preset IN {machines: Array(String)}", { + machines: options.machines, + }); + } } export function parseRunListInputOptions(data: any): RunListInputOptions { diff --git a/apps/webapp/app/utils/cn.ts b/apps/webapp/app/utils/cn.ts index d33fe61b52..ef578364ce 100644 --- a/apps/webapp/app/utils/cn.ts +++ b/apps/webapp/app/utils/cn.ts @@ -25,6 +25,87 @@ const customTwMerge = extendTailwindMerge({ ], }, ], + size: [ + { + size: [ + "0", + "0.5", + "1", + "1.5", + "2", + "2.5", + "3", + "3.5", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "14", + "16", + "20", + "24", + "28", + "32", + "36", + "40", + "44", + "48", + "52", + "56", + "60", + "64", + "72", + "80", + "96", + "auto", + "px", + "0.5", + "1", + "1.5", + "2", + "2.5", + "3", + "3.5", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "14", + "16", + "20", + "24", + "28", + "32", + "36", + "40", + "44", + "48", + "52", + "56", + "60", + "64", + "72", + "80", + "96", + "auto", + "px", + "full", + "min", + "max", + "fit", + ], + }, + ], }, }); From 13a57af308b4670e4b739870cd63739b75754e59 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Thu, 17 Jul 2025 18:38:13 +0100 Subject: [PATCH 05/11] Added version filtering --- .../app/components/runs/v3/RunFilters.tsx | 166 +++++++++++++++++- .../v3/VersionListPresenter.server.ts | 69 ++++++++ ...ts.$projectParam.env.$envParam.versions.ts | 55 ++++++ .../app/services/runsRepository.server.ts | 8 +- 4 files changed, 292 insertions(+), 6 deletions(-) create mode 100644 apps/webapp/app/presenters/v3/VersionListPresenter.server.ts create mode 100644 apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts diff --git a/apps/webapp/app/components/runs/v3/RunFilters.tsx b/apps/webapp/app/components/runs/v3/RunFilters.tsx index 57f2c15a56..a44cd808a3 100644 --- a/apps/webapp/app/components/runs/v3/RunFilters.tsx +++ b/apps/webapp/app/components/runs/v3/RunFilters.tsx @@ -9,7 +9,7 @@ import { XMarkIcon, } from "@heroicons/react/20/solid"; import { Form, useFetcher } from "@remix-run/react"; -import { IconToggleLeft } from "@tabler/icons-react"; +import { IconToggleLeft, IconRotateClockwise2 } from "@tabler/icons-react"; import { MachinePresetName } from "@trigger.dev/core/v3"; import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database"; import { ListFilterIcon } from "lucide-react"; @@ -57,6 +57,7 @@ import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags"; +import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions"; import { Button } from "../../primitives/Buttons"; import { BulkActionTypeCombo } from "./BulkAction"; import { appliedSummary, FilterMenuProvider, TimeFilter } from "./SharedFilters"; @@ -68,6 +69,7 @@ import { TaskRunStatusCombo, } from "./TaskRunStatus"; import { TaskTriggerSourceIcon } from "./TaskTriggerSource"; +import { Badge } from "~/components/primitives/Badge"; export const RunStatus = z.enum(allTaskRunStatuses); @@ -177,6 +179,8 @@ export function filterTitle(filterKey: string) { return "Queues"; case "machines": return "Machine"; + case "versions": + return "Version"; default: return filterKey; } @@ -213,6 +217,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined { return ; case "machines": return ; + case "versions": + return ; default: return undefined; } @@ -255,6 +261,10 @@ export function getRunFiltersFromSearchParams( searchParams.getAll("machines").filter((v) => v.length > 0).length > 0 ? searchParams.getAll("machines") : undefined, + versions: + searchParams.getAll("versions").filter((v) => v.length > 0).length > 0 + ? searchParams.getAll("versions") + : undefined, }; const parsed = TaskRunListSearchFilters.safeParse(params); @@ -290,7 +300,8 @@ export function RunsFilters(props: RunFiltersProps) { searchParams.has("runId") || searchParams.has("scheduleId") || searchParams.has("queues") || - searchParams.has("machines"); + searchParams.has("machines") || + searchParams.has("versions"); return (
@@ -318,6 +329,7 @@ const filterTypes = [ }, { name: "tasks", title: "Tasks", icon: }, { name: "tags", title: "Tags", icon: }, + { name: "versions", title: "Versions", icon: }, { name: "queues", title: "Queues", icon: }, { name: "machines", title: "Machines", icon: }, { name: "run", title: "Run ID", icon: }, @@ -370,6 +382,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) { + @@ -410,6 +423,8 @@ function Menu(props: MenuProps) { return props.setFilterType(undefined)} {...props} />; case "schedule": return props.setFilterType(undefined)} {...props} />; + case "versions": + return props.setFilterType(undefined)} {...props} />; } } @@ -1130,6 +1145,153 @@ function AppliedMachinesFilter() { ); } +function VersionsDropdown({ + trigger, + clearSearchValue, + searchValue, + onClose, +}: { + trigger: ReactNode; + clearSearchValue: () => void; + searchValue: string; + onClose?: () => void; +}) { + const organization = useOrganization(); + const project = useProject(); + const environment = useEnvironment(); + const { values, replace } = useSearchParams(); + + const handleChange = (values: string[]) => { + clearSearchValue(); + replace({ + versions: values.length > 0 ? values : undefined, + cursor: undefined, + direction: undefined, + }); + }; + + const versionValues = values("versions").filter((v) => v !== ""); + const selected = versionValues.length > 0 ? versionValues : undefined; + + const fetcher = useFetcher(); + + useDebounceEffect( + searchValue, + (s) => { + const searchParams = new URLSearchParams(); + if (searchValue) { + searchParams.set("query", encodeURIComponent(s)); + } + fetcher.load( + `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${ + environment.slug + }/versions?${searchParams.toString()}` + ); + }, + 250 + ); + + const filtered = useMemo(() => { + let items: { version: string; isCurrent: boolean }[] = []; + + for (const version of selected ?? []) { + const versionItem = fetcher.data?.versions.find((v) => v.version === version); + if (!versionItem) { + items.push({ + version, + isCurrent: false, + }); + } + } + + if (fetcher.data === undefined) { + return matchSorter(items, searchValue); + } + + items.push(...fetcher.data.versions); + + if (searchValue === "") { + return items; + } + + return matchSorter(Array.from(new Set(items)), searchValue, { + keys: ["version"], + }); + }, [searchValue, fetcher.data]); + + return ( + + {trigger} + { + if (onClose) { + onClose(); + return false; + } + + return true; + }} + > + ( +
+ + {fetcher.state === "loading" && } +
+ )} + /> + + {filtered.length > 0 + ? filtered.map((version) => ( + + {version.version}{" "} + {version.isCurrent ? current : null} + + )) + : null} + {filtered.length === 0 && fetcher.state !== "loading" && ( + No versions found + )} + +
+
+ ); +} + +function AppliedVersionsFilter() { + const { values, del } = useSearchParams(); + + const versions = values("versions"); + + if (versions.length === 0 || versions.every((v) => v === "")) { + return null; + } + + return ( + + {(search, setSearch) => ( + }> + del(["versions", "cursor", "direction"])} + variant="secondary/small" + /> + + } + searchValue={search} + clearSearchValue={() => setSearch("")} + /> + )} + + ); +} + function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { const { value, values, replace } = useSearchParams(); const searchValue = value("rootOnly"); diff --git a/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts b/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts new file mode 100644 index 0000000000..a58f4ab69e --- /dev/null +++ b/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts @@ -0,0 +1,69 @@ +import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; +import { BasePresenter } from "./basePresenter.server"; +import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic"; + +const DEFAULT_ITEMS_PER_PAGE = 25; +const MAX_ITEMS_PER_PAGE = 100; + +export class VersionListPresenter extends BasePresenter { + private readonly perPage: number; + + constructor(perPage: number = DEFAULT_ITEMS_PER_PAGE) { + super(); + this.perPage = Math.min(perPage, MAX_ITEMS_PER_PAGE); + } + + public async call({ + environment, + query, + }: { + environment: AuthenticatedEnvironment; + query?: string; + }) { + const hasFilters = query !== undefined && query.length > 0; + + const versions = await this._replica.backgroundWorker.findMany({ + select: { + version: true, + }, + where: { + runtimeEnvironmentId: environment.id, + }, + orderBy: { + createdAt: "desc", + }, + take: this.perPage, + }); + + let currentVersion: string | undefined; + + if (environment.type !== "DEVELOPMENT") { + const currentWorker = await this._replica.workerDeploymentPromotion.findFirst({ + select: { + deployment: { + select: { + version: true, + }, + }, + }, + where: { + environmentId: environment.id, + label: CURRENT_DEPLOYMENT_LABEL, + }, + }); + + if (currentWorker) { + currentVersion = currentWorker.deployment.version; + } + } + + return { + success: true as const, + versions: versions.map((version) => ({ + version: version.version, + isCurrent: version.version === currentVersion, + })), + hasFilters, + }; + } +} diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts new file mode 100644 index 0000000000..bf56ecc8b1 --- /dev/null +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts @@ -0,0 +1,55 @@ +import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; +import { z } from "zod"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; +import { VersionListPresenter } from "~/presenters/v3/VersionListPresenter.server"; +import { requireUserId } from "~/services/session.server"; +import { EnvironmentParamSchema } from "~/utils/pathBuilder"; + +const SearchParamsSchema = z.object({ + query: z.string().optional(), + per_page: z.coerce.number().min(1).default(25), +}); + +export async function loader({ request, params }: LoaderFunctionArgs) { + const userId = await requireUserId(request); + const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); + + const url = new URL(request.url); + const { per_page, query } = SearchParamsSchema.parse(Object.fromEntries(url.searchParams)); + + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + if (!project) { + throw new Response(undefined, { + status: 404, + statusText: "Project not found", + }); + } + + const environment = await findEnvironmentBySlug(project.id, envParam, userId); + if (!environment) { + throw new Response(undefined, { + status: 404, + statusText: "Environment not found", + }); + } + + const presenter = new VersionListPresenter(per_page); + + const result = await presenter.call({ + environment: environment, + query, + }); + + if (!result.success) { + return { + versions: [], + hasFilters: Boolean(query?.trim()), + }; + } + + return { + versions: result.versions, + hasFilters: result.hasFilters, + }; +} diff --git a/apps/webapp/app/services/runsRepository.server.ts b/apps/webapp/app/services/runsRepository.server.ts index b1e8205254..3196c436b3 100644 --- a/apps/webapp/app/services/runsRepository.server.ts +++ b/apps/webapp/app/services/runsRepository.server.ts @@ -1,13 +1,13 @@ import { type ClickHouse, type ClickhouseQueryBuilder } from "@internal/clickhouse"; import { type Tracer } from "@internal/tracing"; import { type Logger, type LogLevel } from "@trigger.dev/core/logger"; -import { Prisma, TaskRunStatus } from "@trigger.dev/database"; +import { MachinePresetName } from "@trigger.dev/core/v3"; +import { BulkActionId, RunId } from "@trigger.dev/core/v3/isomorphic"; +import { TaskRunStatus } from "@trigger.dev/database"; import parseDuration from "parse-duration"; +import { z } from "zod"; import { timeFilters } from "~/components/runs/v3/SharedFilters"; import { type PrismaClient } from "~/db.server"; -import { z } from "zod"; -import { BulkActionId, RunId } from "@trigger.dev/core/v3/isomorphic"; -import { MachinePresetName } from "@trigger.dev/core/v3"; export type RunsRepositoryOptions = { clickhouse: ClickHouse; From 810943a65a599b8e7059cc48550158e03bd527bd Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Fri, 18 Jul 2025 13:59:39 +0100 Subject: [PATCH 06/11] Filter by version in the db --- apps/webapp/app/presenters/v3/VersionListPresenter.server.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts b/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts index a58f4ab69e..f8a4a36538 100644 --- a/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/VersionListPresenter.server.ts @@ -28,6 +28,11 @@ export class VersionListPresenter extends BasePresenter { }, where: { runtimeEnvironmentId: environment.id, + version: query + ? { + contains: query, + } + : undefined, }, orderBy: { createdAt: "desc", From 33af5f02201464ef36061d559c7a4fe3030f9730 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 13:59:13 +0100 Subject: [PATCH 07/11] Removed duplicate classes --- apps/webapp/app/utils/cn.ts | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/apps/webapp/app/utils/cn.ts b/apps/webapp/app/utils/cn.ts index ef578364ce..842542049d 100644 --- a/apps/webapp/app/utils/cn.ts +++ b/apps/webapp/app/utils/cn.ts @@ -64,41 +64,6 @@ const customTwMerge = extendTailwindMerge({ "96", "auto", "px", - "0.5", - "1", - "1.5", - "2", - "2.5", - "3", - "3.5", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "14", - "16", - "20", - "24", - "28", - "32", - "36", - "40", - "44", - "48", - "52", - "56", - "60", - "64", - "72", - "80", - "96", - "auto", - "px", "full", "min", "max", From 7e242bf944d4ba492fba50f7e24dc61a37d1e536 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 14:01:12 +0100 Subject: [PATCH 08/11] Version filtering hasFilters consistency --- ...izationSlug.projects.$projectParam.env.$envParam.versions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts index bf56ecc8b1..f17f2a95c8 100644 --- a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts +++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions.ts @@ -44,7 +44,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (!result.success) { return { versions: [], - hasFilters: Boolean(query?.trim()), + hasFilters: query !== undefined && query.length > 0, }; } From a8bed200d8f4085d4ddedd4b87c40145823b8712 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 14:07:40 +0100 Subject: [PATCH 09/11] Added queues and machines to the bulk action summary --- .../components/BulkActionFilterSummary.tsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/apps/webapp/app/components/BulkActionFilterSummary.tsx b/apps/webapp/app/components/BulkActionFilterSummary.tsx index 9a815c08a4..b00d77d438 100644 --- a/apps/webapp/app/components/BulkActionFilterSummary.tsx +++ b/apps/webapp/app/components/BulkActionFilterSummary.tsx @@ -201,6 +201,32 @@ export function BulkActionFilterSummary({ /> ); } + case "queues": { + const values = Array.isArray(value) ? value : [`${value}`]; + return ( + v.replace("task/", "")))} + removable={false} + /> + ); + } + case "machines": { + const values = Array.isArray(value) ? value : [`${value}`]; + return ( + + ); + } default: { assertNever(typedKey); } From fc28c743b93afad65827a34af5a794f655b7fe05 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 14:57:17 +0100 Subject: [PATCH 10/11] runs.list filtering for queue and machine --- .changeset/two-eagles-report.md | 5 +++ .../v3/ApiRunListPresenter.server.ts | 36 ++++++++++++++++++- packages/core/src/v3/apiClient/index.ts | 25 +++++++++++++ packages/core/src/v3/apiClient/types.ts | 28 ++++++++++++++- packages/core/src/v3/schemas/queues.ts | 23 ++++++------ references/hello-world/src/trigger/sdk.ts | 14 ++++++++ 6 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 .changeset/two-eagles-report.md diff --git a/.changeset/two-eagles-report.md b/.changeset/two-eagles-report.md new file mode 100644 index 0000000000..11f034ed3f --- /dev/null +++ b/.changeset/two-eagles-report.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sdk": patch +--- + +Added runs.list filtering for queue and machine diff --git a/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts index b541f75a47..38ed8cccca 100644 --- a/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts @@ -1,4 +1,10 @@ -import { parsePacket, RunStatus } from "@trigger.dev/core/v3"; +import { + type ListRunResponse, + type ListRunResponseItem, + MachinePresetName, + parsePacket, + RunStatus, +} from "@trigger.dev/core/v3"; import { type Project, type RuntimeEnvironment, type TaskRunStatus } from "@trigger.dev/database"; import assertNever from "assert-never"; import { z } from "zod"; @@ -104,6 +110,34 @@ export const ApiRunListSearchParams = z.object({ "filter[createdAt][to]": CoercedDate, "filter[createdAt][period]": z.string().optional(), "filter[batch]": z.string().optional(), + "filter[queue]": z + .string() + .optional() + .transform((value) => { + return value ? value.split(",") : undefined; + }), + "filter[machine]": z + .string() + .optional() + .transform((value, ctx) => { + const values = value ? value.split(",") : undefined; + if (!values) { + return undefined; + } + + const parsedValues = values.map((v) => MachinePresetName.safeParse(v)); + const invalidValues = parsedValues.filter((result) => !result.success); + if (invalidValues.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid machine values: ${invalidValues.join(", ")}`, + }); + + return z.NEVER; + } + + return parsedValues.map((result) => result.data).filter(Boolean); + }), }); type ApiRunListSearchParams = z.infer; diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index e230374974..4eab7d0089 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -21,6 +21,7 @@ import { ListRunResponseItem, ListScheduleOptions, QueueItem, + QueueTypeName, ReplayRunResponse, RescheduleRunRequestBody, RetrieveBatchV2Response, @@ -1147,11 +1148,35 @@ function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchPar if (query.batch) { searchParams.append("filter[batch]", query.batch); } + + if (query.queue) { + searchParams.append( + "filter[queue]", + Array.isArray(query.queue) + ? query.queue.map((q) => queueNameFromQueueTypeName(q)).join(",") + : queueNameFromQueueTypeName(query.queue) + ); + } + + if (query.machine) { + searchParams.append( + "filter[machine]", + Array.isArray(query.machine) ? query.machine.join(",") : query.machine + ); + } } return searchParams; } +function queueNameFromQueueTypeName(queue: QueueTypeName): string { + if (queue.type === "task") { + return `task/${queue.name}`; + } + + return queue.name; +} + function createSearchQueryForListWaitpointTokens( query?: ListWaitpointTokensQueryParams ): URLSearchParams { diff --git a/packages/core/src/v3/apiClient/types.ts b/packages/core/src/v3/apiClient/types.ts index 5715d881cc..79baaf74ef 100644 --- a/packages/core/src/v3/apiClient/types.ts +++ b/packages/core/src/v3/apiClient/types.ts @@ -1,4 +1,9 @@ -import { RunStatus, WaitpointTokenStatus } from "../schemas/index.js"; +import { + MachinePresetName, + QueueTypeName, + RunStatus, + WaitpointTokenStatus, +} from "../schemas/index.js"; import { CursorPageParams } from "./pagination.js"; export interface ImportEnvironmentVariablesParams { @@ -32,6 +37,27 @@ export interface ListRunsQueryParams extends CursorPageParams { schedule?: string; isTest?: boolean; batch?: string; + /** + * The queue type and name, or multiple of them. + * + * @example + * ```ts + * const runs = await runs.list({ + * queue: { type: "task", name: "my-task-id" }, + * }); + * + * // Or multiple queues + * const runs = await runs.list({ + * queue: [ + * { type: "custom", name: "my-custom-queue" }, + * { type: "task", name: "my-task-id" }, + * ], + * }); + * ``` + * */ + queue?: Array | QueueTypeName; + /** The machine name, or multiple of them. */ + machine?: Array | MachinePresetName; } export interface ListProjectRunsQueryParams extends CursorPageParams, ListRunsQueryParams { diff --git a/packages/core/src/v3/schemas/queues.ts b/packages/core/src/v3/schemas/queues.ts index 2b511eb44c..cf42f0ed9a 100644 --- a/packages/core/src/v3/schemas/queues.ts +++ b/packages/core/src/v3/schemas/queues.ts @@ -45,6 +45,17 @@ export const ListQueueOptions = z.object({ export type ListQueueOptions = z.infer; +export const QueueTypeName = z.object({ + /** "task" or "custom" */ + type: QueueType, + /** The name of your queue. + * For "task" type it will be the task id, for "custom" it will be the name you specified. + * */ + name: z.string(), +}); + +export type QueueTypeName = z.infer; + /** * When retrieving a queue you can either use the queue id, * or the type and name. @@ -63,16 +74,6 @@ export type ListQueueOptions = z.infer; * const q3 = await queues.retrieve({ type: "custom", name: "my-custom-queue" }); * ``` */ -export const RetrieveQueueParam = z.union([ - z.string(), - z.object({ - /** "task" or "custom" */ - type: QueueType, - /** The name of your queue. - * For "task" type it will be the task id, for "custom" it will be the name you specified. - * */ - name: z.string(), - }), -]); +export const RetrieveQueueParam = z.union([z.string(), QueueTypeName]); export type RetrieveQueueParam = z.infer; diff --git a/references/hello-world/src/trigger/sdk.ts b/references/hello-world/src/trigger/sdk.ts index 9f714e4729..b124aa1452 100644 --- a/references/hello-world/src/trigger/sdk.ts +++ b/references/hello-world/src/trigger/sdk.ts @@ -20,6 +20,20 @@ export const sdkMethods = task({ logger.info("failed run", { run }); } + for await (const run of runs.list({ + queue: { type: "task", name: "sdk-methods" }, + limit: 5, + })) { + logger.info("sdk-methods run", { run }); + } + + for await (const run of runs.list({ + machine: ["small-1x", "small-2x"], + limit: 5, + })) { + logger.info("small machine run", { run }); + } + return runs; }, }); From b1ef11e58c9394f7e8d335dc85c69b2744fa13a4 Mon Sep 17 00:00:00 2001 From: Matt Aitken Date: Mon, 21 Jul 2025 16:10:28 +0100 Subject: [PATCH 11/11] Fix for machine errors --- .../webapp/app/presenters/v3/ApiRunListPresenter.server.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts b/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts index 38ed8cccca..a2e44969bf 100644 --- a/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/ApiRunListPresenter.server.ts @@ -126,7 +126,12 @@ export const ApiRunListSearchParams = z.object({ } const parsedValues = values.map((v) => MachinePresetName.safeParse(v)); - const invalidValues = parsedValues.filter((result) => !result.success); + const invalidValues: string[] = []; + parsedValues.forEach((result, index) => { + if (!result.success) { + invalidValues.push(values[index]); + } + }); if (invalidValues.length > 0) { ctx.addIssue({ code: z.ZodIssueCode.custom,