|
4 | 4 | ClockIcon, |
5 | 5 | CpuChipIcon, |
6 | 6 | FingerPrintIcon, |
| 7 | + GlobeAltIcon, |
7 | 8 | PlusIcon, |
8 | 9 | RectangleStackIcon, |
9 | 10 | Squares2X2Icon, |
@@ -61,6 +62,7 @@ import { useShortcutKeys } from "~/hooks/useShortcutKeys"; |
61 | 62 | import { ShortcutKey } from "~/components/primitives/ShortcutKey"; |
62 | 63 | import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags"; |
63 | 64 | import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; |
| 65 | +import { type loader as regionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions"; |
64 | 66 | import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions"; |
65 | 67 | import { Button } from "../../primitives/Buttons"; |
66 | 68 | import { AIFilterInput } from "./AIFilterInput"; |
@@ -187,6 +189,9 @@ export const TaskRunListSearchFilters = z.object({ |
187 | 189 | "Schedule ID to filter by - shows runs from a specific schedule. They start with sched_" |
188 | 190 | ), |
189 | 191 | queues: StringOrStringArray.describe("Queue names to filter by (these are user-defined names)"), |
| 192 | + regions: StringOrStringArray.describe( |
| 193 | + "Region master-queue identifiers to filter by (the worker instance group masterQueue values)" |
| 194 | + ), |
190 | 195 | machines: MachinePresetOrMachinePresetArray.describe( |
191 | 196 | `Machine presets to filter by (${machines.join(", ")})` |
192 | 197 | ), |
@@ -229,6 +234,8 @@ export function filterTitle(filterKey: string) { |
229 | 234 | return "Schedule ID"; |
230 | 235 | case "queues": |
231 | 236 | return "Queues"; |
| 237 | + case "regions": |
| 238 | + return "Region"; |
232 | 239 | case "machines": |
233 | 240 | return "Machine"; |
234 | 241 | case "versions": |
@@ -271,6 +278,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined { |
271 | 278 | return <ClockIcon className="size-4" />; |
272 | 279 | case "queues": |
273 | 280 | return <RectangleStackIcon className="size-4" />; |
| 281 | + case "regions": |
| 282 | + return <GlobeAltIcon className="size-4" />; |
274 | 283 | case "machines": |
275 | 284 | return <MachineDefaultIcon className="size-4" />; |
276 | 285 | case "versions": |
@@ -317,6 +326,10 @@ export function getRunFiltersFromSearchParams( |
317 | 326 | searchParams.getAll("queues").filter((v) => v.length > 0).length > 0 |
318 | 327 | ? searchParams.getAll("queues") |
319 | 328 | : undefined, |
| 329 | + regions: |
| 330 | + searchParams.getAll("regions").filter((v) => v.length > 0).length > 0 |
| 331 | + ? searchParams.getAll("regions") |
| 332 | + : undefined, |
320 | 333 | machines: |
321 | 334 | searchParams.getAll("machines").filter((v) => v.length > 0).length > 0 |
322 | 335 | ? searchParams.getAll("machines") |
@@ -402,6 +415,7 @@ const filterTypes = [ |
402 | 415 | { name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> }, |
403 | 416 | { name: "versions", title: "Versions", icon: <IconRotateClockwise2 className="size-4" /> }, |
404 | 417 | { name: "queues", title: "Queues", icon: <RectangleStackIcon className="size-4" /> }, |
| 418 | + { name: "regions", title: "Region", icon: <GlobeAltIcon className="size-4" /> }, |
405 | 419 | { name: "machines", title: "Machines", icon: <MachineDefaultIcon className="size-4" /> }, |
406 | 420 | { name: "run", title: "Run ID", icon: <FingerPrintIcon className="size-4" /> }, |
407 | 421 | { name: "batch", title: "Batch ID", icon: <Squares2X2Icon className="size-4" /> }, |
@@ -456,6 +470,7 @@ function AppliedFilters({ bulkActions }: RunFiltersProps) { |
456 | 470 | <AppliedTagsFilter /> |
457 | 471 | <AppliedVersionsFilter /> |
458 | 472 | <AppliedQueuesFilter /> |
| 473 | + <AppliedRegionsFilter /> |
459 | 474 | <AppliedMachinesFilter /> |
460 | 475 | <AppliedRunIdFilter /> |
461 | 476 | <AppliedBatchIdFilter /> |
@@ -485,6 +500,8 @@ function Menu(props: MenuProps) { |
485 | 500 | return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
486 | 501 | case "queues": |
487 | 502 | return <QueuesDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
| 503 | + case "regions": |
| 504 | + return <RegionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
488 | 505 | case "machines": |
489 | 506 | return <MachinesDropdown onClose={() => props.setFilterType(undefined)} {...props} />; |
490 | 507 | case "run": |
@@ -1260,6 +1277,169 @@ function AppliedQueuesFilter() { |
1260 | 1277 | ); |
1261 | 1278 | } |
1262 | 1279 |
|
| 1280 | +function RegionsDropdown({ |
| 1281 | + trigger, |
| 1282 | + clearSearchValue, |
| 1283 | + searchValue, |
| 1284 | + onClose, |
| 1285 | +}: { |
| 1286 | + trigger: ReactNode; |
| 1287 | + clearSearchValue: () => void; |
| 1288 | + searchValue: string; |
| 1289 | + onClose?: () => void; |
| 1290 | +}) { |
| 1291 | + const organization = useOrganization(); |
| 1292 | + const project = useProject(); |
| 1293 | + const environment = useEnvironment(); |
| 1294 | + const { values, replace } = useSearchParams(); |
| 1295 | + |
| 1296 | + const handleChange = (values: string[]) => { |
| 1297 | + clearSearchValue(); |
| 1298 | + replace({ |
| 1299 | + regions: values.length > 0 ? values : undefined, |
| 1300 | + cursor: undefined, |
| 1301 | + direction: undefined, |
| 1302 | + }); |
| 1303 | + }; |
| 1304 | + |
| 1305 | + const selected = values("regions").filter((v) => v !== ""); |
| 1306 | + |
| 1307 | + const fetcher = useFetcher<typeof regionsLoader>(); |
| 1308 | + |
| 1309 | + useEffect(() => { |
| 1310 | + if (fetcher.state === "idle" && fetcher.data === undefined) { |
| 1311 | + fetcher.load( |
| 1312 | + `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/regions` |
| 1313 | + ); |
| 1314 | + } |
| 1315 | + }, [fetcher.state, fetcher.data, organization.slug, project.slug, environment.slug]); |
| 1316 | + |
| 1317 | + const filtered = useMemo(() => { |
| 1318 | + const items: { masterQueue: string; name: string }[] = []; |
| 1319 | + |
| 1320 | + for (const masterQueue of selected) { |
| 1321 | + const known = fetcher.data?.regions.find((r) => r.masterQueue === masterQueue); |
| 1322 | + if (!known) { |
| 1323 | + items.push({ masterQueue, name: masterQueue }); |
| 1324 | + } |
| 1325 | + } |
| 1326 | + |
| 1327 | + if (fetcher.data) { |
| 1328 | + for (const region of fetcher.data.regions) { |
| 1329 | + if (!items.some((i) => i.masterQueue === region.masterQueue)) { |
| 1330 | + items.push({ masterQueue: region.masterQueue, name: region.name }); |
| 1331 | + } |
| 1332 | + } |
| 1333 | + } |
| 1334 | + |
| 1335 | + return matchSorter(items, searchValue, { keys: ["name", "masterQueue"] }); |
| 1336 | + }, [searchValue, fetcher.data, selected.join(",")]); |
| 1337 | + |
| 1338 | + return ( |
| 1339 | + <SelectProvider value={selected} setValue={handleChange} virtualFocus={true}> |
| 1340 | + {trigger} |
| 1341 | + <SelectPopover |
| 1342 | + className="min-w-0 max-w-[min(240px,var(--popover-available-width))]" |
| 1343 | + hideOnEscape={() => { |
| 1344 | + if (onClose) { |
| 1345 | + onClose(); |
| 1346 | + return false; |
| 1347 | + } |
| 1348 | + return true; |
| 1349 | + }} |
| 1350 | + > |
| 1351 | + <ComboBox |
| 1352 | + value={searchValue} |
| 1353 | + render={(props) => ( |
| 1354 | + <div className="flex items-center justify-stretch"> |
| 1355 | + <input {...props} placeholder={"Filter by region..."} /> |
| 1356 | + {fetcher.state === "loading" && <Spinner color="muted" />} |
| 1357 | + </div> |
| 1358 | + )} |
| 1359 | + /> |
| 1360 | + <SelectList> |
| 1361 | + {filtered.length > 0 |
| 1362 | + ? filtered.map((region) => ( |
| 1363 | + <SelectItem |
| 1364 | + key={region.masterQueue} |
| 1365 | + value={region.masterQueue} |
| 1366 | + icon={<GlobeAltIcon className="size-4 shrink-0 text-text-dimmed" />} |
| 1367 | + className="text-text-bright" |
| 1368 | + > |
| 1369 | + {region.name} |
| 1370 | + </SelectItem> |
| 1371 | + )) |
| 1372 | + : null} |
| 1373 | + {filtered.length === 0 && fetcher.state !== "loading" && ( |
| 1374 | + <SelectItem disabled>No regions found</SelectItem> |
| 1375 | + )} |
| 1376 | + </SelectList> |
| 1377 | + </SelectPopover> |
| 1378 | + </SelectProvider> |
| 1379 | + ); |
| 1380 | +} |
| 1381 | + |
| 1382 | +function AppliedRegionsFilter() { |
| 1383 | + const { values, del } = useSearchParams(); |
| 1384 | + const organization = useOrganization(); |
| 1385 | + const project = useProject(); |
| 1386 | + const environment = useEnvironment(); |
| 1387 | + const fetcher = useFetcher<typeof regionsLoader>(); |
| 1388 | + |
| 1389 | + const regions = values("regions"); |
| 1390 | + |
| 1391 | + useEffect(() => { |
| 1392 | + if ( |
| 1393 | + regions.length > 0 && |
| 1394 | + !regions.every((v) => v === "") && |
| 1395 | + fetcher.state === "idle" && |
| 1396 | + fetcher.data === undefined |
| 1397 | + ) { |
| 1398 | + fetcher.load( |
| 1399 | + `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/regions` |
| 1400 | + ); |
| 1401 | + } |
| 1402 | + }, [ |
| 1403 | + regions.join(","), |
| 1404 | + fetcher.state, |
| 1405 | + fetcher.data, |
| 1406 | + organization.slug, |
| 1407 | + project.slug, |
| 1408 | + environment.slug, |
| 1409 | + ]); |
| 1410 | + |
| 1411 | + if (regions.length === 0 || regions.every((v) => v === "")) { |
| 1412 | + return null; |
| 1413 | + } |
| 1414 | + |
| 1415 | + const labels = regions.map((mq) => { |
| 1416 | + const match = fetcher.data?.regions.find((r) => r.masterQueue === mq); |
| 1417 | + return match?.name ?? mq; |
| 1418 | + }); |
| 1419 | + |
| 1420 | + return ( |
| 1421 | + <FilterMenuProvider> |
| 1422 | + {(search, setSearch) => ( |
| 1423 | + <RegionsDropdown |
| 1424 | + trigger={ |
| 1425 | + <Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}> |
| 1426 | + <AppliedFilter |
| 1427 | + label="Region" |
| 1428 | + icon={filterIcon("regions")} |
| 1429 | + value={appliedSummary(labels)} |
| 1430 | + onRemove={() => del(["regions", "cursor", "direction"])} |
| 1431 | + variant="secondary/small" |
| 1432 | + /> |
| 1433 | + </Ariakit.Select> |
| 1434 | + } |
| 1435 | + searchValue={search} |
| 1436 | + clearSearchValue={() => setSearch("")} |
| 1437 | + /> |
| 1438 | + )} |
| 1439 | + </FilterMenuProvider> |
| 1440 | + ); |
| 1441 | +} |
| 1442 | + |
1263 | 1443 | function MachinesDropdown({ |
1264 | 1444 | trigger, |
1265 | 1445 | clearSearchValue, |
|
0 commit comments