@@ -10,14 +10,21 @@ import {
1010} from "@heroicons/react/20/solid" ;
1111import { Form , useFetcher } from "@remix-run/react" ;
1212import { IconToggleLeft } from "@tabler/icons-react" ;
13+ import { MachinePresetName } from "@trigger.dev/core/v3" ;
1314import type { BulkActionType , TaskRunStatus , TaskTriggerSource } from "@trigger.dev/database" ;
1415import { ListFilterIcon } from "lucide-react" ;
1516import { matchSorter } from "match-sorter" ;
1617import { type ReactNode , useCallback , useEffect , useMemo , useState } from "react" ;
1718import { z } from "zod" ;
1819import { ListCheckedIcon } from "~/assets/icons/ListCheckedIcon" ;
20+ import { MachineDefaultIcon } from "~/assets/icons/MachineIcon" ;
1921import { StatusIcon } from "~/assets/icons/StatusIcon" ;
2022import { TaskIcon } from "~/assets/icons/TaskIcon" ;
23+ import {
24+ formatMachinePresetName ,
25+ MachineLabelCombo ,
26+ machines ,
27+ } from "~/components/MachineLabelCombo" ;
2128import { AppliedFilter } from "~/components/primitives/AppliedFilter" ;
2229import { DateTime } from "~/components/primitives/DateTime" ;
2330import { FormError } from "~/components/primitives/FormError" ;
@@ -42,6 +49,7 @@ import {
4249 TooltipProvider ,
4350 TooltipTrigger ,
4451} from "~/components/primitives/Tooltip" ;
52+ import { useDebounceEffect } from "~/hooks/useDebounce" ;
4553import { useEnvironment } from "~/hooks/useEnvironment" ;
4654import { useOptimisticLocation } from "~/hooks/useOptimisticLocation" ;
4755import { useOrganization } from "~/hooks/useOrganizations" ;
@@ -60,7 +68,6 @@ import {
6068 TaskRunStatusCombo ,
6169} from "./TaskRunStatus" ;
6270import { TaskTriggerSourceIcon } from "./TaskTriggerSource" ;
63- import { useDebounceEffect } from "~/hooks/useDebounce" ;
6471
6572export const RunStatus = z . enum ( allTaskRunStatuses ) ;
6673
@@ -80,6 +87,27 @@ const StringOrStringArray = z.preprocess((value) => {
8087 return undefined ;
8188} , z . string ( ) . array ( ) . optional ( ) ) ;
8289
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+
83111export const TaskRunListSearchFilters = z . object ( {
84112 cursor : z . string ( ) . optional ( ) ,
85113 direction : z . enum ( [ "forward" , "backward" ] ) . optional ( ) ,
@@ -111,6 +139,7 @@ export const TaskRunListSearchFilters = z.object({
111139 runId : StringOrStringArray ,
112140 scheduleId : z . string ( ) . optional ( ) ,
113141 queues : StringOrStringArray ,
142+ machines : MachinePresetOrMachinePresetArray ,
114143} ) ;
115144
116145export type TaskRunListSearchFilters = z . infer < typeof TaskRunListSearchFilters > ;
@@ -146,6 +175,8 @@ export function filterTitle(filterKey: string) {
146175 return "Schedule ID" ;
147176 case "queues" :
148177 return "Queues" ;
178+ case "machines" :
179+ return "Machine" ;
149180 default :
150181 return filterKey ;
151182 }
@@ -157,7 +188,7 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
157188 case "direction" :
158189 return undefined ;
159190 case "statuses" :
160- return < StatusIcon className = "size-4" /> ;
191+ return < StatusIcon className = "size-4 border-text-bright " /> ;
161192 case "tasks" :
162193 return < TaskIcon className = "size-4" /> ;
163194 case "tags" :
@@ -180,6 +211,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
180211 return < ClockIcon className = "size-4" /> ;
181212 case "queues" :
182213 return < RectangleStackIcon className = "size-4" /> ;
214+ case "machines" :
215+ return < MachineDefaultIcon className = "size-4" /> ;
183216 default :
184217 return undefined ;
185218 }
@@ -218,6 +251,10 @@ export function getRunFiltersFromSearchParams(
218251 searchParams . getAll ( "queues" ) . filter ( ( v ) => v . length > 0 ) . length > 0
219252 ? searchParams . getAll ( "queues" )
220253 : undefined ,
254+ machines :
255+ searchParams . getAll ( "machines" ) . filter ( ( v ) => v . length > 0 ) . length > 0
256+ ? searchParams . getAll ( "machines" )
257+ : undefined ,
221258 } ;
222259
223260 const parsed = TaskRunListSearchFilters . safeParse ( params ) ;
@@ -252,7 +289,8 @@ export function RunsFilters(props: RunFiltersProps) {
252289 searchParams . has ( "batchId" ) ||
253290 searchParams . has ( "runId" ) ||
254291 searchParams . has ( "scheduleId" ) ||
255- searchParams . has ( "queues" ) ;
292+ searchParams . has ( "queues" ) ||
293+ searchParams . has ( "machines" ) ;
256294
257295 return (
258296 < div className = "flex flex-row flex-wrap items-center gap-1" >
@@ -276,11 +314,12 @@ const filterTypes = [
276314 {
277315 name : "statuses" ,
278316 title : "Status" ,
279- icon : < StatusIcon className = "size-4" /> ,
317+ icon : < StatusIcon className = "size-4 border-text-bright " /> ,
280318 } ,
281319 { name : "tasks" , title : "Tasks" , icon : < TaskIcon className = "size-4" /> } ,
282320 { name : "tags" , title : "Tags" , icon : < TagIcon className = "size-4" /> } ,
283321 { name : "queues" , title : "Queues" , icon : < RectangleStackIcon className = "size-4" /> } ,
322+ { name : "machines" , title : "Machines" , icon : < MachineDefaultIcon className = "size-4" /> } ,
284323 { name : "run" , title : "Run ID" , icon : < FingerPrintIcon className = "size-4" /> } ,
285324 { name : "batch" , title : "Batch ID" , icon : < Squares2X2Icon className = "size-4" /> } ,
286325 { name : "schedule" , title : "Schedule ID" , icon : < ClockIcon className = "size-4" /> } ,
@@ -332,6 +371,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
332371 < AppliedTaskFilter possibleTasks = { possibleTasks } />
333372 < AppliedTagsFilter />
334373 < AppliedQueuesFilter />
374+ < AppliedMachinesFilter />
335375 < AppliedRunIdFilter />
336376 < AppliedBatchIdFilter />
337377 < AppliedScheduleIdFilter />
@@ -362,6 +402,8 @@ function Menu(props: MenuProps) {
362402 return < TagsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
363403 case "queues" :
364404 return < QueuesDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
405+ case "machines" :
406+ return < MachinesDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
365407 case "run" :
366408 return < RunIdDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
367409 case "batch" :
@@ -874,10 +916,6 @@ function QueuesDropdown({
874916
875917 const filtered = useMemo ( ( ) => {
876918 let items : { name : string ; type : "custom" | "task" ; value : string } [ ] = [ ] ;
877- if ( searchValue === "" ) {
878- // items = selected ?? [];
879- items = [ ] ;
880- }
881919
882920 for ( const queueName of selected ?? [ ] ) {
883921 const queueItem = fetcher . data ?. queues . find ( ( q ) => q . name === queueName ) ;
@@ -997,6 +1035,101 @@ function AppliedQueuesFilter() {
9971035 ) ;
9981036}
9991037
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+
10001133function RootOnlyToggle ( { defaultValue } : { defaultValue : boolean } ) {
10011134 const { value, values, replace } = useSearchParams ( ) ;
10021135 const searchValue = value ( "rootOnly" ) ;
0 commit comments