@@ -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" ;
1011import { Form , useFetcher } from "@remix-run/react" ;
1112import { IconToggleLeft } from "@tabler/icons-react" ;
1213import type { BulkActionType , TaskRunStatus , TaskTriggerSource } from "@trigger.dev/database" ;
13- import { ListChecks , ListFilterIcon } from "lucide-react" ;
14+ import { ListFilterIcon } from "lucide-react" ;
1415import { matchSorter } from "match-sorter" ;
1516import { type ReactNode , useCallback , useEffect , useMemo , useState } from "react" ;
1617import { z } from "zod" ;
18+ import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon" ;
1719import { StatusIcon } from "~/assets/icons/StatusIcon" ;
1820import { TaskIcon } from "~/assets/icons/TaskIcon" ;
1921import { 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" ;
4346import { useOptimisticLocation } from "~/hooks/useOptimisticLocation" ;
47+ import { useOrganization } from "~/hooks/useOrganizations" ;
4448import { useProject } from "~/hooks/useProject" ;
4549import { useSearchParams } from "~/hooks/useSearchParam" ;
50+ import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues" ;
4651import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags" ;
4752import { Button } from "../../primitives/Buttons" ;
4853import { BulkActionTypeCombo } from "./BulkAction" ;
@@ -55,8 +60,6 @@ import {
5560 TaskRunStatusCombo ,
5661} from "./TaskRunStatus" ;
5762import { TaskTriggerSourceIcon } from "./TaskTriggerSource" ;
58- import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon" ;
59- import { cn } from "~/utils/cn" ;
6063
6164export 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
111115export 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+
810996function RootOnlyToggle ( { defaultValue } : { defaultValue : boolean } ) {
811997 const { value, values, replace } = useSearchParams ( ) ;
812998 const searchValue = value ( "rootOnly" ) ;
0 commit comments