|
3 | 3 | CalendarIcon, |
4 | 4 | ClockIcon, |
5 | 5 | FingerPrintIcon, |
| 6 | + RectangleStackIcon, |
6 | 7 | Squares2X2Icon, |
7 | 8 | TagIcon, |
8 | 9 | XMarkIcon, |
@@ -41,9 +42,12 @@ import { |
41 | 42 | TooltipProvider, |
42 | 43 | TooltipTrigger, |
43 | 44 | } from "~/components/primitives/Tooltip"; |
| 45 | +import { useEnvironment } from "~/hooks/useEnvironment"; |
44 | 46 | import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; |
| 47 | +import { useOrganization } from "~/hooks/useOrganizations"; |
45 | 48 | import { useProject } from "~/hooks/useProject"; |
46 | 49 | import { useSearchParams } from "~/hooks/useSearchParam"; |
| 50 | +import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; |
47 | 51 | import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags"; |
48 | 52 | import { Button } from "../../primitives/Buttons"; |
49 | 53 | import { BulkActionTypeCombo } from "./BulkAction"; |
@@ -105,6 +109,7 @@ export const TaskRunListSearchFilters = z.object({ |
105 | 109 | batchId: z.string().optional(), |
106 | 110 | runId: StringOrStringArray, |
107 | 111 | scheduleId: z.string().optional(), |
| 112 | + queues: StringOrStringArray, |
108 | 113 | }); |
109 | 114 |
|
110 | 115 | export type TaskRunListSearchFilters = z.infer<typeof TaskRunListSearchFilters>; |
@@ -138,6 +143,8 @@ export function filterTitle(filterKey: string) { |
138 | 143 | return "Run ID"; |
139 | 144 | case "scheduleId": |
140 | 145 | return "Schedule ID"; |
| 146 | + case "queues": |
| 147 | + return "Queues"; |
141 | 148 | default: |
142 | 149 | return filterKey; |
143 | 150 | } |
@@ -170,6 +177,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined { |
170 | 177 | return <FingerPrintIcon className="size-4" />; |
171 | 178 | case "scheduleId": |
172 | 179 | return <ClockIcon className="size-4" />; |
| 180 | + case "queues": |
| 181 | + return <RectangleStackIcon className="size-4" />; |
173 | 182 | default: |
174 | 183 | return undefined; |
175 | 184 | } |
@@ -204,6 +213,10 @@ export function getRunFiltersFromSearchParams( |
204 | 213 | : undefined, |
205 | 214 | batchId: searchParams.get("batchId") ?? undefined, |
206 | 215 | scheduleId: searchParams.get("scheduleId") ?? undefined, |
| 216 | + queues: |
| 217 | + searchParams.getAll("queues").filter((v) => v.length > 0).length > 0 |
| 218 | + ? searchParams.getAll("queues") |
| 219 | + : undefined, |
207 | 220 | }; |
208 | 221 |
|
209 | 222 | const parsed = TaskRunListSearchFilters.safeParse(params); |
@@ -237,7 +250,8 @@ export function RunsFilters(props: RunFiltersProps) { |
237 | 250 | searchParams.has("tags") || |
238 | 251 | searchParams.has("batchId") || |
239 | 252 | searchParams.has("runId") || |
240 | | - searchParams.has("scheduleId"); |
| 253 | + searchParams.has("scheduleId") || |
| 254 | + searchParams.has("queues"); |
241 | 255 |
|
242 | 256 | return ( |
243 | 257 | <div className="flex flex-row flex-wrap items-center gap-1"> |
@@ -265,6 +279,7 @@ const filterTypes = [ |
265 | 279 | }, |
266 | 280 | { name: "tasks", title: "Tasks", icon: <TaskIcon className="size-4" /> }, |
267 | 281 | { name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> }, |
| 282 | + { name: "queues", title: "Queues", icon: <RectangleStackIcon className="size-4" /> }, |
268 | 283 | { name: "run", title: "Run ID", icon: <FingerPrintIcon className="size-4" /> }, |
269 | 284 | { name: "batch", title: "Batch ID", icon: <Squares2X2Icon className="size-4" /> }, |
270 | 285 | { name: "schedule", title: "Schedule ID", icon: <ClockIcon className="size-4" /> }, |
@@ -315,6 +330,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) { |
315 | 330 | <AppliedStatusFilter /> |
316 | 331 | <AppliedTaskFilter possibleTasks={possibleTasks} /> |
317 | 332 | <AppliedTagsFilter /> |
| 333 | + <AppliedQueuesFilter /> |
318 | 334 | <AppliedRunIdFilter /> |
319 | 335 | <AppliedBatchIdFilter /> |
320 | 336 | <AppliedScheduleIdFilter /> |
@@ -343,6 +359,8 @@ function Menu(props: MenuProps) { |
343 | 359 | return <BulkActionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
344 | 360 | case "tags": |
345 | 361 | return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
| 362 | + case "queues": |
| 363 | + return <QueuesDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
346 | 364 | case "run": |
347 | 365 | return <RunIdDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
348 | 366 | case "batch": |
@@ -806,6 +824,175 @@ function AppliedTagsFilter() { |
806 | 824 | ); |
807 | 825 | } |
808 | 826 |
|
| 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 | + |
809 | 996 | function RootOnlyToggle({ defaultValue }: { defaultValue: boolean }) { |
810 | 997 | const { value, values, replace } = useSearchParams(); |
811 | 998 | const searchValue = value("rootOnly"); |
|
0 commit comments