@@ -10,14 +10,21 @@ import {
10
10
} from "@heroicons/react/20/solid" ;
11
11
import { Form , useFetcher } from "@remix-run/react" ;
12
12
import { IconToggleLeft } from "@tabler/icons-react" ;
13
+ import { MachinePresetName } from "@trigger.dev/core/v3" ;
13
14
import type { BulkActionType , TaskRunStatus , TaskTriggerSource } from "@trigger.dev/database" ;
14
15
import { ListFilterIcon } from "lucide-react" ;
15
16
import { matchSorter } from "match-sorter" ;
16
17
import { type ReactNode , useCallback , useEffect , useMemo , useState } from "react" ;
17
18
import { z } from "zod" ;
18
19
import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon" ;
20
+ import { MachineDefaultIcon } from "~/assets/icons/MachineIcon" ;
19
21
import { StatusIcon } from "~/assets/icons/StatusIcon" ;
20
22
import { TaskIcon } from "~/assets/icons/TaskIcon" ;
23
+ import {
24
+ formatMachinePresetName ,
25
+ MachineLabelCombo ,
26
+ machines ,
27
+ } from "~/components/MachineLabelCombo" ;
21
28
import { AppliedFilter } from "~/components/primitives/AppliedFilter" ;
22
29
import { DateTime } from "~/components/primitives/DateTime" ;
23
30
import { FormError } from "~/components/primitives/FormError" ;
@@ -42,6 +49,7 @@ import {
42
49
TooltipProvider ,
43
50
TooltipTrigger ,
44
51
} from "~/components/primitives/Tooltip" ;
52
+ import { useDebounceEffect } from "~/hooks/useDebounce" ;
45
53
import { useEnvironment } from "~/hooks/useEnvironment" ;
46
54
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation" ;
47
55
import { useOrganization } from "~/hooks/useOrganizations" ;
@@ -60,7 +68,6 @@ import {
60
68
TaskRunStatusCombo ,
61
69
} from "./TaskRunStatus" ;
62
70
import { TaskTriggerSourceIcon } from "./TaskTriggerSource" ;
63
- import { useDebounceEffect } from "~/hooks/useDebounce" ;
64
71
65
72
export const RunStatus = z . enum ( allTaskRunStatuses ) ;
66
73
@@ -80,6 +87,27 @@ const StringOrStringArray = z.preprocess((value) => {
80
87
return undefined ;
81
88
} , z . string ( ) . array ( ) . optional ( ) ) ;
82
89
90
+ export const MachinePresetOrMachinePresetArray = z . preprocess ( ( value ) => {
91
+ if ( typeof value === "string" ) {
92
+ if ( value . length > 0 ) {
93
+ const parsed = MachinePresetName . safeParse ( value ) ;
94
+ return parsed . success ? [ parsed . data ] : undefined ;
95
+ }
96
+
97
+ return undefined ;
98
+ }
99
+
100
+ if ( Array . isArray ( value ) ) {
101
+ return value
102
+ . filter ( ( v ) => typeof v === "string" && v . length > 0 )
103
+ . map ( ( v ) => MachinePresetName . safeParse ( v ) )
104
+ . filter ( ( result ) => result . success )
105
+ . map ( ( result ) => result . data ) ;
106
+ }
107
+
108
+ return undefined ;
109
+ } , MachinePresetName . array ( ) . optional ( ) ) ;
110
+
83
111
export const TaskRunListSearchFilters = z . object ( {
84
112
cursor : z . string ( ) . optional ( ) ,
85
113
direction : z . enum ( [ "forward" , "backward" ] ) . optional ( ) ,
@@ -111,6 +139,7 @@ export const TaskRunListSearchFilters = z.object({
111
139
runId : StringOrStringArray ,
112
140
scheduleId : z . string ( ) . optional ( ) ,
113
141
queues : StringOrStringArray ,
142
+ machines : MachinePresetOrMachinePresetArray ,
114
143
} ) ;
115
144
116
145
export type TaskRunListSearchFilters = z . infer < typeof TaskRunListSearchFilters > ;
@@ -146,6 +175,8 @@ export function filterTitle(filterKey: string) {
146
175
return "Schedule ID" ;
147
176
case "queues" :
148
177
return "Queues" ;
178
+ case "machines" :
179
+ return "Machine" ;
149
180
default :
150
181
return filterKey ;
151
182
}
@@ -157,7 +188,7 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
157
188
case "direction" :
158
189
return undefined ;
159
190
case "statuses" :
160
- return < StatusIcon className = "size-4" /> ;
191
+ return < StatusIcon className = "size-4 border-text-bright " /> ;
161
192
case "tasks" :
162
193
return < TaskIcon className = "size-4" /> ;
163
194
case "tags" :
@@ -180,6 +211,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
180
211
return < ClockIcon className = "size-4" /> ;
181
212
case "queues" :
182
213
return < RectangleStackIcon className = "size-4" /> ;
214
+ case "machines" :
215
+ return < MachineDefaultIcon className = "size-4" /> ;
183
216
default :
184
217
return undefined ;
185
218
}
@@ -218,6 +251,10 @@ export function getRunFiltersFromSearchParams(
218
251
searchParams . getAll ( "queues" ) . filter ( ( v ) => v . length > 0 ) . length > 0
219
252
? searchParams . getAll ( "queues" )
220
253
: undefined ,
254
+ machines :
255
+ searchParams . getAll ( "machines" ) . filter ( ( v ) => v . length > 0 ) . length > 0
256
+ ? searchParams . getAll ( "machines" )
257
+ : undefined ,
221
258
} ;
222
259
223
260
const parsed = TaskRunListSearchFilters . safeParse ( params ) ;
@@ -252,7 +289,8 @@ export function RunsFilters(props: RunFiltersProps) {
252
289
searchParams . has ( "batchId" ) ||
253
290
searchParams . has ( "runId" ) ||
254
291
searchParams . has ( "scheduleId" ) ||
255
- searchParams . has ( "queues" ) ;
292
+ searchParams . has ( "queues" ) ||
293
+ searchParams . has ( "machines" ) ;
256
294
257
295
return (
258
296
< div className = "flex flex-row flex-wrap items-center gap-1" >
@@ -276,11 +314,12 @@ const filterTypes = [
276
314
{
277
315
name : "statuses" ,
278
316
title : "Status" ,
279
- icon : < StatusIcon className = "size-4" /> ,
317
+ icon : < StatusIcon className = "size-4 border-text-bright " /> ,
280
318
} ,
281
319
{ name : "tasks" , title : "Tasks" , icon : < TaskIcon className = "size-4" /> } ,
282
320
{ name : "tags" , title : "Tags" , icon : < TagIcon className = "size-4" /> } ,
283
321
{ name : "queues" , title : "Queues" , icon : < RectangleStackIcon className = "size-4" /> } ,
322
+ { name : "machines" , title : "Machines" , icon : < MachineDefaultIcon className = "size-4" /> } ,
284
323
{ name : "run" , title : "Run ID" , icon : < FingerPrintIcon className = "size-4" /> } ,
285
324
{ name : "batch" , title : "Batch ID" , icon : < Squares2X2Icon className = "size-4" /> } ,
286
325
{ name : "schedule" , title : "Schedule ID" , icon : < ClockIcon className = "size-4" /> } ,
@@ -332,6 +371,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
332
371
< AppliedTaskFilter possibleTasks = { possibleTasks } />
333
372
< AppliedTagsFilter />
334
373
< AppliedQueuesFilter />
374
+ < AppliedMachinesFilter />
335
375
< AppliedRunIdFilter />
336
376
< AppliedBatchIdFilter />
337
377
< AppliedScheduleIdFilter />
@@ -362,6 +402,8 @@ function Menu(props: MenuProps) {
362
402
return < TagsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
363
403
case "queues" :
364
404
return < QueuesDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
405
+ case "machines" :
406
+ return < MachinesDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
365
407
case "run" :
366
408
return < RunIdDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
367
409
case "batch" :
@@ -874,10 +916,6 @@ function QueuesDropdown({
874
916
875
917
const filtered = useMemo ( ( ) => {
876
918
let items : { name : string ; type : "custom" | "task" ; value : string } [ ] = [ ] ;
877
- if ( searchValue === "" ) {
878
- // items = selected ?? [];
879
- items = [ ] ;
880
- }
881
919
882
920
for ( const queueName of selected ?? [ ] ) {
883
921
const queueItem = fetcher . data ?. queues . find ( ( q ) => q . name === queueName ) ;
@@ -997,6 +1035,101 @@ function AppliedQueuesFilter() {
997
1035
) ;
998
1036
}
999
1037
1038
+ function MachinesDropdown ( {
1039
+ trigger,
1040
+ clearSearchValue,
1041
+ searchValue,
1042
+ onClose,
1043
+ } : {
1044
+ trigger : ReactNode ;
1045
+ clearSearchValue : ( ) => void ;
1046
+ searchValue : string ;
1047
+ onClose ?: ( ) => void ;
1048
+ } ) {
1049
+ const { values, replace } = useSearchParams ( ) ;
1050
+
1051
+ const handleChange = ( values : string [ ] ) => {
1052
+ clearSearchValue ( ) ;
1053
+ replace ( { machines : values , cursor : undefined , direction : undefined } ) ;
1054
+ } ;
1055
+
1056
+ const filtered = useMemo ( ( ) => {
1057
+ if ( searchValue === "" ) {
1058
+ return machines ;
1059
+ }
1060
+ return matchSorter ( machines , searchValue ) ;
1061
+ } , [ searchValue ] ) ;
1062
+
1063
+ return (
1064
+ < SelectProvider value = { values ( "machines" ) } setValue = { handleChange } virtualFocus = { true } >
1065
+ { trigger }
1066
+ < SelectPopover
1067
+ className = "min-w-0 max-w-[min(240px,var(--popover-available-width))]"
1068
+ hideOnEscape = { ( ) => {
1069
+ if ( onClose ) {
1070
+ onClose ( ) ;
1071
+ return false ;
1072
+ }
1073
+
1074
+ return true ;
1075
+ } }
1076
+ >
1077
+ < ComboBox placeholder = { "Filter by machine..." } value = { searchValue } />
1078
+ < SelectList >
1079
+ { filtered . map ( ( item , index ) => (
1080
+ < SelectItem
1081
+ key = { item }
1082
+ value = { item }
1083
+ shortcut = { shortcutFromIndex ( index , { shortcutsEnabled : true } ) }
1084
+ >
1085
+ < MachineLabelCombo preset = { item } />
1086
+ </ SelectItem >
1087
+ ) ) }
1088
+ </ SelectList >
1089
+ </ SelectPopover >
1090
+ </ SelectProvider >
1091
+ ) ;
1092
+ }
1093
+
1094
+ function AppliedMachinesFilter ( ) {
1095
+ const { values, del } = useSearchParams ( ) ;
1096
+ const machines = values ( "machines" ) ;
1097
+
1098
+ if ( machines . length === 0 || machines . every ( ( v ) => v === "" ) ) {
1099
+ return null ;
1100
+ }
1101
+
1102
+ return (
1103
+ < FilterMenuProvider >
1104
+ { ( search , setSearch ) => (
1105
+ < MachinesDropdown
1106
+ trigger = {
1107
+ < Ariakit . Select render = { < div className = "group cursor-pointer focus-custom" /> } >
1108
+ < AppliedFilter
1109
+ label = "Machines"
1110
+ icon = { filterIcon ( "machines" ) }
1111
+ value = { appliedSummary (
1112
+ machines . map ( ( v ) => {
1113
+ const parsed = MachinePresetName . safeParse ( v ) ;
1114
+ if ( ! parsed . success ) {
1115
+ return v ;
1116
+ }
1117
+ return formatMachinePresetName ( parsed . data ) ;
1118
+ } )
1119
+ ) }
1120
+ onRemove = { ( ) => del ( [ "machines" , "cursor" , "direction" ] ) }
1121
+ variant = "secondary/small"
1122
+ />
1123
+ </ Ariakit . Select >
1124
+ }
1125
+ searchValue = { search }
1126
+ clearSearchValue = { ( ) => setSearch ( "" ) }
1127
+ />
1128
+ ) }
1129
+ </ FilterMenuProvider >
1130
+ ) ;
1131
+ }
1132
+
1000
1133
function RootOnlyToggle ( { defaultValue } : { defaultValue : boolean } ) {
1001
1134
const { value, values, replace } = useSearchParams ( ) ;
1002
1135
const searchValue = value ( "rootOnly" ) ;
0 commit comments