44 ClockIcon ,
55 CpuChipIcon ,
66 FingerPrintIcon ,
7+ GlobeAltIcon ,
78 PlusIcon ,
89 RectangleStackIcon ,
910 Squares2X2Icon ,
@@ -61,6 +62,8 @@ import { useShortcutKeys } from "~/hooks/useShortcutKeys";
6162import { ShortcutKey } from "~/components/primitives/ShortcutKey" ;
6263import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags" ;
6364import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues" ;
65+ import { useRegions } from "~/hooks/useRegions" ;
66+ import { RegionLabel } from "./RegionLabel" ;
6467import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions" ;
6568import { Button } from "../../primitives/Buttons" ;
6669import { AIFilterInput } from "./AIFilterInput" ;
@@ -187,6 +190,9 @@ export const TaskRunListSearchFilters = z.object({
187190 "Schedule ID to filter by - shows runs from a specific schedule. They start with sched_"
188191 ) ,
189192 queues : StringOrStringArray . describe ( "Queue names to filter by (these are user-defined names)" ) ,
193+ regions : StringOrStringArray . describe (
194+ "Region master-queue identifiers to filter by (the worker instance group masterQueue values)"
195+ ) ,
190196 machines : MachinePresetOrMachinePresetArray . describe (
191197 `Machine presets to filter by (${ machines . join ( ", " ) } )`
192198 ) ,
@@ -229,6 +235,8 @@ export function filterTitle(filterKey: string) {
229235 return "Schedule ID" ;
230236 case "queues" :
231237 return "Queues" ;
238+ case "regions" :
239+ return "Region" ;
232240 case "machines" :
233241 return "Machine" ;
234242 case "versions" :
@@ -271,6 +279,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
271279 return < ClockIcon className = "size-4" /> ;
272280 case "queues" :
273281 return < RectangleStackIcon className = "size-4" /> ;
282+ case "regions" :
283+ return < GlobeAltIcon className = "size-4" /> ;
274284 case "machines" :
275285 return < MachineDefaultIcon className = "size-4" /> ;
276286 case "versions" :
@@ -317,6 +327,10 @@ export function getRunFiltersFromSearchParams(
317327 searchParams . getAll ( "queues" ) . filter ( ( v ) => v . length > 0 ) . length > 0
318328 ? searchParams . getAll ( "queues" )
319329 : undefined ,
330+ regions :
331+ searchParams . getAll ( "regions" ) . filter ( ( v ) => v . length > 0 ) . length > 0
332+ ? searchParams . getAll ( "regions" )
333+ : undefined ,
320334 machines :
321335 searchParams . getAll ( "machines" ) . filter ( ( v ) => v . length > 0 ) . length > 0
322336 ? searchParams . getAll ( "machines" )
@@ -369,6 +383,7 @@ export function RunsFilters(props: RunFiltersProps) {
369383 searchParams . has ( "runId" ) ||
370384 searchParams . has ( "scheduleId" ) ||
371385 searchParams . has ( "queues" ) ||
386+ searchParams . has ( "regions" ) ||
372387 searchParams . has ( "machines" ) ||
373388 searchParams . has ( "versions" ) ||
374389 searchParams . has ( "errorId" ) ||
@@ -402,6 +417,7 @@ const filterTypes = [
402417 { name : "tags" , title : "Tags" , icon : < TagIcon className = "size-4" /> } ,
403418 { name : "versions" , title : "Versions" , icon : < IconRotateClockwise2 className = "size-4" /> } ,
404419 { name : "queues" , title : "Queues" , icon : < RectangleStackIcon className = "size-4" /> } ,
420+ { name : "regions" , title : "Region" , icon : < GlobeAltIcon className = "size-4" /> } ,
405421 { name : "machines" , title : "Machines" , icon : < MachineDefaultIcon className = "size-4" /> } ,
406422 { name : "run" , title : "Run ID" , icon : < FingerPrintIcon className = "size-4" /> } ,
407423 { name : "batch" , title : "Batch ID" , icon : < Squares2X2Icon className = "size-4" /> } ,
@@ -456,6 +472,7 @@ function AppliedFilters({ bulkActions }: RunFiltersProps) {
456472 < AppliedTagsFilter />
457473 < AppliedVersionsFilter />
458474 < AppliedQueuesFilter />
475+ < AppliedRegionsFilter />
459476 < AppliedMachinesFilter />
460477 < AppliedRunIdFilter />
461478 < AppliedBatchIdFilter />
@@ -485,6 +502,8 @@ function Menu(props: MenuProps) {
485502 return < TagsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
486503 case "queues" :
487504 return < QueuesDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
505+ case "regions" :
506+ return < RegionsDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
488507 case "machines" :
489508 return < MachinesDropdown onClose = { ( ) => props . setFilterType ( undefined ) } { ...props } /> ;
490509 case "run" :
@@ -503,11 +522,14 @@ function Menu(props: MenuProps) {
503522}
504523
505524function MainMenu ( { searchValue, trigger, clearSearchValue, setFilterType } : MenuProps ) {
525+ const environment = useEnvironment ( ) ;
526+ const showRegion = environment . type !== "DEVELOPMENT" ;
506527 const filtered = useMemo ( ( ) => {
507528 return filterTypes . filter ( ( item ) => {
529+ if ( item . name === "regions" && ! showRegion ) return false ;
508530 return item . title . toLowerCase ( ) . includes ( searchValue . toLowerCase ( ) ) ;
509531 } ) ;
510- } , [ searchValue ] ) ;
532+ } , [ searchValue , showRegion ] ) ;
511533
512534 return (
513535 < SelectProvider virtualFocus = { true } >
@@ -1260,6 +1282,138 @@ function AppliedQueuesFilter() {
12601282 ) ;
12611283}
12621284
1285+ function RegionsDropdown ( {
1286+ trigger,
1287+ clearSearchValue,
1288+ searchValue,
1289+ onClose,
1290+ } : {
1291+ trigger : ReactNode ;
1292+ clearSearchValue : ( ) => void ;
1293+ searchValue : string ;
1294+ onClose ?: ( ) => void ;
1295+ } ) {
1296+ const { values, replace } = useSearchParams ( ) ;
1297+ const regions = useRegions ( ) ;
1298+
1299+ const handleChange = ( values : string [ ] ) => {
1300+ clearSearchValue ( ) ;
1301+ replace ( {
1302+ regions : values . length > 0 ? values : undefined ,
1303+ cursor : undefined ,
1304+ direction : undefined ,
1305+ } ) ;
1306+ } ;
1307+
1308+ const selected = values ( "regions" ) . filter ( ( v ) => v !== "" ) ;
1309+
1310+ const filtered = useMemo ( ( ) => {
1311+ type RegionItem = { masterQueue : string ; name : string ; location ?: string } ;
1312+ const items : RegionItem [ ] = [ ] ;
1313+
1314+ for ( const masterQueue of selected ) {
1315+ const known = regions . find ( ( r ) => r . masterQueue === masterQueue ) ;
1316+ if ( ! known ) {
1317+ items . push ( { masterQueue, name : masterQueue } ) ;
1318+ }
1319+ }
1320+
1321+ for ( const region of regions ) {
1322+ if ( ! items . some ( ( i ) => i . masterQueue === region . masterQueue ) ) {
1323+ items . push ( {
1324+ masterQueue : region . masterQueue ,
1325+ name : region . name ,
1326+ location : region . location ,
1327+ } ) ;
1328+ }
1329+ }
1330+
1331+ return matchSorter ( items , searchValue , { keys : [ "name" , "masterQueue" ] } ) ;
1332+ } , [ searchValue , regions , selected . join ( "," ) ] ) ;
1333+
1334+ return (
1335+ < SelectProvider value = { selected } setValue = { handleChange } virtualFocus = { true } >
1336+ { trigger }
1337+ < SelectPopover
1338+ className = "min-w-0 max-w-[min(320px,var(--popover-available-width))]"
1339+ hideOnEscape = { ( ) => {
1340+ if ( onClose ) {
1341+ onClose ( ) ;
1342+ return false ;
1343+ }
1344+ return true ;
1345+ } }
1346+ >
1347+ < ComboBox
1348+ value = { searchValue }
1349+ render = { ( props ) => (
1350+ < div className = "flex items-center justify-stretch" >
1351+ < input { ...props } placeholder = { "Filter by region..." } />
1352+ </ div >
1353+ ) }
1354+ />
1355+ < SelectList >
1356+ { filtered . length > 0
1357+ ? filtered . map ( ( region ) => (
1358+ < SelectItem
1359+ key = { region . masterQueue }
1360+ value = { region . masterQueue }
1361+ className = "text-text-bright"
1362+ >
1363+ < RegionLabel region = { region } iconClassName = "size-4" />
1364+ </ SelectItem >
1365+ ) )
1366+ : null }
1367+ { filtered . length === 0 && < SelectItem disabled > No regions found</ SelectItem > }
1368+ </ SelectList >
1369+ </ SelectPopover >
1370+ </ SelectProvider >
1371+ ) ;
1372+ }
1373+
1374+ function AppliedRegionsFilter ( ) {
1375+ const { values, del } = useSearchParams ( ) ;
1376+ const environment = useEnvironment ( ) ;
1377+ const knownRegions = useRegions ( ) ;
1378+
1379+ const regions = values ( "regions" ) ;
1380+
1381+ if ( environment . type === "DEVELOPMENT" ) {
1382+ return null ;
1383+ }
1384+
1385+ if ( regions . length === 0 || regions . every ( ( v ) => v === "" ) ) {
1386+ return null ;
1387+ }
1388+
1389+ const labels = regions . map ( ( mq ) => {
1390+ const match = knownRegions . find ( ( r ) => r . masterQueue === mq ) ;
1391+ return match ?. name ?? mq ;
1392+ } ) ;
1393+
1394+ return (
1395+ < FilterMenuProvider >
1396+ { ( search , setSearch ) => (
1397+ < RegionsDropdown
1398+ trigger = {
1399+ < Ariakit . Select render = { < div className = "group cursor-pointer focus-custom" /> } >
1400+ < AppliedFilter
1401+ label = "Region"
1402+ icon = { filterIcon ( "regions" ) }
1403+ value = { appliedSummary ( labels ) }
1404+ onRemove = { ( ) => del ( [ "regions" , "cursor" , "direction" ] ) }
1405+ variant = "secondary/small"
1406+ />
1407+ </ Ariakit . Select >
1408+ }
1409+ searchValue = { search }
1410+ clearSearchValue = { ( ) => setSearch ( "" ) }
1411+ />
1412+ ) }
1413+ </ FilterMenuProvider >
1414+ ) ;
1415+ }
1416+
12631417function MachinesDropdown ( {
12641418 trigger,
12651419 clearSearchValue,
0 commit comments