Skip to content

Commit 21c25a9

Browse files
committed
Queue in run table and filtering
1 parent 0796df2 commit 21c25a9

File tree

5 files changed

+234
-7
lines changed

5 files changed

+234
-7
lines changed

apps/webapp/app/components/runs/v3/RunFilters.tsx

Lines changed: 190 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@ import {
33
CalendarIcon,
44
ClockIcon,
55
FingerPrintIcon,
6+
RectangleStackIcon,
67
Squares2X2Icon,
78
TagIcon,
89
XMarkIcon,
910
} from "@heroicons/react/20/solid";
1011
import { Form, useFetcher } from "@remix-run/react";
1112
import { IconToggleLeft } from "@tabler/icons-react";
1213
import type { BulkActionType, TaskRunStatus, TaskTriggerSource } from "@trigger.dev/database";
13-
import { ListChecks, ListFilterIcon } from "lucide-react";
14+
import { ListFilterIcon } from "lucide-react";
1415
import { matchSorter } from "match-sorter";
1516
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
1617
import { z } from "zod";
18+
import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon";
1719
import { StatusIcon } from "~/assets/icons/StatusIcon";
1820
import { TaskIcon } from "~/assets/icons/TaskIcon";
1921
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
@@ -40,9 +42,12 @@ import {
4042
TooltipProvider,
4143
TooltipTrigger,
4244
} from "~/components/primitives/Tooltip";
45+
import { useEnvironment } from "~/hooks/useEnvironment";
4346
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
47+
import { useOrganization } from "~/hooks/useOrganizations";
4448
import { useProject } from "~/hooks/useProject";
4549
import { useSearchParams } from "~/hooks/useSearchParam";
50+
import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues";
4651
import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags";
4752
import { Button } from "../../primitives/Buttons";
4853
import { BulkActionTypeCombo } from "./BulkAction";
@@ -55,8 +60,6 @@ import {
5560
TaskRunStatusCombo,
5661
} from "./TaskRunStatus";
5762
import { TaskTriggerSourceIcon } from "./TaskTriggerSource";
58-
import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon";
59-
import { cn } from "~/utils/cn";
6063

6164
export const RunStatus = z.enum(allTaskRunStatuses);
6265

@@ -106,6 +109,7 @@ export const TaskRunListSearchFilters = z.object({
106109
batchId: z.string().optional(),
107110
runId: StringOrStringArray,
108111
scheduleId: z.string().optional(),
112+
queues: StringOrStringArray,
109113
});
110114

111115
export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>;
@@ -139,6 +143,8 @@ export function filterTitle(filterKey: string) {
139143
return "Run ID";
140144
case "scheduleId":
141145
return "Schedule ID";
146+
case "queues":
147+
return "Queues";
142148
default:
143149
return filterKey;
144150
}
@@ -171,6 +177,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
171177
return <FingerPrintIcon className="size-4" />;
172178
case "scheduleId":
173179
return <ClockIcon className="size-4" />;
180+
case "queues":
181+
return <RectangleStackIcon className="size-4" />;
174182
default:
175183
return undefined;
176184
}
@@ -205,6 +213,10 @@ export function getRunFiltersFromSearchParams(
205213
: undefined,
206214
batchId: searchParams.get("batchId") ?? undefined,
207215
scheduleId: searchParams.get("scheduleId") ?? undefined,
216+
queues:
217+
searchParams.getAll("queues").filter((v) => v.length > 0).length > 0
218+
? searchParams.getAll("queues")
219+
: undefined,
208220
};
209221

