Filter runs by queue, machine, version (#2277)
* Queue in run table and filtering * Debounce the filter changes * Remove console log * Added machine filtering * Added version filtering * Filter by version in the db * Removed duplicate classes * Version filtering hasFilters consistency * Added queues and machines to the bulk action summary * runs.list filtering for queue and machine * Fix for machine errors
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Added runs.list filtering for queue and machine
|
||||
@@ -27,7 +27,7 @@ export function MachineIcon({ preset, className }: { preset?: string; className?
|
||||
}
|
||||
}
|
||||
|
||||
function MachineDefaultIcon({ className }: { className?: string }) {
|
||||
export function MachineDefaultIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg className={className} viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
|
||||
@@ -201,6 +201,32 @@ export function BulkActionFilterSummary({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "queues": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values.map((v) => v.replace("task/", "")))}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "machines": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values)}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
assertNever(typedKey);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { type MachinePresetName } from "@trigger.dev/core/v3";
|
||||
import { MachinePresetName } from "@trigger.dev/core/v3";
|
||||
import { MachineIcon } from "~/assets/icons/MachineIcon";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
export const machines = Object.values(MachinePresetName.enum);
|
||||
|
||||
export function MachineLabelCombo({
|
||||
preset,
|
||||
className,
|
||||
|
||||
@@ -3,20 +3,28 @@ import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
FingerPrintIcon,
|
||||
RectangleStackIcon,
|
||||
Squares2X2Icon,
|
||||
TagIcon,
|
||||
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";
|
||||
import { matchSorter } from "match-sorter";
|
||||
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon";
|
||||
import { MachineDefaultIcon } from "~/assets/icons/MachineIcon";
|
||||
import { StatusIcon } from "~/assets/icons/StatusIcon";
|
||||
import { TaskIcon } from "~/assets/icons/TaskIcon";
|
||||
import {
|
||||
formatMachinePresetName,
|
||||
MachineLabelCombo,
|
||||
machines,
|
||||
} from "~/components/MachineLabelCombo";
|
||||
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { FormError } from "~/components/primitives/FormError";
|
||||
@@ -41,10 +49,15 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "~/components/primitives/Tooltip";
|
||||
import { useDebounceEffect } from "~/hooks/useDebounce";
|
||||
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 { 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";
|
||||
@@ -56,6 +69,7 @@ import {
|
||||
TaskRunStatusCombo,
|
||||
} from "./TaskRunStatus";
|
||||
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
|
||||
export const RunStatus = z.enum(allTaskRunStatuses);
|
||||
|
||||
@@ -75,6 +89,27 @@ const StringOrStringArray = z.preprocess((value) => {
|
||||
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(),
|
||||
@@ -105,6 +140,8 @@ export const TaskRunListSearchFilters = z.object({
|
||||
batchId: z.string().optional(),
|
||||
runId: StringOrStringArray,
|
||||
scheduleId: z.string().optional(),
|
||||
queues: StringOrStringArray,
|
||||
machines: MachinePresetOrMachinePresetArray,
|
||||
});
|
||||
|
||||
export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
|
||||
@@ -138,6 +175,12 @@ export function filterTitle(filterKey: string) {
|
||||
return "Run ID";
|
||||
case "scheduleId":
|
||||
return "Schedule ID";
|
||||
case "queues":
|
||||
return "Queues";
|
||||
case "machines":
|
||||
return "Machine";
|
||||
case "versions":
|
||||
return "Version";
|
||||
default:
|
||||
return filterKey;
|
||||
}
|
||||
@@ -149,7 +192,7 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
|
||||
case "direction":
|
||||
return undefined;
|
||||
case "statuses":
|
||||
return <StatusIcon className="size-4" />;
|
||||
return <StatusIcon className="size-4 border-text-bright" />;
|
||||
case "tasks":
|
||||
return <TaskIcon className="size-4" />;
|
||||
case "tags":
|
||||
@@ -170,6 +213,12 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
|
||||
return <FingerPrintIcon className="size-4" />;
|
||||
case "scheduleId":
|
||||
return <ClockIcon className="size-4" />;
|
||||
case "queues":
|
||||
return <RectangleStackIcon className="size-4" />;
|
||||
case "machines":
|
||||
return <MachineDefaultIcon className="size-4" />;
|
||||
case "versions":
|
||||
return <IconRotateClockwise2 className="size-4" />;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -204,6 +253,18 @@ 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,
|
||||
machines:
|
||||
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);
|
||||
@@ -237,7 +298,10 @@ export function RunsFilters(props: RunFiltersProps) {
|
||||
searchParams.has("tags") ||
|
||||
searchParams.has("batchId") ||
|
||||
searchParams.has("runId") ||
|
||||
searchParams.has("scheduleId");
|
||||
searchParams.has("scheduleId") ||
|
||||
searchParams.has("queues") ||
|
||||
searchParams.has("machines") ||
|
||||
searchParams.has("versions");
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap items-center gap-1">
|
||||
@@ -261,10 +325,13 @@ const filterTypes = [
|
||||
{
|
||||
name: "statuses",
|
||||
title: "Status",
|
||||
icon: <StatusIcon className="size-4" />,
|
||||
icon: <StatusIcon className="size-4 border-text-bright" />,
|
||||
},
|
||||
{ name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> },
|
||||
{ name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> },
|
||||
{ name: "versions", title: "Versions", icon: <IconRotateClockwise2 className="size-4" /> },
|
||||
{ name: "queues", title: "Queues", icon: <RectangleStackIcon className="size-4" /> },
|
||||
{ name: "machines", title: "Machines", icon: <MachineDefaultIcon className="size-4" /> },
|
||||
{ name: "run", title: "Run ID", icon: <FingerPrintIcon className="size-4" /> },
|
||||
{ name: "batch", title: "Batch ID", icon: <Squares2X2Icon className="size-4" /> },
|
||||
{ name: "schedule", title: "Schedule ID", icon: <ClockIcon className="size-4" /> },
|
||||
@@ -315,6 +382,9 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
|
||||
<AppliedStatusFilter />
|
||||
<AppliedTaskFilter possibleTasks={possibleTasks} />
|
||||
<AppliedTagsFilter />
|
||||
<AppliedVersionsFilter />
|
||||
<AppliedQueuesFilter />
|
||||
<AppliedMachinesFilter />
|
||||
<AppliedRunIdFilter />
|
||||
<AppliedBatchIdFilter />
|
||||
<AppliedScheduleIdFilter />
|
||||
@@ -343,12 +413,18 @@ function Menu(props: MenuProps) {
|
||||
return <BulkActionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "tags":
|
||||
return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "queues":
|
||||
return <QueuesDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "machines":
|
||||
return <MachinesDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "run":
|
||||
return <RunIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "batch":
|
||||
return <BatchIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "schedule":
|
||||
return <ScheduleIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "versions":
|
||||
return <VersionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -806,6 +882,416 @@ 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<typeof queuesLoader>();
|
||||
|
||||
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(() => {
|
||||
let items: { name: string; type: "custom" | "task"; value: string }[] = [];
|
||||
|
||||
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 (
|
||||
<SelectProvider value={selected ?? []} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox
|
||||
value={searchValue}
|
||||
render={(props) => (
|
||||
<div className="flex items-center justify-stretch">
|
||||
<input {...props} placeholder={"Filter by queues..."} />
|
||||
{fetcher.state === "loading" && <Spinner color="muted" />}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectList>
|
||||
{filtered.length > 0
|
||||
? filtered.map((queue) => (
|
||||
<SelectItem
|
||||
key={queue.value}
|
||||
value={queue.value}
|
||||
icon={
|
||||
queue.type === "task" ? (
|
||||
<TaskIcon className="size-4 shrink-0 text-blue-500" />
|
||||
) : (
|
||||
<RectangleStackIcon className="size-4 shrink-0 text-purple-500" />
|
||||
)
|
||||
}
|
||||
>
|
||||
{queue.name}
|
||||
</SelectItem>
|
||||
))
|
||||
: null}
|
||||
{filtered.length === 0 && fetcher.state !== "loading" && (
|
||||
<SelectItem disabled>No queues found</SelectItem>
|
||||
)}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedQueuesFilter() {
|
||||
const { values, del } = useSearchParams();
|
||||
|
||||
const queues = values("queues");
|
||||
|
||||
if (queues.length === 0 || queues.every((v) => v === "")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<QueuesDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
|
||||
<AppliedFilter
|
||||
label="Queues"
|
||||
icon={filterIcon("queues")}
|
||||
value={appliedSummary(values("queues").map((v) => v.replace("task/", "")))}
|
||||
onRemove={() => del(["queues", "cursor", "direction"])}
|
||||
variant="secondary/small"
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<SelectProvider value={values("machines")} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox placeholder={"Filter by machine..."} value={searchValue} />
|
||||
<SelectList>
|
||||
{filtered.map((item, index) => (
|
||||
<SelectItem
|
||||
key={item}
|
||||
value={item}
|
||||
shortcut={shortcutFromIndex(index, { shortcutsEnabled: true })}
|
||||
>
|
||||
<MachineLabelCombo preset={item} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedMachinesFilter() {
|
||||
const { values, del } = useSearchParams();
|
||||
const machines = values("machines");
|
||||
|
||||
if (machines.length === 0 || machines.every((v) => v === "")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<MachinesDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
|
||||
<AppliedFilter
|
||||
label="Machines"
|
||||
icon={filterIcon("machines")}
|
||||
value={appliedSummary(
|
||||
machines.map((v) => {
|
||||
const parsed = MachinePresetName.safeParse(v);
|
||||
if (!parsed.success) {
|
||||
return v;
|
||||
}
|
||||
return formatMachinePresetName(parsed.data);
|
||||
})
|
||||
)}
|
||||
onRemove={() => del(["machines", "cursor", "direction"])}
|
||||
variant="secondary/small"
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
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<typeof versionsLoader>();
|
||||
|
||||
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 (
|
||||
<SelectProvider value={selected ?? []} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox
|
||||
value={searchValue}
|
||||
render={(props) => (
|
||||
<div className="flex items-center justify-stretch">
|
||||
<input {...props} placeholder={"Filter by versions..."} />
|
||||
{fetcher.state === "loading" && <Spinner color="muted" />}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectList>
|
||||
{filtered.length > 0
|
||||
? filtered.map((version) => (
|
||||
<SelectItem key={version.version} value={version.version}>
|
||||
{version.version}{" "}
|
||||
{version.isCurrent ? <Badge variant="extra-small">current</Badge> : null}
|
||||
</SelectItem>
|
||||
))
|
||||
: null}
|
||||
{filtered.length === 0 && fetcher.state !== "loading" && (
|
||||
<SelectItem disabled>No versions found</SelectItem>
|
||||
)}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedVersionsFilter() {
|
||||
const { values, del } = useSearchParams();
|
||||
|
||||
const versions = values("versions");
|
||||
|
||||
if (versions.length === 0 || versions.every((v) => v === "")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<VersionsDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
|
||||
<AppliedFilter
|
||||
label="Versions"
|
||||
icon={filterIcon("versions")}
|
||||
value={appliedSummary(values("versions"))}
|
||||
onRemove={() => del(["versions", "cursor", "direction"])}
|
||||
variant="secondary/small"
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) {
|
||||
const { value, values, replace } = useSearchParams();
|
||||
const searchValue = value("rootOnly");
|
||||
|
||||
@@ -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,9 +55,6 @@ import {
|
||||
filterableTaskRunStatuses,
|
||||
TaskRunStatusCombo,
|
||||
} from "./TaskRunStatus";
|
||||
import { MachineIcon } from "~/assets/icons/MachineIcon";
|
||||
import { MachineLabelCombo } from "~/components/MachineLabelCombo";
|
||||
import { MachineTooltipInfo } from "~/components/MachineTooltipInfo";
|
||||
|
||||
type RunsTableProps = {
|
||||
total: number;
|
||||
@@ -82,9 +78,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;
|
||||
@@ -211,6 +206,7 @@ export function TaskRunsTable({
|
||||
<TableHeaderCell className="pl-4" tooltip={<MachineTooltipInfo />}>
|
||||
Machine
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>Queue</TableHeaderCell>
|
||||
<TableHeaderCell>Test</TableHeaderCell>
|
||||
<TableHeaderCell>Created at</TableHeaderCell>
|
||||
<TableHeaderCell
|
||||
@@ -388,6 +384,22 @@ export function TaskRunsTable({
|
||||
<TableCell to={path}>
|
||||
<MachineLabelCombo preset={run.machinePreset} />
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
<span className="flex items-center gap-1">
|
||||
{run.queue.type === "task" ? (
|
||||
<SimpleTooltip
|
||||
button={<TaskIconSmall className="size-[1.125rem] text-blue-500" />}
|
||||
content={`This queue was automatically created from your "${run.queue.name}" task`}
|
||||
/>
|
||||
) : (
|
||||
<SimpleTooltip
|
||||
button={<RectangleStackIcon className="size-[1.125rem] text-purple-500" />}
|
||||
content={`This is a custom queue you added in your code.`}
|
||||
/>
|
||||
)}
|
||||
<span>{run.queue.name}</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell to={path}>
|
||||
{run.isTest ? <CheckIcon className="size-4 text-charcoal-400" /> : "–"}
|
||||
</TableCell>
|
||||
|
||||
@@ -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<T extends (...args: any[]) => 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<T>(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
|
||||
}
|
||||
|
||||
@@ -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<Pick<ParsedRunFilters, "rootOnly">>;
|
||||
|
||||
export async function getRunFiltersFromRequest(request: Request): Promise<FiltersFromRequest> {
|
||||
const url = new URL(request.url);
|
||||
let rootOnlyValue = false;
|
||||
if (url.searchParams.has("rootOnly")) {
|
||||
@@ -29,6 +32,8 @@ export async function getRunFiltersFromRequest(request: Request) {
|
||||
runId,
|
||||
batchId,
|
||||
scheduleId,
|
||||
queues,
|
||||
machines,
|
||||
} = TaskRunListSearchFilters.parse(s);
|
||||
|
||||
return {
|
||||
@@ -46,5 +51,7 @@ export async function getRunFiltersFromRequest(request: Request) {
|
||||
rootOnly: rootOnlyValue,
|
||||
direction: direction,
|
||||
cursor: cursor,
|
||||
queues,
|
||||
machines,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,39 @@ 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: string[] = [];
|
||||
parsedValues.forEach((result, index) => {
|
||||
if (!result.success) {
|
||||
invalidValues.push(values[index]);
|
||||
}
|
||||
});
|
||||
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<typeof ApiRunListSearchParams>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { type ClickHouse } from "@internal/clickhouse";
|
||||
import { MachinePresetName } from "@trigger.dev/core/v3";
|
||||
import {
|
||||
type PrismaClient,
|
||||
type PrismaClientOrTransaction,
|
||||
@@ -30,6 +31,8 @@ export type RunListOptions = {
|
||||
rootOnly?: boolean;
|
||||
batchId?: string;
|
||||
runId?: string[];
|
||||
queues?: string[];
|
||||
machines?: MachinePresetName[];
|
||||
//pagination
|
||||
direction?: Direction;
|
||||
cursor?: string;
|
||||
@@ -65,6 +68,8 @@ export class NextRunListPresenter {
|
||||
rootOnly,
|
||||
batchId,
|
||||
runId,
|
||||
queues,
|
||||
machines,
|
||||
from,
|
||||
to,
|
||||
direction = "forward",
|
||||
@@ -90,6 +95,8 @@ export class NextRunListPresenter {
|
||||
(tags !== undefined && tags.length > 0) ||
|
||||
batchId !== undefined ||
|
||||
(runId !== undefined && runId.length > 0) ||
|
||||
(queues !== undefined && queues.length > 0) ||
|
||||
(machines !== undefined && machines.length > 0) ||
|
||||
typeof isTest === "boolean" ||
|
||||
rootOnly === true ||
|
||||
!time.isDefault;
|
||||
@@ -173,6 +180,8 @@ export class NextRunListPresenter {
|
||||
batchId,
|
||||
runId,
|
||||
bulkId,
|
||||
queues,
|
||||
machines,
|
||||
page: {
|
||||
size: pageSize,
|
||||
cursor,
|
||||
@@ -233,6 +242,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: {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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,
|
||||
version: query
|
||||
? {
|
||||
contains: query,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
+55
@@ -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: query !== undefined && query.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
versions: result.versions,
|
||||
hasFilters: result.hasFilters,
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -1,12 +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";
|
||||
|
||||
export type RunsRepositoryOptions = {
|
||||
clickhouse: ClickHouse;
|
||||
@@ -36,6 +37,8 @@ const RunListInputOptionsSchema = z.object({
|
||||
batchId: z.string().optional(),
|
||||
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<typeof RunListInputOptionsSchema>;
|
||||
@@ -44,6 +47,11 @@ export type RunListInputFilters = Omit<
|
||||
"organizationId" | "projectId" | "environmentId"
|
||||
>;
|
||||
|
||||
export type ParsedRunFilters = RunListInputFilters & {
|
||||
cursor?: string;
|
||||
direction?: "forward" | "backward";
|
||||
};
|
||||
|
||||
type FilterRunsOptions = Omit<RunListInputOptions, "period"> & {
|
||||
period: number | undefined;
|
||||
};
|
||||
@@ -170,6 +178,7 @@ export class RunsRepository {
|
||||
metadata: true,
|
||||
metadataType: true,
|
||||
machinePreset: true,
|
||||
queue: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -353,6 +362,16 @@ function applyRunFiltersToQueryBuilder<T>(
|
||||
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 });
|
||||
}
|
||||
|
||||
if (options.machines && options.machines.length > 0) {
|
||||
queryBuilder.where("machine_preset IN {machines: Array(String)}", {
|
||||
machines: options.machines,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRunListInputOptions(data: any): RunListInputOptions {
|
||||
|
||||
@@ -25,6 +25,52 @@ 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",
|
||||
"full",
|
||||
"min",
|
||||
"max",
|
||||
"fit",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> | QueueTypeName;
|
||||
/** The machine name, or multiple of them. */
|
||||
machine?: Array<MachinePresetName> | MachinePresetName;
|
||||
}
|
||||
|
||||
export interface ListProjectRunsQueryParams extends CursorPageParams, ListRunsQueryParams {
|
||||
|
||||
@@ -45,6 +45,17 @@ export const ListQueueOptions = z.object({
|
||||
|
||||
export type ListQueueOptions = z.infer<typeof ListQueueOptions>;
|
||||
|
||||
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<typeof QueueTypeName>;
|
||||
|
||||
/**
|
||||
* 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<typeof ListQueueOptions>;
|
||||
* 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<typeof RetrieveQueueParam>;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user