99 XMarkIcon ,
1010} from "@heroicons/react/20/solid" ;
1111import { Form , useFetcher } from "@remix-run/react" ;
12- import { IconToggleLeft } from "@tabler/icons-react" ;
12+ import { IconToggleLeft , IconRotateClockwise2 } from "@tabler/icons-react" ;
1313import { MachinePresetName } from "@trigger.dev/core/v3" ;
1414import type { BulkActionType , TaskRunStatus , TaskTriggerSource } from "@trigger.dev/database" ;
1515import { ListFilterIcon } from "lucide-react" ;
@@ -57,6 +57,7 @@ import { useProject } from "~/hooks/useProject";
5757import { useSearchParams } from "~/hooks/useSearchParam" ;
5858import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues" ;
5959import { 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" ;
6061import { Button } from "../../primitives/Buttons" ;
6162import { BulkActionTypeCombo } from "./BulkAction" ;
6263import { appliedSummary , FilterMenuProvider , TimeFilter } from "./SharedFilters" ;
@@ -68,6 +69,7 @@ import {
6869 TaskRunStatusCombo ,
6970} from "./TaskRunStatus" ;
7071import { TaskTriggerSourceIcon } from "./TaskTriggerSource" ;
72+ import { Badge } from "~/components/primitives/Badge" ;
7173
7274export const RunStatus = z . enum ( allTaskRunStatuses ) ;
7375
@@ -177,6 +179,8 @@ export function filterTitle(filterKey: string) {
177179 return "Queues" ;
178180 case "machines" :
179181 return "Machine" ;
182+ case "versions" :
183+ return "Version" ;
180184 default :
181185 return filterKey ;
182186 }
@@ -213,6 +217,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
213217 return < RectangleStackIcon className = "size-4" /> ;
214218 case "machines" :
215219 return < MachineDefaultIcon className = "size-4" /> ;
220+ case "versions" :
221+ return < IconRotateClockwise2 className = "size-4" /> ;
216222 default :
217223 return undefined ;
218224 }
@@ -255,6 +261,10 @@ export function getRunFiltersFromSearchParams(
255261 searchParams . getAll ( "machines" ) . filter ( ( v ) => v . length > 0 ) . length > 0
256262 ? searchParams . getAll ( "machines" )
257263 : undefined ,
264+ versions :
265+ searchParams . getAll ( "versions" ) . filter ( ( v ) => v . length > 0 ) . length > 0
266+ ? searchParams . getAll ( "versions" )
267+ : undefined ,
258268 } ;
259269
260270 const parsed = TaskRunListSearchFilters . safeParse ( params ) ;
@@ -290,7 +300,8 @@ export function RunsFilters(props: RunFiltersProps) {
290300 searchParams . has ( "runId" ) ||
291301 searchParams . has ( "scheduleId" ) ||
292302 searchParams . has ( "queues" ) ||
293- searchParams . has ( "machines" ) ;
303+ searchParams . has ( "machines" ) ||
304+ searchParams . has ( "versions" ) ;
294305
295306 return (
296307 < div className = "flex flex-row flex-wrap items-center gap-1" >
@@ -318,6 +329,7 @@ const filterTypes = [
318329 } ,
319330 { name : "tasks" , title : "Tasks" , icon : < TaskIcon className = "size-4" /> } ,
320331 { name : "tags" , title : "Tags" , icon : < TagIcon className = "size-4" /> } ,
332+ { name : "versions" , title : "Versions" , icon : < IconRotateClockwise2 className = "size-4" /> } ,
321333 { name : "queues" , title : "Queues" , icon : < RectangleStackIcon className = "size-4" /> } ,
322334 { name : "machines" , title : "Machines" , icon : < MachineDefaultIcon className = "size-4" /> } ,
323335 { name : "run" , title : "Run ID" , icon : < FingerPrintIcon className = "size-4" /> } ,
@@ -370,6 +382,7 @@ function AppliedFilters({ possibleTasks, bulkActions }: RunFiltersProps) {
370382 < AppliedStatusFilter />
371383 < AppliedTaskFilter possibleTasks = { possibleTasks } />
372384 < AppliedTagsFilter />
385+ < AppliedVersionsFilter />
373386 < AppliedQueuesFilter />
374387 < AppliedMachinesFilter />
375388 < AppliedRunIdFilter />
@@ -410,6 +423,8 @@ function Menu(props: MenuProps) {
410423 return < BatchIdDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
411424 case "schedule" :
412425 return < ScheduleIdDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
426+ case "versions" :
427+ return < VersionsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
413428 }
414429}
415430
@@ -1130,6 +1145,153 @@ function AppliedMachinesFilter() {
11301145 ) ;
11311146}
11321147
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+
11331295function RootOnlyToggle ( { defaultValue } : { defaultValue : boolean } ) {
11341296 const { value, values, replace } = useSearchParams ( ) ;
11351297 const searchValue = value ( "rootOnly" ) ;
0 commit comments