210222
const parsed = TaskRunListSearchFilters.safeParse(params);
@@ -238,7 +250,8 @@ export function RunsFilters(props: RunFiltersProps) {
238250
searchParams.has("tags") ||
239251
searchParams.has("batchId") ||
240252
searchParams.has("runId") ||
241-
searchParams.has("scheduleId");
253+
searchParams.has("scheduleId") ||
254+
searchParams.has("queues");
242255

243256
return (
244257
<div className="flex flex-row flex-wrap items-center gap-1">
@@ -266,6 +279,7 @@ const filterTypes = [
266279
},
267280
{ name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> },
268281
{ name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> },
282+
{ name: "queues", title: "Queues", icon: <RectangleStackIcon className="size-4" /> },
269283
{ name: "run", title: "Run ID", icon: <FingerPrintIcon className="size-4" /> },
270284
{ name: "batch", title: "Batch ID", icon: <Squares2X2Icon className="size-4" /> },
271285
{ name: "schedule", title: "Schedule ID", icon: <ClockIcon className="size-4" /> },
@@ -316,6 +330,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
316330
<AppliedStatusFilter />
317331
<AppliedTaskFilter possibleTasks={possibleTasks} />
318332
<AppliedTagsFilter />
333+
<AppliedQueuesFilter />
319334
<AppliedRunIdFilter />
320335
<AppliedBatchIdFilter />
321336
<AppliedScheduleIdFilter />
@@ -344,6 +359,8 @@ function Menu(props: MenuProps) {
344359
return <BulkActionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
345360
case "tags":
346361
return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
362+
case "queues":
363+
return <QueuesDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
347364
case "run":
348365
return <RunIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
349366
case "batch":
@@ -807,6 +824,175 @@ function AppliedTagsFilter() {
807824
);
808825
}
809826

827+
function QueuesDropdown({
828+
trigger,
829+
clearSearchValue,
830+
searchValue,
831+
onClose,
832+
}: {
833+
trigger: ReactNode;
834+
clearSearchValue: () => void;
835+
searchValue: string;
836+
onClose?: () => void;
837+
}) {
838+
const organization = useOrganization();
839+
const project = useProject();
840+
const environment = useEnvironment();
841+
const { values, replace } = useSearchParams();
842+
843+
const handleChange = (values: string[]) => {
844+
clearSearchValue();
845+
replace({
846+
queues: values.length > 0 ? values : undefined,
847+
cursor: undefined,
848+
direction: undefined,
849+
});
850+
};
851+
852+
const queueValues = values("queues").filter((v) => v !== "");
853+
const selected = queueValues.length > 0 ? queueValues : undefined;
854+
855+
const fetcher = useFetcher<typeof queuesLoader>();
856+
857+
useEffect(() => {
858+
const searchParams = new URLSearchParams();
859+
searchParams.set("per_page", "25");
860+
if (searchValue) {
861+
searchParams.set("query", encodeURIComponent(searchValue));
862+
}
863+
fetcher.load(
864+
`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${
865+
environment.slug
866+
}/queues?${searchParams.toString()}`
867+
);
868+
}, [searchValue]);
869+
870+
const filtered = useMemo(() => {
871+
console.log(fetcher.data);
872+
let items: { name: string; type: "custom" | "task"; value: string }[] = [];
873+
if (searchValue === "") {
874+
// items = selected ?? [];
875+
items = [];
876+
}
877+
878+
for (const queueName of selected ?? []) {
879+
const queueItem = fetcher.data?.queues.find((q) => q.name === queueName);
880+
if (!queueItem) {
881+
if (queueName.startsWith("task/")) {
882+
items.push({
883+
name: queueName.replace("task/", ""),
884+
type: "task",
885+
value: queueName,
886+
});
887+
} else {
888+
items.push({
889+
name: queueName,
890+
type: "custom",
891+
value: queueName,
892+
});
893+
}
894+
}
895+
}
896+
897+
if (fetcher.data === undefined) {
898+
return matchSorter(items, searchValue);
899+
}
900+
901+
items.push(
902+
...fetcher.data.queues.map((q) => ({
903+
name: q.name,
904+
type: q.type,
905+
value: q.type === "task" ? `task/${q.name}` : q.name,
906+
}))
907+
);
908+
909+
return matchSorter(Array.from(new Set(items)), searchValue, {
910+
keys: ["name"],
911+
});
912+
}, [searchValue, fetcher.data]);
913+
914+
return (
915+
<SelectProvider value={selected ?? []} setValue={handleChange} virtualFocus={true}>
916+
{trigger}
917+
<SelectPopover
918+
className="min-w-0 max-w-[min(240px,var(--popover-available-width))]"
919+
hideOnEscape={() => {
920+
if (onClose) {
921+
onClose();
922+
return false;
923+
}
924+
925+
return true;
926+
}}
927+
>
928+
<ComboBox
929+
value={searchValue}
930+
render={(props) => (
931+
<div className="flex items-center justify-stretch">
932+
<input {...props} placeholder={"Filter by queues..."} />
933+
{fetcher.state === "loading" && <Spinner color="muted" />}
934+
</div>
935+
)}
936+
/>
937+
<SelectList>
938+
{filtered.length > 0
939+
? filtered.map((queue) => (
940+
<SelectItem
941+
key={queue.value}
942+
value={queue.value}
943+
icon={
944+
queue.type === "task" ? (
945+
<TaskIcon className="size-4 shrink-0 text-blue-500" />
946+
) : (
947+
<RectangleStackIcon className="size-4 shrink-0 text-purple-500" />
948+
)
949+
}
950+
>
951+
{queue.name}
952+
</SelectItem>
953+
))
954+
: null}
955+
{filtered.length === 0 && fetcher.state !== "loading" && (
956+
<SelectItem disabled>No queues found</SelectItem>
957+
)}
958+
</SelectList>
959+
</SelectPopover>
960+
</SelectProvider>
961+
);
962+
}
963+
964+
function AppliedQueuesFilter() {
965+
const { values, del } = useSearchParams();
966+
967+
const queues = values("queues");
968+
969+
if (queues.length === 0 || queues.every((v) => v === "")) {
970+
return null;
971+
}
972+
973+
return (
974+
<FilterMenuProvider>
975+
{(search, setSearch) => (
976+
<QueuesDropdown
977+
trigger={
978+
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
979+
<AppliedFilter
980+
label="Queues"
981+
icon={filterIcon("queues")}
982+
value={appliedSummary(values("queues").map((v) => v.replace("task/", "")))}
983+
onRemove={() => del(["queues", "cursor", "direction"])}
984+
variant="secondary/small"
985+
/>
986+
</Ariakit.Select>
987+
}
988+
searchValue={search}
989+
clearSearchValue={() => setSearch("")}
990+
/>
991+
)}
992+
</FilterMenuProvider>
993+
);
994+
}
995+
810996
function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) {
811997
const { value, values, replace } = useSearchParams();
812998
const searchValue = value("rootOnly");

apps/webapp/app/components/runs/v3/TaskRunsTable.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
import { MachineIcon } from "~/assets/icons/MachineIcon";
6060
import { MachineLabelCombo } from "~/components/MachineLabelCombo";
6161
import { MachineTooltipInfo } from "~/components/MachineTooltipInfo";
62+
import { TaskIconSmall } from "~/assets/icons/TaskIcon";
6263

6364
type RunsTableProps = {
6465
total: number;
@@ -82,9 +83,8 @@ export function TaskRunsTable({
8283
}: RunsTableProps) {
8384
const organization = useOrganization();
8485
const project = useProject();
85-
const environment = useEnvironment();
8686
const checkboxes = useRef<(HTMLInputElement | null)[]>([]);
87-
const { selectedItems, has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection);
87+
const { has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection);
8888
const { isManagedCloud } = useFeatures();
8989

9090
const showCompute = isManagedCloud;
@@ -213,6 +213,7 @@ export function TaskRunsTable({
213213
</TableHeaderCell>
214214
<TableHeaderCell>Test</TableHeaderCell>
215215
<TableHeaderCell>Created at</TableHeaderCell>
216+
<TableHeaderCell>Queue</TableHeaderCell>
216217
<TableHeaderCell
217218
tooltip={
218219
<div className="max-w-xs p-1">
@@ -394,6 +395,22 @@ export function TaskRunsTable({
394395
<TableCell to={path}>
395396
{run.createdAt ? <DateTime date={run.createdAt} /> : "–"}
396397
</TableCell>
398+
<TableCell to={path}>
399+
<span className="flex items-center gap-1">
400+
{run.queue.type === "task" ? (
401+
<SimpleTooltip
402+
button={<TaskIconSmall className="size-[1.125rem] text-blue-500" />}
403+
content={`This queue was automatically created from your "${run.queue.name}" task`}
404+
/>
405+
) : (
406+
<SimpleTooltip
407+
button={<RectangleStackIcon className="size-[1.125rem] text-purple-500" />}
408+
content={`This is a custom queue you added in your code.`}
409+
/>
410+
)}
411+
<span>{run.queue.name}</span>
412+
</span>
413+
</TableCell>
397414
<TableCell to={path}>
398415
{run.delayUntil ? <DateTime date={run.delayUntil} /> : "–"}
399416
</TableCell>

apps/webapp/app/presenters/RunFilters.server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import {
33
TaskRunListSearchFilters,
44
} from "~/components/runs/v3/RunFilters";
55
import { getRootOnlyFilterPreference } from "~/services/preferences/uiPreferences.server";
6+
import { type ParsedRunFilters } from "~/services/runsRepository.server";
67

7-
export async function getRunFiltersFromRequest(request: Request) {
8+
type FiltersFromRequest = ParsedRunFilters & Required<Pick<ParsedRunFilters, "rootOnly">>;
9+
10+
export async function getRunFiltersFromRequest(request: Request): Promise<FiltersFromRequest> {
811
const url = new URL(request.url);
912
let rootOnlyValue = false;
1013
if (url.searchParams.has("rootOnly")) {
@@ -29,6 +32,7 @@ export async function getRunFiltersFromRequest(request: Request) {
2932
runId,
3033
batchId,
3134
scheduleId,
35+
queues,
3236
} = TaskRunListSearchFilters.parse(s);
3337

3438
return {
@@ -46,5 +50,6 @@ export async function getRunFiltersFromRequest(request: Request) {
4650
rootOnly: rootOnlyValue,
4751
direction: direction,
4852
cursor: cursor,
53+
queues,
4954
};
5055
}

0 commit comments

Comments
 (0)