Skip to content

Commit eb89909

Browse files
committed
feat(webapp,core,cli): filter runs by region in dashboard, API, and MCP
Adds a "Region" column and "Region" filter (under More filters) to the runs list, plus the same filter on the public runs list API (filter[region]) and the MCP list_runs tool (region input). Each run's executing region is exposed as a new optional region field on the runs list and run retrieve responses, populated from the worker instance group's masterQueue identifier.
1 parent a90949b commit eb89909

20 files changed

Lines changed: 315 additions & 6 deletions

.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 (

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

Lines changed: 180 additions & 0 deletions
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,7 @@ 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 { type loader as regionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions";
6466
import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions";
6567
import { Button } from "../../primitives/Buttons";
6668
import { AIFilterInput } from "./AIFilterInput";
@@ -187,6 +189,9 @@ export const TaskRunListSearchFilters = z.object({
187189
"Schedule ID to filter by - shows runs from a specific schedule. They start with sched_"
188190
),
189191
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+
),
190195
machines: MachinePresetOrMachinePresetArray.describe(
191196
`Machine presets to filter by (${machines.join(", ")})`
192197
),
@@ -229,6 +234,8 @@ export function filterTitle(filterKey: string) {
229234
return "Schedule ID";
230235
case "queues":
231236
return "Queues";
237+
case "regions":
238+
return "Region";
232239
case "machines":
233240
return "Machine";
234241
case "versions":
@@ -271,6 +278,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
271278
return <ClockIcon className="size-4" />;
272279
case "queues":
273280
return <RectangleStackIcon className="size-4" />;
281+
case "regions":
282+
return <GlobeAltIcon className="size-4" />;
274283
case "machines":
275284
return <MachineDefaultIcon className="size-4" />;
276285
case "versions":
@@ -317,6 +326,10 @@ export function getRunFiltersFromSearchParams(
317326
searchParams.getAll("queues").filter((v) => v.length > 0).length > 0
318327
? searchParams.getAll("queues")
319328
: undefined,
329+
regions:
330+
searchParams.getAll("regions").filter((v) => v.length > 0).length > 0
331+
? searchParams.getAll("regions")
332+
: undefined,
320333
machines:
321334
searchParams.getAll("machines").filter((v) => v.length > 0).length > 0
322335
? searchParams.getAll("machines")
@@ -402,6 +415,7 @@ const filterTypes = [
402415
{ name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> },
403416
{ name: "versions", title: "Versions", icon: <IconRotateClockwise2 className="size-4" /> },
404417
{ name: "queues", title: "Queues", icon: <RectangleStackIcon className="size-4" /> },
418+
{ name: "regions", title: "Region", icon: <GlobeAltIcon className="size-4" /> },
405419
{ name: "machines", title: "Machines", icon: <MachineDefaultIcon className="size-4" /> },
406420
{ name: "run", title: "Run ID", icon: <FingerPrintIcon className="size-4" /> },
407421
{ name: "batch", title: "Batch ID", icon: <Squares2X2Icon className="size-4" /> },
@@ -456,6 +470,7 @@ function AppliedFilters({ bulkActions }: RunFiltersProps) {
456470
<AppliedTagsFilter />
457471
<AppliedVersionsFilter />
458472
<AppliedQueuesFilter />
473+
<AppliedRegionsFilter />
459474
<AppliedMachinesFilter />
460475
<AppliedRunIdFilter />
461476
<AppliedBatchIdFilter />
@@ -485,6 +500,8 @@ function Menu(props: MenuProps) {
485500
return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
486501
case "queues":
487502
return <QueuesDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
503+
case "regions":
504+
return <RegionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
488505
case "machines":
489506
return <MachinesDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
490507
case "run":
@@ -1260,6 +1277,169 @@ function AppliedQueuesFilter() {
12601277
);
12611278
}
12621279

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+
12631443
function MachinesDropdown({
12641444
trigger,
12651445
clearSearchValue,

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

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type RunsTableProps = {
7272
variant?: TableVariant;
7373
disableAdjacentRows?: boolean;
7474
additionalTableState?: Record<string, string>;
75+
regions?: { masterQueue: string; name: string }[];
7576
};
7677

7778
export function TaskRunsTable({
@@ -85,7 +86,11 @@ export function TaskRunsTable({
8586
allowSelection = false,
8687
variant = "dimmed",
8788
additionalTableState,
89+
regions,
8890
}: RunsTableProps) {
91+
const regionNameByMasterQueue = new Map(
92+
(regions ?? []).map((r) => [r.masterQueue, r.name] as const)
93+
);
8994
const organization = useOrganization();
9095
const project = useProject();
9196
const checkboxes = useRef<(HTMLInputElement | null)[]>([]);
@@ -233,6 +238,7 @@ export function TaskRunsTable({
233238
Machine
234239
</TableHeaderCell>
235240
<TableHeaderCell>Queue</TableHeaderCell>
241+
<TableHeaderCell>Region</TableHeaderCell>
236242
<TableHeaderCell>Test</TableHeaderCell>
237243
<TableHeaderCell>Created at</TableHeaderCell>
238244
<TableHeaderCell
@@ -312,7 +318,7 @@ export function TaskRunsTable({
312318
</TableHeader>
313319
<TableBody>
314320
{total === 0 && !hasFilters ? (
315-
<TableBlankRow colSpan={15}>
321+
<TableBlankRow colSpan={16}>
316322
{!isLoading && <NoRuns title="No runs found" />}
317323
</TableBlankRow>
318324
) : runs.length === 0 ? (
@@ -441,6 +447,9 @@ export function TaskRunsTable({
441447
<span>{run.queue.name}</span>
442448
</span>
443449
</TableCell>
450+
<TableCell to={path}>
451+
{run.region ? regionNameByMasterQueue.get(run.region) ?? run.region : "–"}
452+
</TableCell>
444453
<TableCell to={path}>
445454
{run.isTest ? (
446455
<CheckIcon className="size-4 text-charcoal-400 group-hover/table-row:text-text-bright" />
@@ -467,7 +476,7 @@ export function TaskRunsTable({
467476
)}
468477
{isLoading && (
469478
<TableBlankRow
470-
colSpan={15}
479+
colSpan={16}
471480
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-background-dimmed"
472481
>
473482
<Spinner /> <span className="text-text-dimmed">Loading…</span>
@@ -607,7 +616,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
607616
const organization = useOrganization();
608617
const project = useProject();
609618
const environment = useEnvironment();
610-
if (isLoading) return <TableBlankRow colSpan={15}></TableBlankRow>;
619+
if (isLoading) return <TableBlankRow colSpan={16}></TableBlankRow>;
611620

612621
const { tasks, from, to, ...otherFilters } = filters;
613622
const singleTaskFromFilters = filters.tasks.length === 1 ? filters.tasks[0] : null;
@@ -622,7 +631,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
622631
Object.values(otherFilters).every((filterArray) => filterArray.length === 0)
623632
) {
624633
return (
625-
<TableBlankRow colSpan={15}>
634+
<TableBlankRow colSpan={16}>
626635
<Paragraph className="w-auto" variant="base/bright" spacing>
627636
There are no runs for {filters.tasks[0]}
628637
</Paragraph>
@@ -650,7 +659,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
650659
}
651660

652661
return (
653-
<TableBlankRow colSpan={15}>
662+
<TableBlankRow colSpan={16}>
654663
<div className="flex flex-col items-center justify-center gap-6">
655664
<Paragraph className="w-auto" variant="base/bright">
656665
No runs match your filters. Try refreshing, modifying your filters or run a test.

apps/webapp/app/presenters/RunFilters.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export async function getRunFiltersFromRequest(request: Request): Promise<Filter
3434
batchId,
3535
scheduleId,
3636
queues,
37+
regions,
3738
machines,
3839
errorId,
3940
sources,
@@ -55,6 +56,7 @@ export async function getRunFiltersFromRequest(request: Request): Promise<Filter
5556
direction: direction,
5657
cursor: cursor,
5758
queues,
59+
regions,
5860
machines,
5961
errorId,
6062
sources,

apps/webapp/app/presenters/v3/ApiRetrieveRunPresenter.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const commonRunSelect = {
4242
isTest: true,
4343
depth: true,
4444
scheduleId: true,
45+
workerQueue: true,
4546
lockedToVersion: {
4647
select: {
4748
version: true,
@@ -463,6 +464,7 @@ async function createCommonRunStructure(run: CommonRelatedRun, apiVersion: API_V
463464
triggerFunction: resolveTriggerFunction(run),
464465
batchId: run.batch?.friendlyId,
465466
metadata,
467+
region: run.workerQueue || undefined,
466468
};
467469
}
468470

0 commit comments

Comments
 (0)