@@ -3,17 +3,19 @@ import {
3
3
CalendarIcon ,
4
4
ClockIcon ,
5
5
FingerPrintIcon ,
6
+ RectangleStackIcon ,
6
7
Squares2X2Icon ,
7
8
TagIcon ,
8
9
XMarkIcon ,
9
10
} from "@heroicons/react/20/solid" ;
10
11
import { Form , useFetcher } from "@remix-run/react" ;
11
12
import { IconToggleLeft } from "@tabler/icons-react" ;
12
13
import type { BulkActionType , TaskRunStatus , TaskTriggerSource } from "@trigger.dev/database" ;
13
- import { ListChecks , ListFilterIcon } from "lucide-react" ;
14
+ import { ListFilterIcon } from "lucide-react" ;
14
15
import { matchSorter } from "match-sorter" ;
15
16
import { type ReactNode , useCallback , useEffect , useMemo , useState } from "react" ;
16
17
import { z } from "zod" ;
18
+ import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon" ;
17
19
import { StatusIcon } from "~/assets/icons/StatusIcon" ;
18
20
import { TaskIcon } from "~/assets/icons/TaskIcon" ;
19
21
import { AppliedFilter } from "~/components/primitives/AppliedFilter" ;
@@ -40,9 +42,12 @@ import {
40
42
TooltipProvider ,
41
43
TooltipTrigger ,
42
44
} from "~/components/primitives/Tooltip" ;
45
+ import { useEnvironment } from "~/hooks/useEnvironment" ;
43
46
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation" ;
47
+ import { useOrganization } from "~/hooks/useOrganizations" ;
44
48
import { useProject } from "~/hooks/useProject" ;
45
49
import { useSearchParams } from "~/hooks/useSearchParam" ;
50
+ import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues" ;
46
51
import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags" ;
47
52
import { Button } from "../../primitives/Buttons" ;
48
53
import { BulkActionTypeCombo } from "./BulkAction" ;
@@ -55,8 +60,6 @@ import {
55
60
TaskRunStatusCombo ,
56
61
} from "./TaskRunStatus" ;
57
62
import { TaskTriggerSourceIcon } from "./TaskTriggerSource" ;
58
- import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon" ;
59
- import { cn } from "~/utils/cn" ;
60
63
61
64
export const RunStatus = z . enum ( allTaskRunStatuses ) ;
62
65
@@ -106,6 +109,7 @@ export const TaskRunListSearchFilters = z.object({
106
109
batchId : z . string ( ) . optional ( ) ,
107
110
runId : StringOrStringArray ,
108
111
scheduleId : z . string ( ) . optional ( ) ,
112
+ queues : StringOrStringArray ,
109
113
} ) ;
110
114
111
115
export type TaskRunListSearchFilters = z . infer < typeof TaskRunListSearchFilters > ;
@@ -139,6 +143,8 @@ export function filterTitle(filterKey: string) {
139
143
return "Run ID" ;
140
144
case "scheduleId" :
141
145
return "Schedule ID" ;
146
+ case "queues" :
147
+ return "Queues" ;
142
148
default :
143
149
return filterKey ;
144
150
}
@@ -171,6 +177,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
171
177
return < FingerPrintIcon className = "size-4" /> ;
172
178
case "scheduleId" :
173
179
return < ClockIcon className = "size-4" /> ;
180
+ case "queues" :
181
+ return < RectangleStackIcon className = "size-4" /> ;
174
182
default :
175
183
return undefined ;
176
184
}
@@ -205,6 +213,10 @@ export function getRunFiltersFromSearchParams(
205
213
: undefined ,
206
214
batchId : searchParams . get ( "batchId" ) ?? undefined ,
207
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 ,
208
220
} ;
209
221
210
222
const parsed = TaskRunListSearchFilters . safeParse ( params ) ;
@@ -238,7 +250,8 @@ export function RunsFilters(props: RunFiltersProps) {
238
250
searchParams . has ( "tags" ) ||
239
251
searchParams . has ( "batchId" ) ||
240
252
searchParams . has ( "runId" ) ||
241
- searchParams . has ( "scheduleId" ) ;
253
+ searchParams . has ( "scheduleId" ) ||
254
+ searchParams . has ( "queues" ) ;
242
255
243
256
return (
244
257
< div className = "flex flex-row flex-wrap items-center gap-1" >
@@ -266,6 +279,7 @@ const filterTypes = [
266
279
} ,
267
280
{ name : "tasks" , title : "Tasks" , icon : < TaskIcon className = "size-4" /> } ,
268
281
{ name : "tags" , title : "Tags" , icon : < TagIcon className = "size-4" /> } ,
282
+ { name : "queues" , title : "Queues" , icon : < RectangleStackIcon className = "size-4" /> } ,
269
283
{ name : "run" , title : "Run ID" , icon : < FingerPrintIcon className = "size-4" /> } ,
270
284
{ name : "batch" , title : "Batch ID" , icon : < Squares2X2Icon className = "size-4" /> } ,
271
285
{ name : "schedule" , title : "Schedule ID" , icon : < ClockIcon className = "size-4" /> } ,
@@ -316,6 +330,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
316
330
< AppliedStatusFilter />
317
331
< AppliedTaskFilter possibleTasks = { possibleTasks } />
318
332
< AppliedTagsFilter />
333
+ < AppliedQueuesFilter />
319
334
< AppliedRunIdFilter />
320
335
< AppliedBatchIdFilter />
321
336
< AppliedScheduleIdFilter />
@@ -344,6 +359,8 @@ function Menu(props: MenuProps) {
344
359
return < BulkActionsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
345
360
case "tags" :
346
361
return < TagsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
362
+ case "queues" :
363
+ return < QueuesDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
347
364
case "run" :
348
365
return < RunIdDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
349
366
case "batch" :
@@ -807,6 +824,175 @@ function AppliedTagsFilter() {
807
824
) ;
808
825
}
809
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
+
810
996
function RootOnlyToggle ( { defaultValue } : { defaultValue : boolean } ) {
811
997
const { value, values, replace } = useSearchParams ( ) ;
812
998
const searchValue = value ( "rootOnly" ) ;
0 commit comments