9
9
XMarkIcon ,
10
10
} from "@heroicons/react/20/solid" ;
11
11
import { Form , useFetcher } from "@remix-run/react" ;
12
- import { IconToggleLeft } from "@tabler/icons-react" ;
12
+ import { IconToggleLeft , IconRotateClockwise2 } from "@tabler/icons-react" ;
13
13
import { MachinePresetName } from "@trigger.dev/core/v3" ;
14
14
import type { BulkActionType , TaskRunStatus , TaskTriggerSource } from "@trigger.dev/database" ;
15
15
import { ListFilterIcon } from "lucide-react" ;
@@ -57,6 +57,7 @@ import { useProject } from "~/hooks/useProject";
57
57
import { useSearchParams } from "~/hooks/useSearchParam" ;
58
58
import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues" ;
59
59
import { type loader as tagsLoader } from "~/routes/resources.projects.$projectParam.runs.tags" ;
60
+ import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions" ;
60
61
import { Button } from "../../primitives/Buttons" ;
61
62
import { BulkActionTypeCombo } from "./BulkAction" ;
62
63
import { appliedSummary , FilterMenuProvider , TimeFilter } from "./SharedFilters" ;
@@ -68,6 +69,7 @@ import {
68
69
TaskRunStatusCombo ,
69
70
} from "./TaskRunStatus" ;
70
71
import { TaskTriggerSourceIcon } from "./TaskTriggerSource" ;
72
+ import { Badge } from "~/components/primitives/Badge" ;
71
73
72
74
export const RunStatus = z . enum ( allTaskRunStatuses ) ;
73
75
@@ -177,6 +179,8 @@ export function filterTitle(filterKey: string) {
177
179
return "Queues" ;
178
180
case "machines" :
179
181
return "Machine" ;
182
+ case "versions" :
183
+ return "Version" ;
180
184
default :
181
185
return filterKey ;
182
186
}
@@ -213,6 +217,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
213
217
return < RectangleStackIcon className = "size-4" /> ;
214
218
case "machines" :
215
219
return < MachineDefaultIcon className = "size-4" /> ;
220
+ case "versions" :
221
+ return < IconRotateClockwise2 className = "size-4" /> ;
216
222
default :
217
223
return undefined ;
218
224
}
@@ -255,6 +261,10 @@ export function getRunFiltersFromSearchParams(
255
261
searchParams . getAll ( "machines" ) . filter ( ( v ) => v . length > 0 ) . length > 0
256
262
? searchParams . getAll ( "machines" )
257
263
: undefined ,
264
+ versions :
265
+ searchParams . getAll ( "versions" ) . filter ( ( v ) => v . length > 0 ) . length > 0
266
+ ? searchParams . getAll ( "versions" )
267
+ : undefined ,
258
268
} ;
259
269
260
270
const parsed = TaskRunListSearchFilters . safeParse ( params ) ;
@@ -290,7 +300,8 @@ export function RunsFilters(props: RunFiltersProps) {
290
300
searchParams . has ( "runId" ) ||
291
301
searchParams . has ( "scheduleId" ) ||
292
302
searchParams . has ( "queues" ) ||
293
- searchParams . has ( "machines" ) ;
303
+ searchParams . has ( "machines" ) ||
304
+ searchParams . has ( "versions" ) ;
294
305
295
306
return (
296
307
< div className = "flex flex-row flex-wrap items-center gap-1" >
@@ -318,6 +329,7 @@ const filterTypes = [
318
329
} ,
319
330
{ name : "tasks" , title : "Tasks" , icon : < TaskIcon className = "size-4" /> } ,
320
331
{ name : "tags" , title : "Tags" , icon : < TagIcon className = "size-4" /> } ,
332
+ { name : "versions" , title : "Versions" , icon : < IconRotateClockwise2 className = "size-4" /> } ,
321
333
{ name : "queues" , title : "Queues" , icon : < RectangleStackIcon className = "size-4" /> } ,
322
334
{ name : "machines" , title : "Machines" , icon : < MachineDefaultIcon className = "size-4" /> } ,
323
335
{ name : "run" , title : "Run ID" , icon : < FingerPrintIcon className = "size-4" /> } ,
@@ -370,6 +382,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
370
382
< AppliedStatusFilter />
371
383
< AppliedTaskFilter possibleTasks = { possibleTasks } />
372
384
< AppliedTagsFilter />
385
+ < AppliedVersionsFilter />
373
386
< AppliedQueuesFilter />
374
387
< AppliedMachinesFilter />
375
388
< AppliedRunIdFilter />
@@ -410,6 +423,8 @@ function Menu(props: MenuProps) {
410
423
return < BatchIdDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
411
424
case "schedule" :
412
425
return < ScheduleIdDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
426
+ case "versions" :
427
+ return < VersionsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
413
428
}
414
429
}
415
430
@@ -1130,6 +1145,153 @@ function AppliedMachinesFilter() {
1130
1145
) ;
1131
1146
}
1132
1147
1148
+ function VersionsDropdown ( {
1149
+ trigger,
1150
+ clearSearchValue,
1151
+ searchValue,
1152
+ onClose,
1153
+ } : {
1154
+ trigger : ReactNode ;
1155
+ clearSearchValue : ( ) => void ;
1156
+ searchValue : string ;
1157
+ onClose ?: ( ) => void ;
1158
+ } ) {
1159
+ const organization = useOrganization ( ) ;
1160
+ const project = useProject ( ) ;
1161
+ const environment = useEnvironment ( ) ;
1162
+ const { values, replace } = useSearchParams ( ) ;
1163
+
1164
+ const handleChange = ( values : string [ ] ) => {
1165
+ clearSearchValue ( ) ;
1166
+ replace ( {
1167
+ versions : values . length > 0 ? values : undefined ,
1168
+ cursor : undefined ,
1169
+ direction : undefined ,
1170
+ } ) ;
1171
+ } ;
1172
+
1173
+ const versionValues = values ( "versions" ) . filter ( ( v ) => v !== "" ) ;
1174
+ const selected = versionValues . length > 0 ? versionValues : undefined ;
1175
+
1176
+ const fetcher = useFetcher < typeof versionsLoader > ( ) ;
1177
+
1178
+ useDebounceEffect (
1179
+ searchValue ,
1180
+ ( s ) => {
1181
+ const searchParams = new URLSearchParams ( ) ;
1182
+ if ( searchValue ) {
1183
+ searchParams . set ( "query" , encodeURIComponent ( s ) ) ;
1184
+ }
1185
+ fetcher . load (
1186
+ `/resources/orgs/${ organization . slug } /projects/${ project . slug } /env/${
1187
+ environment . slug
1188
+ } /versions?${ searchParams . toString ( ) } `
1189
+ ) ;
1190
+ } ,
1191
+ 250
1192
+ ) ;
1193
+
1194
+ const filtered = useMemo ( ( ) => {
1195
+ let items : { version : string ; isCurrent : boolean } [ ] = [ ] ;
1196
+
1197
+ for ( const version of selected ?? [ ] ) {
1198
+ const versionItem = fetcher . data ?. versions . find ( ( v ) => v . version === version ) ;
1199
+ if ( ! versionItem ) {
1200
+ items . push ( {
1201
+ version,
1202
+ isCurrent : false ,
1203
+ } ) ;
1204
+ }
1205
+ }
1206
+
1207
+ if ( fetcher . data === undefined ) {
1208
+ return matchSorter ( items , searchValue ) ;
1209
+ }
1210
+
1211
+ items . push ( ...fetcher . data . versions ) ;
1212
+
1213
+ if ( searchValue === "" ) {
1214
+ return items ;
1215
+ }
1216
+
1217
+ return matchSorter ( Array . from ( new Set ( items ) ) , searchValue , {
1218
+ keys : [ "version" ] ,
1219
+ } ) ;
1220
+ } , [ searchValue , fetcher . data ] ) ;
1221
+
1222
+ return (
1223
+ < SelectProvider value = { selected ?? [ ] } setValue = { handleChange } virtualFocus = { true } >
1224
+ { trigger }
1225
+ < SelectPopover
1226
+ className = "min-w-0 max-w-[min(240px,var(--popover-available-width))]"
1227
+ hideOnEscape = { ( ) => {
1228
+ if ( onClose ) {
1229
+ onClose ( ) ;
1230
+ return false ;
1231
+ }
1232
+
1233
+ return true ;
1234
+ } }
1235
+ >
1236
+ < ComboBox
1237
+ value = { searchValue }
1238
+ render = { ( props ) => (
1239
+ < div className = "flex items-center justify-stretch" >
1240
+ < input { ...props } placeholder = { "Filter by versions..." } />
1241
+ { fetcher . state === "loading" && < Spinner color = "muted" /> }
1242
+ </ div >
1243
+ ) }
1244
+ />
1245
+ < SelectList >
1246
+ { filtered . length > 0
1247
+ ? filtered . map ( ( version ) => (
1248
+ < SelectItem key = { version . version } value = { version . version } >
1249
+ { version . version } { " " }
1250
+ { version . isCurrent ? < Badge variant = "extra-small" > current</ Badge > : null }
1251
+ </ SelectItem >
1252
+ ) )
1253
+ : null }
1254
+ { filtered . length === 0 && fetcher . state !== "loading" && (
1255
+ < SelectItem disabled > No versions found</ SelectItem >
1256
+ ) }
1257
+ </ SelectList >
1258
+ </ SelectPopover >
1259
+ </ SelectProvider >
1260
+ ) ;
1261
+ }
1262
+
1263
+ function AppliedVersionsFilter ( ) {
1264
+ const { values, del } = useSearchParams ( ) ;
1265
+
1266
+ const versions = values ( "versions" ) ;
1267
+
1268
+ if ( versions . length === 0 || versions . every ( ( v ) => v === "" ) ) {
1269
+ return null ;
1270
+ }
1271
+
1272
+ return (
1273
+ < FilterMenuProvider >
1274
+ { ( search , setSearch ) => (
1275
+ < VersionsDropdown
1276
+ trigger = {
1277
+ < Ariakit . Select render = { < div className = "group cursor-pointer focus-custom" /> } >
1278
+ < AppliedFilter
1279
+ label = "Versions"
1280
+ icon = { filterIcon ( "versions" ) }
1281
+ value = { appliedSummary ( values ( "versions" ) ) }
1282
+ onRemove = { ( ) => del ( [ "versions" , "cursor" , "direction" ] ) }
1283
+ variant = "secondary/small"
1284
+ />
1285
+ </ Ariakit . Select >
1286
+ }
1287
+ searchValue = { search }
1288
+ clearSearchValue = { ( ) => setSearch ( "" ) }
1289
+ />
1290
+ ) }
1291
+ </ FilterMenuProvider >
1292
+ ) ;
1293
+ }
1294
+
1133
1295
function RootOnlyToggle ( { defaultValue } : { defaultValue : boolean } ) {
1134
1296
const { value, values, replace } = useSearchParams ( ) ;
1135
1297
const searchValue = value ( "rootOnly" ) ;
0 commit comments