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" })
```
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
MCP `list_runs` tool: add a `region` filter input and surface each run's executing region in the formatted summary.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
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.
|
||||
@@ -215,6 +215,19 @@ export function BulkActionFilterSummary({
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "regions": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
<AppliedFilter
|
||||
variant="minimal/medium"
|
||||
key={key}
|
||||
label={filterTitle(key)}
|
||||
icon={filterIcon(key)}
|
||||
value={appliedSummary(values)}
|
||||
removable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case "machines": {
|
||||
const values = Array.isArray(value) ? value : [`${value}`];
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { FlagIcon } from "~/assets/icons/RegionIcons";
|
||||
import { cn } from "~/utils/cn";
|
||||
|
||||
type RegionLabelProps = {
|
||||
region: {
|
||||
name: string;
|
||||
location?: string | null;
|
||||
};
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
};
|
||||
|
||||
export function RegionLabel({ region, className, iconClassName }: RegionLabelProps) {
|
||||
return (
|
||||
<span className={cn("flex items-center gap-1", className)}>
|
||||
{region.location ? (
|
||||
<FlagIcon region={region.location} className={cn("size-5", iconClassName)} />
|
||||
) : null}
|
||||
{region.name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ClockIcon,
|
||||
CpuChipIcon,
|
||||
FingerPrintIcon,
|
||||
GlobeAltIcon,
|
||||
PlusIcon,
|
||||
RectangleStackIcon,
|
||||
Squares2X2Icon,
|
||||
@@ -61,6 +62,8 @@ import { useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { ShortcutKey } from "~/components/primitives/ShortcutKey";
|
||||
import { type loader as tagsLoader } from "~/routes/resources.environments.$envId.runs.tags";
|
||||
import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues";
|
||||
import { useRegions } from "~/hooks/useRegions";
|
||||
import { RegionLabel } from "./RegionLabel";
|
||||
import { type loader as versionsLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.versions";
|
||||
import { Button } from "../../primitives/Buttons";
|
||||
import { AIFilterInput } from "./AIFilterInput";
|
||||
@@ -187,6 +190,9 @@ export const TaskRunListSearchFilters = z.object({
|
||||
"Schedule ID to filter by - shows runs from a specific schedule. They start with sched_"
|
||||
),
|
||||
queues: StringOrStringArray.describe("Queue names to filter by (these are user-defined names)"),
|
||||
regions: StringOrStringArray.describe(
|
||||
"Region master-queue identifiers to filter by (the worker instance group masterQueue values)"
|
||||
),
|
||||
machines: MachinePresetOrMachinePresetArray.describe(
|
||||
`Machine presets to filter by (${machines.join(", ")})`
|
||||
),
|
||||
@@ -229,6 +235,8 @@ export function filterTitle(filterKey: string) {
|
||||
return "Schedule ID";
|
||||
case "queues":
|
||||
return "Queues";
|
||||
case "regions":
|
||||
return "Region";
|
||||
case "machines":
|
||||
return "Machine";
|
||||
case "versions":
|
||||
@@ -271,6 +279,8 @@ export function filterIcon(filterKey: string): ReactNode | undefined {
|
||||
return <ClockIcon className="size-4" />;
|
||||
case "queues":
|
||||
return <RectangleStackIcon className="size-4" />;
|
||||
case "regions":
|
||||
return <GlobeAltIcon className="size-4" />;
|
||||
case "machines":
|
||||
return <MachineDefaultIcon className="size-4" />;
|
||||
case "versions":
|
||||
@@ -317,6 +327,10 @@ export function getRunFiltersFromSearchParams(
|
||||
searchParams.getAll("queues").filter((v) => v.length > 0).length > 0
|
||||
? searchParams.getAll("queues")
|
||||
: undefined,
|
||||
regions:
|
||||
searchParams.getAll("regions").filter((v) => v.length > 0).length > 0
|
||||
? searchParams.getAll("regions")
|
||||
: undefined,
|
||||
machines:
|
||||
searchParams.getAll("machines").filter((v) => v.length > 0).length > 0
|
||||
? searchParams.getAll("machines")
|
||||
@@ -369,6 +383,7 @@ export function RunsFilters(props: RunFiltersProps) {
|
||||
searchParams.has("runId") ||
|
||||
searchParams.has("scheduleId") ||
|
||||
searchParams.has("queues") ||
|
||||
searchParams.has("regions") ||
|
||||
searchParams.has("machines") ||
|
||||
searchParams.has("versions") ||
|
||||
searchParams.has("errorId") ||
|
||||
@@ -402,6 +417,7 @@ const filterTypes = [
|
||||
{ name: "tags", title: "Tags", icon: <TagIcon className="size-4" /> },
|
||||
{ name: "versions", title: "Versions", icon: <IconRotateClockwise2 className="size-4" /> },
|
||||
{ name: "queues", title: "Queues", icon: <RectangleStackIcon className="size-4" /> },
|
||||
{ name: "regions", title: "Region", icon: <GlobeAltIcon className="size-4" /> },
|
||||
{ name: "machines", title: "Machines", icon: <MachineDefaultIcon className="size-4" /> },
|
||||
{ name: "run", title: "Run ID", icon: <FingerPrintIcon className="size-4" /> },
|
||||
{ name: "batch", title: "Batch ID", icon: <Squares2X2Icon className="size-4" /> },
|
||||
@@ -456,6 +472,7 @@ function AppliedFilters({ bulkActions }: RunFiltersProps) {
|
||||
<AppliedTagsFilter />
|
||||
<AppliedVersionsFilter />
|
||||
<AppliedQueuesFilter />
|
||||
<AppliedRegionsFilter />
|
||||
<AppliedMachinesFilter />
|
||||
<AppliedRunIdFilter />
|
||||
<AppliedBatchIdFilter />
|
||||
@@ -485,6 +502,8 @@ function Menu(props: MenuProps) {
|
||||
return <TagsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "queues":
|
||||
return <QueuesDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "regions":
|
||||
return <RegionsDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "machines":
|
||||
return <MachinesDropdown onClose={() => props.setFilterType(undefined)} {...props} />;
|
||||
case "run":
|
||||
@@ -503,11 +522,14 @@ function Menu(props: MenuProps) {
|
||||
}
|
||||
|
||||
function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: MenuProps) {
|
||||
const environment = useEnvironment();
|
||||
const showRegion = environment.type !== "DEVELOPMENT";
|
||||
const filtered = useMemo(() => {
|
||||
return filterTypes.filter((item) => {
|
||||
if (item.name === "regions" && !showRegion) return false;
|
||||
return item.title.toLowerCase().includes(searchValue.toLowerCase());
|
||||
});
|
||||
}, [searchValue]);
|
||||
}, [searchValue, showRegion]);
|
||||
|
||||
return (
|
||||
<SelectProvider virtualFocus={true}>
|
||||
@@ -1260,6 +1282,138 @@ function AppliedQueuesFilter() {
|
||||
);
|
||||
}
|
||||
|
||||
function RegionsDropdown({
|
||||
trigger,
|
||||
clearSearchValue,
|
||||
searchValue,
|
||||
onClose,
|
||||
}: {
|
||||
trigger: ReactNode;
|
||||
clearSearchValue: () => void;
|
||||
searchValue: string;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { values, replace } = useSearchParams();
|
||||
const regions = useRegions();
|
||||
|
||||
const handleChange = (values: string[]) => {
|
||||
clearSearchValue();
|
||||
replace({
|
||||
regions: values.length > 0 ? values : undefined,
|
||||
cursor: undefined,
|
||||
direction: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const selected = values("regions").filter((v) => v !== "");
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
type RegionItem = { masterQueue: string; name: string; location?: string };
|
||||
const items: RegionItem[] = [];
|
||||
|
||||
for (const masterQueue of selected) {
|
||||
const known = regions.find((r) => r.masterQueue === masterQueue);
|
||||
if (!known) {
|
||||
items.push({ masterQueue, name: masterQueue });
|
||||
}
|
||||
}
|
||||
|
||||
for (const region of regions) {
|
||||
if (!items.some((i) => i.masterQueue === region.masterQueue)) {
|
||||
items.push({
|
||||
masterQueue: region.masterQueue,
|
||||
name: region.name,
|
||||
location: region.location,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return matchSorter(items, searchValue, { keys: ["name", "masterQueue"] });
|
||||
}, [searchValue, regions, selected.join(",")]);
|
||||
|
||||
return (
|
||||
<SelectProvider value={selected} setValue={handleChange} virtualFocus={true}>
|
||||
{trigger}
|
||||
<SelectPopover
|
||||
className="min-w-0 max-w-[min(320px,var(--popover-available-width))]"
|
||||
hideOnEscape={() => {
|
||||
if (onClose) {
|
||||
onClose();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
<ComboBox
|
||||
value={searchValue}
|
||||
render={(props) => (
|
||||
<div className="flex items-center justify-stretch">
|
||||
<input {...props} placeholder={"Filter by region..."} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<SelectList>
|
||||
{filtered.length > 0
|
||||
? filtered.map((region) => (
|
||||
<SelectItem
|
||||
key={region.masterQueue}
|
||||
value={region.masterQueue}
|
||||
className="text-text-bright"
|
||||
>
|
||||
<RegionLabel region={region} iconClassName="size-4" />
|
||||
</SelectItem>
|
||||
))
|
||||
: null}
|
||||
{filtered.length === 0 && <SelectItem disabled>No regions found</SelectItem>}
|
||||
</SelectList>
|
||||
</SelectPopover>
|
||||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function AppliedRegionsFilter() {
|
||||
const { values, del } = useSearchParams();
|
||||
const environment = useEnvironment();
|
||||
const knownRegions = useRegions();
|
||||
|
||||
const regions = values("regions");
|
||||
|
||||
if (environment.type === "DEVELOPMENT") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (regions.length === 0 || regions.every((v) => v === "")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const labels = regions.map((mq) => {
|
||||
const match = knownRegions.find((r) => r.masterQueue === mq);
|
||||
return match?.name ?? mq;
|
||||
});
|
||||
|
||||
return (
|
||||
<FilterMenuProvider>
|
||||
{(search, setSearch) => (
|
||||
<RegionsDropdown
|
||||
trigger={
|
||||
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
|
||||
<AppliedFilter
|
||||
label="Region"
|
||||
icon={filterIcon("regions")}
|
||||
value={appliedSummary(labels)}
|
||||
onRemove={() => del(["regions", "cursor", "direction"])}
|
||||
variant="secondary/small"
|
||||
/>
|
||||
</Ariakit.Select>
|
||||
}
|
||||
searchValue={search}
|
||||
clearSearchValue={() => setSearch("")}
|
||||
/>
|
||||
)}
|
||||
</FilterMenuProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function MachinesDropdown({
|
||||
trigger,
|
||||
clearSearchValue,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useSelectedItems } from "~/components/primitives/SelectedItemsProvider"
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
|
||||
import { useEnvironment } from "~/hooks/useEnvironment";
|
||||
import { useRegions } from "~/hooks/useRegions";
|
||||
import { useFeatures } from "~/hooks/useFeatures";
|
||||
import { useOrganization } from "~/hooks/useOrganizations";
|
||||
import { useProject } from "~/hooks/useProject";
|
||||
@@ -47,6 +48,7 @@ import {
|
||||
type TableVariant,
|
||||
} from "../../primitives/Table";
|
||||
import { CancelRunDialog } from "./CancelRunDialog";
|
||||
import { RegionLabel } from "./RegionLabel";
|
||||
import { LiveTimer } from "./LiveTimer";
|
||||
import { ReplayRunDialog } from "./ReplayRunDialog";
|
||||
import { RunTag } from "./RunTag";
|
||||
@@ -86,8 +88,11 @@ export function TaskRunsTable({
|
||||
variant = "dimmed",
|
||||
additionalTableState,
|
||||
}: RunsTableProps) {
|
||||
const regions = useRegions();
|
||||
const regionByMasterQueue = new Map(regions.map((r) => [r.masterQueue, r] as const));
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
const checkboxes = useRef<(HTMLInputElement | null)[]>([]);
|
||||
const { has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection);
|
||||
const { isManagedCloud } = useFeatures();
|
||||
@@ -107,6 +112,7 @@ export function TaskRunsTable({
|
||||
const tableStateParam = disableAdjacentRows ? "" : encodeURIComponent(search);
|
||||
|
||||
const showCompute = isManagedCloud;
|
||||
const showRegion = environment.type !== "DEVELOPMENT";
|
||||
|
||||
const navigateCheckboxes = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>, index: number) => {
|
||||
@@ -233,6 +239,7 @@ export function TaskRunsTable({
|
||||
Machine
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>Queue</TableHeaderCell>
|
||||
{showRegion && <TableHeaderCell>Region</TableHeaderCell>}
|
||||
<TableHeaderCell>Test</TableHeaderCell>
|
||||
<TableHeaderCell>Created at</TableHeaderCell>
|
||||
<TableHeaderCell
|
||||
@@ -312,11 +319,11 @@ export function TaskRunsTable({
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{total === 0 && !hasFilters ? (
|
||||
<TableBlankRow colSpan={15}>
|
||||
<TableBlankRow colSpan={showRegion ? 16 : 15}>
|
||||
{!isLoading && <NoRuns title="No runs found" />}
|
||||
</TableBlankRow>
|
||||
) : runs.length === 0 ? (
|
||||
<BlankState isLoading={isLoading} filters={filters} />
|
||||
<BlankState isLoading={isLoading} filters={filters} showRegion={showRegion} />
|
||||
) : (
|
||||
runs.map((run, index) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
@@ -441,6 +448,20 @@ export function TaskRunsTable({
|
||||
<span>{run.queue.name}</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
{showRegion && (
|
||||
<TableCell to={path}>
|
||||
{run.region ? (
|
||||
<RegionLabel
|
||||
region={
|
||||
regionByMasterQueue.get(run.region) ?? { name: run.region }
|
||||
}
|
||||
iconClassName="size-4"
|
||||
/>
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell to={path}>
|
||||
{run.isTest ? (
|
||||
<CheckIcon className="size-4 text-charcoal-400 group-hover/table-row:text-text-bright" />
|
||||
@@ -467,7 +488,7 @@ export function TaskRunsTable({
|
||||
)}
|
||||
{isLoading && (
|
||||
<TableBlankRow
|
||||
colSpan={15}
|
||||
colSpan={showRegion ? 16 : 15}
|
||||
className="absolute left-0 top-0 flex h-full w-full items-center justify-center gap-2 bg-background-dimmed"
|
||||
>
|
||||
<Spinner /> <span className="text-text-dimmed">Loading…</span>
|
||||
@@ -603,11 +624,16 @@ function NoRuns({ title }: { title: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "filters">) {
|
||||
function BlankState({
|
||||
isLoading,
|
||||
filters,
|
||||
showRegion,
|
||||
}: Pick<RunsTableProps, "isLoading" | "filters"> & { showRegion: boolean }) {
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const environment = useEnvironment();
|
||||
if (isLoading) return <TableBlankRow colSpan={15}></TableBlankRow>;
|
||||
const colSpan = showRegion ? 16 : 15;
|
||||
if (isLoading) return <TableBlankRow colSpan={colSpan}></TableBlankRow>;
|
||||
|
||||
const { tasks, from, to, ...otherFilters } = filters;
|
||||
const singleTaskFromFilters = filters.tasks.length === 1 ? filters.tasks[0] : null;
|
||||
@@ -622,7 +648,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
|
||||
Object.values(otherFilters).every((filterArray) => filterArray.length === 0)
|
||||
) {
|
||||
return (
|
||||
<TableBlankRow colSpan={15}>
|
||||
<TableBlankRow colSpan={colSpan}>
|
||||
<Paragraph className="w-auto" variant="base/bright" spacing>
|
||||
There are no runs for {filters.tasks[0]}
|
||||
</Paragraph>
|
||||
@@ -650,7 +676,7 @@ function BlankState({ isLoading, filters }: Pick<RunsTableProps, "isLoading" | "
|
||||
}
|
||||
|
||||
return (
|
||||
<TableBlankRow colSpan={15}>
|
||||
<TableBlankRow colSpan={colSpan}>
|
||||
<div className="flex flex-col items-center justify-center gap-6">
|
||||
<Paragraph className="w-auto" variant="base/bright">
|
||||
No runs match your filters. Try refreshing, modifying your filters or run a test.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { type UIMatch } from "@remix-run/react";
|
||||
import { type UseDataFunctionReturn } from "remix-typedjson";
|
||||
import type { loader as orgLoader } from "~/routes/_app.orgs.$organizationSlug/route";
|
||||
import { organizationMatchId } from "./useOrganizations";
|
||||
import { useTypedMatchesData } from "./useTypedMatchData";
|
||||
|
||||
export type MatchedRegion = UseDataFunctionReturn<typeof orgLoader>["regions"][number];
|
||||
|
||||
export function useRegions(matches?: UIMatch[]): MatchedRegion[] {
|
||||
const routeMatch = useTypedMatchesData<typeof orgLoader>({
|
||||
id: organizationMatchId,
|
||||
matches,
|
||||
});
|
||||
|
||||
return routeMatch?.regions ?? [];
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export async function getRunFiltersFromRequest(request: Request): Promise<Filter
|
||||
batchId,
|
||||
scheduleId,
|
||||
queues,
|
||||
regions,
|
||||
machines,
|
||||
errorId,
|
||||
sources,
|
||||
@@ -55,6 +56,7 @@ export async function getRunFiltersFromRequest(request: Request): Promise<Filter
|
||||
direction: direction,
|
||||
cursor: cursor,
|
||||
queues,
|
||||
regions,
|
||||
machines,
|
||||
errorId,
|
||||
sources,
|
||||
|
||||
@@ -42,6 +42,7 @@ const commonRunSelect = {
|
||||
isTest: true,
|
||||
depth: true,
|
||||
scheduleId: true,
|
||||
workerQueue: true,
|
||||
lockedToVersion: {
|
||||
select: {
|
||||
version: true,
|
||||
@@ -463,6 +464,7 @@ async function createCommonRunStructure(run: CommonRelatedRun, apiVersion: API_V
|
||||
triggerFunction: resolveTriggerFunction(run),
|
||||
batchId: run.batch?.friendlyId,
|
||||
metadata,
|
||||
region: run.workerQueue || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,12 @@ export const ApiRunListSearchParams = z.object({
|
||||
.transform((value) => {
|
||||
return value ? value.split(",") : undefined;
|
||||
}),
|
||||
"filter[region]": z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) => {
|
||||
return value ? value.split(",") : undefined;
|
||||
}),
|
||||
"filter[machine]": z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -255,6 +261,10 @@ export class ApiRunListPresenter extends BasePresenter {
|
||||
options.queues = searchParams["filter[queue]"];
|
||||
}
|
||||
|
||||
if (searchParams["filter[region]"]) {
|
||||
options.regions = searchParams["filter[region]"];
|
||||
}
|
||||
|
||||
if (searchParams["filter[machine]"]) {
|
||||
options.machines = searchParams["filter[machine]"];
|
||||
}
|
||||
@@ -308,6 +318,7 @@ export class ApiRunListPresenter extends BasePresenter {
|
||||
// Match `NextRunListPresenter`'s "STANDARD" fallback so API
|
||||
// consumers and the dashboard see the same value.
|
||||
taskKind: run.taskKind || "STANDARD",
|
||||
region: run.region ?? undefined,
|
||||
...ApiRetrieveRunPresenter.apiBooleanHelpersFromRunStatus(
|
||||
ApiRetrieveRunPresenter.apiStatusFromRunStatus(run.status, apiVersion)
|
||||
),
|
||||
|
||||
@@ -33,6 +33,7 @@ export type RunListOptions = {
|
||||
batchId?: string;
|
||||
runId?: string[];
|
||||
queues?: string[];
|
||||
regions?: string[];
|
||||
machines?: MachinePresetName[];
|
||||
errorId?: string;
|
||||
sources?: string[];
|
||||
@@ -72,6 +73,7 @@ export class NextRunListPresenter {
|
||||
batchId,
|
||||
runId,
|
||||
queues,
|
||||
regions,
|
||||
machines,
|
||||
errorId,
|
||||
sources,
|
||||
@@ -102,6 +104,7 @@ export class NextRunListPresenter {
|
||||
batchId !== undefined ||
|
||||
(runId !== undefined && runId.length > 0) ||
|
||||
(queues !== undefined && queues.length > 0) ||
|
||||
(regions !== undefined && regions.length > 0) ||
|
||||
(machines !== undefined && machines.length > 0) ||
|
||||
(errorId !== undefined && errorId !== "") ||
|
||||
typeof isTest === "boolean" ||
|
||||
@@ -188,6 +191,7 @@ export class NextRunListPresenter {
|
||||
runId,
|
||||
bulkId,
|
||||
queues,
|
||||
regions,
|
||||
machines,
|
||||
errorId,
|
||||
taskKinds: sources,
|
||||
@@ -255,6 +259,7 @@ export class NextRunListPresenter {
|
||||
name: run.queue.replace("task/", ""),
|
||||
type: run.queue.startsWith("task/") ? "task" : "custom",
|
||||
},
|
||||
region: run.workerQueue ? run.workerQueue : undefined,
|
||||
taskKind: RunAnnotations.safeParse(run.annotations).data?.taskKind ?? "STANDARD",
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getCurrentPlan } from "~/services/platform.v3.server";
|
||||
export type Region = {
|
||||
id: string;
|
||||
name: string;
|
||||
masterQueue: string;
|
||||
description?: string;
|
||||
cloudProvider?: string;
|
||||
location?: string;
|
||||
@@ -73,6 +74,7 @@ export class RegionsPresenter extends BasePresenter {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
masterQueue: true,
|
||||
description: true,
|
||||
cloudProvider: true,
|
||||
location: true,
|
||||
@@ -96,6 +98,7 @@ export class RegionsPresenter extends BasePresenter {
|
||||
const regions: Region[] = visibleRegions.map((region) => ({
|
||||
id: region.id,
|
||||
name: region.name,
|
||||
masterQueue: region.masterQueue,
|
||||
description: region.description ?? undefined,
|
||||
cloudProvider: region.cloudProvider ?? undefined,
|
||||
location: region.location ?? undefined,
|
||||
@@ -110,6 +113,7 @@ export class RegionsPresenter extends BasePresenter {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
masterQueue: true,
|
||||
description: true,
|
||||
cloudProvider: true,
|
||||
location: true,
|
||||
@@ -130,6 +134,7 @@ export class RegionsPresenter extends BasePresenter {
|
||||
regions.push({
|
||||
id: defaultWorkerGroup.id,
|
||||
name: defaultWorkerGroup.name,
|
||||
masterQueue: defaultWorkerGroup.masterQueue,
|
||||
description: defaultWorkerGroup.description ?? undefined,
|
||||
cloudProvider: defaultWorkerGroup.cloudProvider ?? undefined,
|
||||
location: defaultWorkerGroup.location ?? undefined,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { prisma } from "~/db.server";
|
||||
import { useOptionalOrganization } from "~/hooks/useOrganizations";
|
||||
import { useTypedMatchesData } from "~/hooks/useTypedMatchData";
|
||||
import { OrganizationsPresenter } from "~/presenters/OrganizationsPresenter.server";
|
||||
import { RegionsPresenter, type Region } from "~/presenters/v3/RegionsPresenter.server";
|
||||
import { getImpersonationId } from "~/services/impersonation.server";
|
||||
import { getCachedUsage, getCurrentPlan } from "~/services/platform.v3.server";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
@@ -88,7 +89,10 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
firstDayOfNextMonth.setUTCDate(1);
|
||||
firstDayOfNextMonth.setUTCHours(0, 0, 0, 0);
|
||||
|
||||
const [plan, usage, customDashboards] = await Promise.all([
|
||||
const shouldLoadRegions =
|
||||
!!projectParam && !!environment && environment.type !== "DEVELOPMENT";
|
||||
|
||||
const [plan, usage, customDashboards, regions] = await Promise.all([
|
||||
getCurrentPlan(organization.id),
|
||||
getCachedUsage(organization.id, { from: firstDayOfMonth, to: firstDayOfNextMonth }),
|
||||
prisma.metricsDashboard.findMany({
|
||||
@@ -100,6 +104,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
shouldLoadRegions
|
||||
? new RegionsPresenter()
|
||||
.call({ userId: user.id, projectSlug: projectParam! })
|
||||
.then(({ regions }) => regions)
|
||||
.catch(() => [] as Region[])
|
||||
: Promise.resolve([] as Region[]),
|
||||
]);
|
||||
|
||||
let hasExceededFreeTier = false;
|
||||
@@ -147,6 +157,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
organization,
|
||||
project,
|
||||
environment,
|
||||
regions,
|
||||
isImpersonating: !!impersonationId,
|
||||
currentPlan: { ...plan, v3Usage: { ...usage, hasExceededFreeTier, usagePercentage } },
|
||||
customDashboards: customDashboardsWithWidgetCount,
|
||||
|
||||
+2
-7
@@ -22,7 +22,7 @@ import { assertNever } from "assert-never";
|
||||
import { useEffect, useState } from "react";
|
||||
import { typedjson, useTypedFetcher } from "remix-typedjson";
|
||||
import { ExitIcon } from "~/assets/icons/ExitIcon";
|
||||
import { FlagIcon } from "~/assets/icons/RegionIcons";
|
||||
import { RegionLabel } from "~/components/runs/v3/RegionLabel";
|
||||
import { AdminDebugRun } from "~/components/admin/debugRun";
|
||||
import { CodeBlock } from "~/components/code/CodeBlock";
|
||||
import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel";
|
||||
@@ -972,12 +972,7 @@ function RunBody({
|
||||
<Property.Item>
|
||||
<Property.Label>Region</Property.Label>
|
||||
<Property.Value>
|
||||
<span className="flex items-center gap-1">
|
||||
{run.region.location ? (
|
||||
<FlagIcon region={run.region.location} className="size-5" />
|
||||
) : null}
|
||||
{run.region.name}
|
||||
</span>
|
||||
<RegionLabel region={run.region} />
|
||||
</Property.Value>
|
||||
</Property.Item>
|
||||
)}
|
||||
|
||||
@@ -151,6 +151,7 @@ export class ClickHouseRunsRepository implements IRunsRepository {
|
||||
metadataType: true,
|
||||
machinePreset: true,
|
||||
queue: true,
|
||||
workerQueue: true,
|
||||
annotations: true,
|
||||
},
|
||||
});
|
||||
@@ -324,6 +325,10 @@ function applyRunFiltersToQueryBuilder<T>(
|
||||
queryBuilder.where("queue IN {queues: Array(String)}", { queues: options.queues });
|
||||
}
|
||||
|
||||
if (options.regions && options.regions.length > 0) {
|
||||
queryBuilder.where("worker_queue IN {regions: Array(String)}", { regions: options.regions });
|
||||
}
|
||||
|
||||
if (options.machines && options.machines.length > 0) {
|
||||
queryBuilder.where("machine_preset IN {machines: Array(String)}", {
|
||||
machines: options.machines,
|
||||
|
||||
@@ -40,6 +40,7 @@ const RunListInputOptionsSchema = z.object({
|
||||
runId: z.array(z.string()).optional(),
|
||||
bulkId: z.string().optional(),
|
||||
queues: z.array(z.string()).optional(),
|
||||
regions: z.array(z.string()).optional(),
|
||||
machines: MachinePresetName.array().optional(),
|
||||
errorId: z.string().optional(),
|
||||
taskKinds: z.array(z.string()).optional(),
|
||||
@@ -104,6 +105,7 @@ export type ListedRun = Prisma.TaskRunGetPayload<{
|
||||
metadataType: true;
|
||||
machinePreset: true;
|
||||
queue: true;
|
||||
workerQueue: true;
|
||||
annotations: true;
|
||||
};
|
||||
}>;
|
||||
|
||||
@@ -427,6 +427,11 @@ function formatRunSummary(run: ListRunResponseItem): string {
|
||||
parts.push(`v${run.version}`);
|
||||
}
|
||||
|
||||
// Region if available
|
||||
if (run.region) {
|
||||
parts.push(`region:${run.region}`);
|
||||
}
|
||||
|
||||
return parts.join(" | ");
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,12 @@ export const ListRunsInput = CommonProjectsInput.extend({
|
||||
.describe("Filter for runs created in the last N time period. e.g. 7d, 30d, 365d")
|
||||
.optional(),
|
||||
machine: MachinePresetName.describe("Filter for runs that match this machine preset").optional(),
|
||||
region: z
|
||||
.string()
|
||||
.describe(
|
||||
"Filter for runs that executed in this region (the worker instance group masterQueue identifier, e.g. 'us-east-1' or 'main')"
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type ListRunsInput = z.output<typeof ListRunsInput>;
|
||||
|
||||
@@ -374,6 +374,7 @@ export const listRunsTool = {
|
||||
to: $to,
|
||||
period: input.period,
|
||||
machine: input.machine,
|
||||
region: input.region,
|
||||
});
|
||||
|
||||
const formattedRuns = formatRunList(result);
|
||||
|
||||
@@ -2080,6 +2080,13 @@ function createSearchQueryForListRuns(query?: ListRunsQueryParams): URLSearchPar
|
||||
Array.isArray(query.machine) ? query.machine.join(",") : query.machine
|
||||
);
|
||||
}
|
||||
|
||||
if (query.region) {
|
||||
searchParams.append(
|
||||
"filter[region]",
|
||||
Array.isArray(query.region) ? query.region.join(",") : query.region
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return searchParams;
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface ListRunsQueryParams extends CursorPageParams {
|
||||
queue?: Array<QueueTypeName> | QueueTypeName;
|
||||
/** The machine name, or multiple of them. */
|
||||
machine?: Array<MachinePresetName> | MachinePresetName;
|
||||
/** The region master-queue identifier, or multiple of them. */
|
||||
region?: Array<string> | string;
|
||||
}
|
||||
|
||||
export interface ListProjectRunsQueryParams extends CursorPageParams, ListRunsQueryParams {
|
||||
|
||||
@@ -1134,6 +1134,7 @@ const CommonRunFields = {
|
||||
durationMs: z.number(),
|
||||
metadata: z.record(z.any()).optional(),
|
||||
taskKind: z.string().optional(),
|
||||
region: z.string().optional(),
|
||||
};
|
||||
|
||||
const RetrieveRunCommandFields = {
|
||||
|
||||
Reference in New Issue
Block a user