import { ArrowPathIcon, ArrowRightIcon, ClockIcon, CpuChipIcon, NoSymbolIcon, RectangleStackIcon, } from "@heroicons/react/20/solid"; import { BookOpenIcon, CheckIcon } from "@heroicons/react/24/solid"; import { useLocation } from "@remix-run/react"; import { formatDuration, formatDurationMilliseconds } from "@trigger.dev/core/v3"; import { useCallback, useRef } from "react"; import { TasksIcon } from "~/assets/icons/TasksIcon"; import { MachineLabelCombo } from "~/components/MachineLabelCombo"; import { MachineTooltipInfo } from "~/components/MachineTooltipInfo"; import { Badge } from "~/components/primitives/Badge"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { Checkbox } from "~/components/primitives/Checkbox"; import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; import { Header3 } from "~/components/primitives/Headers"; import { PopoverMenuItem } from "~/components/primitives/Popover"; 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"; import { type NextRunListAppliedFilters, type NextRunListItem, } from "~/presenters/v3/NextRunListPresenter.server"; import { formatCurrencyAccurate } from "~/utils/numberFormatter"; import { docsPath, v3RunSpanPath, v3TestPath, v3TestTaskPath } from "~/utils/pathBuilder"; import { DateTime } from "../../primitives/DateTime"; import { Paragraph } from "../../primitives/Paragraph"; import { Spinner } from "../../primitives/Spinner"; import { Table, TableBlankRow, TableBody, TableCell, TableCellMenu, TableHeader, TableHeaderCell, TableRow, 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"; import { descriptionForTaskRunStatus, filterableTaskRunStatuses, TaskRunStatusCombo, } from "./TaskRunStatus"; import { RunStatusCellTooltip } from "./RunStatusCellTooltip"; import { TaskTriggerSourceIcon } from "./TaskTriggerSource"; import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; import { useSearchParams } from "~/hooks/useSearchParam"; import type { TaskTriggerSource } from "@trigger.dev/database"; import { BeakerIcon } from "~/assets/icons/BeakerIcon"; type RunsTableProps = { total: number; hasFilters: boolean; filters: NextRunListAppliedFilters; showJob?: boolean; runs: NextRunListItem[]; rootOnlyDefault?: boolean; isLoading?: boolean; allowSelection?: boolean; variant?: TableVariant; disableAdjacentRows?: boolean; additionalTableState?: Record; showTopBorder?: boolean; stickyHeader?: boolean; childrenStatusesBasePath?: string; /** * Display-only write:runs flags from the caller's loader. Default true so * callers that don't pass them (and OSS, where the ability is permissive) * keep the controls enabled. The cancel/replay action routes enforce * write:runs regardless. */ canCancelRuns?: boolean; canReplayRuns?: boolean; }; export function TaskRunsTable({ total, hasFilters, filters, runs, rootOnlyDefault, disableAdjacentRows = false, isLoading = false, allowSelection = false, variant = "dimmed", additionalTableState, showTopBorder = true, stickyHeader = false, childrenStatusesBasePath, canCancelRuns = true, canReplayRuns = true, }: 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(); const { value } = useSearchParams(); const location = useOptimisticLocation(); const params = new URLSearchParams(location.search || ""); if (!value("rootOnly")) { params.set("rootOnly", String(rootOnlyDefault)); } if (additionalTableState) { for (const [key, val] of Object.entries(additionalTableState)) { params.set(key, val); } } const search = params.toString(); /** TableState has to be encoded as a separate URI component, so it's merged under one, 'tableState' param */ const tableStateParam = disableAdjacentRows ? "" : encodeURIComponent(search); const showCompute = isManagedCloud; const showRegion = environment.type !== "DEVELOPMENT"; const navigateCheckboxes = useCallback( (event: React.KeyboardEvent, index: number) => { //indexes are out by one because of the header row if (event.key === "ArrowUp" && index > 0) { checkboxes.current[index - 1]?.focus(); if (event.shiftKey) { const oldItem = runs.at(index - 1); const newItem = runs.at(index - 2); const itemsIds = [oldItem?.friendlyId, newItem?.friendlyId].filter(Boolean); select(itemsIds); } } else if (event.key === "ArrowDown" && index < checkboxes.current.length - 1) { checkboxes.current[index + 1]?.focus(); if (event.shiftKey) { const oldItem = runs.at(index - 1); const newItem = runs.at(index); const itemsIds = [oldItem?.friendlyId, newItem?.friendlyId].filter(Boolean); select(itemsIds); } } }, [checkboxes, runs] ); return ( {allowSelection && ( {runs.length > 0 && ( r.friendlyId))} onChange={(element) => { const ids = runs.map((r) => r.friendlyId); const checked = element.currentTarget.checked; if (checked) { select(ids); } else { deselect(ids); } }} ref={(r) => { checkboxes.current[0] = r; }} onKeyDown={(event) => navigateCheckboxes(event, 0)} /> )} )} ID Task Version {filterableTaskRunStatuses.map((status) => (
{descriptionForTaskRunStatus(status)}
))} } > Status
Started
Queued duration
The amount of time from when the run was created to it starting to run.
Run duration
The total amount of time from the run starting to it finishing. This includes all time spent waiting.
Compute duration
The amount of compute time used in the run. This does not include time spent waiting.
} > Duration
{showCompute && ( <> Compute )} }> Machine Queue {showRegion && Region} Test Created at When you want to trigger a task now, but have it run at a later time, you can use the delay option. Runs that are delayed and have not been enqueued yet will display in the dashboard with a “Delayed” status. Read docs } > Delayed until You can set a TTL (time to live) when triggering a task, which will automatically expire the run if it hasn’t started within the specified time. All runs in development have a default ttl of 10 minutes. You can disable this by setting the ttl option. Read docs } > TTL You can add tags to a run and then filter runs using them. You can add tags when triggering a run or inside the run function. Read docs } > Tags Go to page
{total === 0 && !hasFilters ? ( {!isLoading && } ) : runs.length === 0 ? ( ) : ( runs.map((run, index) => { const searchParams = new URLSearchParams(); if (tableStateParam) { searchParams.set("tableState", tableStateParam); } const path = v3RunSpanPath( organization, project, run.environment, run, { spanId: run.spanId, }, searchParams ); return ( {allowSelection && ( { toggle(run.friendlyId); }} ref={(r) => { checkboxes.current[index + 1] = r; }} onKeyDown={(event) => navigateCheckboxes(event, index + 1)} /> )} {run.taskIdentifier} {run.rootTaskRunId === null ? Root : null} {run.version ?? "–"} {run.rootTaskRunId === null && childrenStatusesBasePath ? ( ) : ( } /> )} {run.startedAt ? : "–"}
{run.isPending ? ( "–" ) : run.startedAt ? ( formatDuration(new Date(run.createdAt), new Date(run.startedAt), { style: "short", }) ) : run.isCancellable ? ( ) : ( formatDuration(new Date(run.createdAt), new Date(run.updatedAt), { style: "short", }) )}
{run.startedAt && run.finishedAt ? ( formatDuration(new Date(run.startedAt), new Date(run.finishedAt), { style: "short", }) ) : run.startedAt ? ( ) : ( "–" )}
{run.usageDurationMs > 0 ? formatDurationMilliseconds(run.usageDurationMs, { style: "short", }) : "–"}
{showCompute && ( {run.costInCents > 0 ? formatCurrencyAccurate((run.costInCents + run.baseCostInCents) / 100) : "–"} )} {run.queue.type === "task" ? ( {run.queue.name} } content={`This queue was automatically created from your "${run.queue.name}" task`} disableHoverableContent /> ) : ( {run.queue.name} } content={`This is a custom queue you added in your code.`} disableHoverableContent /> )} {showRegion && ( {run.region ? ( ) : ( "–" )} )} {run.isTest ? ( ) : ( "–" )} {run.createdAt ? : "–"} {run.delayUntil ? : "–"} {run.ttl ?? "–"}
{run.tags.map((tag) => ) || "–"}
); }) )} {isLoading && ( Loading… )}
); } function RunActionsCell({ run, path, canCancelRuns, canReplayRuns, }: { run: NextRunListItem; path: string; canCancelRuns: boolean; canReplayRuns: boolean; }) { const location = useLocation(); if (!run.isCancellable && !run.isReplayable) return {""}; return ( {run.isCancellable && (canCancelRuns ? ( ) : ( ))} {run.isReplayable && (canReplayRuns ? ( ) : ( ))} } hiddenButtons={ <> {run.isCancellable && canCancelRuns && ( } content="Cancel run" side="left" disableHoverableContent /> )} {run.isCancellable && canCancelRuns && run.isReplayable && canReplayRuns && (
)} {run.isReplayable && canReplayRuns && ( } content="Replay run…" side="left" disableHoverableContent /> )} } /> ); } function NoRuns({ title }: { title: string }) { return (
{title}
); } function BlankState({ isLoading, filters, showRegion, }: Pick & { showRegion: boolean }) { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const colSpan = showRegion ? 16 : 15; if (isLoading) return ; const { tasks, from, to, ...otherFilters } = filters; const singleTaskFromFilters = filters.tasks.length === 1 ? filters.tasks[0] : null; const testPath = singleTaskFromFilters ? v3TestTaskPath(organization, project, environment, { taskIdentifier: singleTaskFromFilters }) : v3TestPath(organization, project, environment); if ( filters.tasks.length === 1 && filters.from === undefined && filters.to === undefined && Object.values(otherFilters).every((filterArray) => filterArray.length === 0) ) { return ( There are no runs for {filters.tasks[0]} ); } return (
No runs match your filters. Try refreshing, modifying your filters or run a test.
or Run a test
); }