Skip to content

Commit 4c42f6c

Browse files
authored
feat(webapp,core,cli): filter runs by region in dashboard, API, and MCP (#3612)
## Summary Adds a Region column and Region filter (under More filters) to the runs list dashboard, the same filter on the public runs list API (`filter[region]`), and a matching `region` input on the MCP `list_runs` tool. Each run's executing region is also surfaced as a new optional `region` field on the runs list and run retrieve responses, populated from the worker instance group's `masterQueue` identifier. Useful when you run tasks across multiple regions and want to slice the runs list — or your existing run-querying scripts — by where the run actually executed. ## Design The filter value in the URL / API is the `masterQueue` identifier (the same string already persisted on `TaskRun` and replicated to ClickHouse as `worker_queue`), so the query just becomes `worker_queue IN (...)` with no server-side translation. The Region dropdown options come from a new resource loader backed by `RegionsPresenter`, which now also exposes `masterQueue` alongside the existing region metadata. ```ts // public API const runs = await runs.list({ region: ["us-east-1", "eu-west-1"] }); // each item: { id, status, ..., region?: "us-east-1" } ``` ```ts // MCP list_runs({ environment: "prod", region: "us-east-1" }) ```
1 parent 454f0c9 commit 4c42f6c

22 files changed

Lines changed: 318 additions & 16 deletions

File tree

.changeset/mcp-list-runs-region.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
MCP `list_runs` tool: add a `region` filter input and surface each run's executing region in the formatted summary.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@trigger.dev/core": patch
3+
"@trigger.dev/sdk": patch
4+
---
5+
6+
Add `region` to the runs list / retrieve API: filter runs by region (`runs.list({ region: "..." })` / `filter[region]=<masterQueue>`) and read each run's executing region from the new `region` field on the response.

apps/webapp/app/components/BulkActionFilterSummary.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,19 @@ export function BulkActionFilterSummary({
215215
/>
216216
);
217217
}
218+
case "regions": {
219+
const values = Array.isArray(value) ? value : [`${value}`];
220+
return (
221+
<AppliedFilter
222+
variant="minimal/medium"
223+
key={key}
224+
label={filterTitle(key)}
225+
icon={filterIcon(key)}
226+
value={appliedSummary(values)}
227+
removable={false}
228+
/>
229+
);
230+
}
218231
case "machines": {
219232
const values = Array.isArray(value) ? value : [`${value}`];
220233
return (
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { FlagIcon } from "~/assets/icons/RegionIcons";
2+
import { cn } from "~/utils/cn";
3+
4+
type RegionLabelProps = {
5+
region: {
6+
name: string;
7+
location?: string | null;
8+
};
9+
className?: string;
10+
iconClassName?: string;
11+
};
12+
13+
export function RegionLabel({ region, className, iconClassName }: RegionLabelProps) {
14+
return (
15+
<span className={cn("flex items-center gap-1", className)}>
16+
{region.location ? (
17+
<FlagIcon region={region.location} className={cn("size-5", iconClassName)} />
18+
) : null}
19+
{region.name}
20+
</span>
21+
);
22+
}

apps/webapp/app/components/runs/v3/RunFilters.tsx

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ClockIcon,
55
CpuChipIcon,
66
FingerPrintIcon,
7+
GlobeAltIcon,
78
PlusIcon,
89
RectangleStackIcon,
910
Squares2X2Icon,
@@ -61,6 +62,8 @@ import { useShortcutKeys } from "~/hooks/useShortcutKeys";
6162
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
6263
import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags";
6364
import { 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";
6467
import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions";
6568
import { Button } from "../../primitives/Buttons";
6669
import { 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

505524
function 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+
12631417
function MachinesDropdown({
12641418
trigger,
12651419
clearSearchValue,

0 commit comments

Comments
 (0)