import { type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDurationMilliseconds } from "@trigger.dev/core/v3"; import { Suspense, useMemo } from "react"; import { TypedAwait, typeddefer, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { BeakerIcon } from "~/assets/icons/BeakerIcon"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { MachineLabelCombo } from "~/components/MachineLabelCombo"; import { PageBody, PageContainer } from "~/components/layout/AppLayout"; import { DirectionSchema, ListPagination } from "~/components/ListPagination"; import { LinkButton } from "~/components/primitives/Buttons"; import { ChartCard } from "~/components/primitives/charts/ChartCard"; import { ChartSyncProvider } from "~/components/primitives/charts/ChartSyncContext"; import { useZoomToTimeFilter } from "~/hooks/useZoomToTimeFilter"; import { Chart, type ChartConfig } from "~/components/primitives/charts/ChartCompound"; import { buildActivityTimeAxis } from "~/components/primitives/charts/activityTimeAxis"; import { statusColor } from "~/components/primitives/charts/statusColors"; import { CopyableText } from "~/components/primitives/CopyableText"; import { DateTime } from "~/components/primitives/DateTime"; import { Header2 } from "~/components/primitives/Headers"; import { NavBar, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "~/components/primitives/Resizable"; import { Spinner } from "~/components/primitives/Spinner"; import { TextLink } from "~/components/primitives/TextLink"; import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable"; import { TimeFilter, timeFilterFromTo } from "~/components/runs/v3/SharedFilters"; import { $replica } from "~/db.server"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server"; import { TaskDetailPresenter, type TaskActivity, type TaskDetail, } from "~/presenters/v3/TaskDetailPresenter.server"; import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server"; import { requireUser } from "~/services/session.server"; import { EnvironmentParamSchema, v3EnvironmentPath, v3QueuesPath, v3TestTaskPath, } from "~/utils/pathBuilder"; import { parseFiniteInt } from "~/utils/searchParams"; export const meta: MetaFunction = ({ data }) => { const slug = (data as { task?: TaskDetail | null } | undefined)?.task?.slug; return [{ title: slug ? `${slug} | Tasks | Trigger.dev` : "Task | Trigger.dev" }]; }; const ParamsSchema = EnvironmentParamSchema.extend({ taskParam: z.string(), }); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const user = await requireUser(request); const userId = user.id; const { organizationSlug, projectParam, envParam, taskParam } = ParamsSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) throw new Response("Project not found", { status: 404 }); const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) throw new Response("Environment not found", { status: 404 }); const url = new URL(request.url); const period = url.searchParams.get("period") ?? undefined; const from = parseFiniteInt(url.searchParams.get("from")); const to = parseFiniteInt(url.searchParams.get("to")); const cursor = url.searchParams.get("cursor") ?? undefined; const directionRaw = url.searchParams.get("direction") ?? undefined; const direction = directionRaw ? DirectionSchema.parse(directionRaw) : undefined; const versions = url.searchParams.getAll("versions").filter((v) => v.length > 0); const clickhouse = await clickhouseFactory.getClickhouseForOrganization( project.organizationId, "standard" ); const presenter = new TaskDetailPresenter($replica, clickhouse); const task = await presenter.findTask({ environmentId: environment.id, environmentType: environment.type, taskSlug: taskParam, expectedTriggerSource: "STANDARD", }); if (!task) throw new Response("Task not found", { status: 404 }); const time = timeFilterFromTo({ period, from, to, defaultPeriod: "7d" }); const activity = presenter .getActivity({ organizationId: project.organizationId, projectId: project.id, environmentId: environment.id, taskSlug: task.slug, from: time.from, to: time.to, }) .catch(() => ({ data: [], statuses: [] }) satisfies TaskActivity); const runList = new NextRunListPresenter($replica, clickhouse) .call(project.organizationId, environment.id, { userId, projectId: project.id, tasks: [task.slug], versions: versions.length > 0 ? versions : undefined, period, from, to, cursor, direction, }) .catch(() => null); return typeddefer({ task, activity, runList, }); }; export default function Page() { const { task, activity, runList } = useTypedLoaderData(); const zoomToTimeFilter = useZoomToTimeFilter(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const tasksListingPath = v3EnvironmentPath(organization, project, environment); const testPath = v3TestTaskPath(organization, project, environment, { taskIdentifier: task.slug, }); const queuesPath = v3QueuesPath(organization, project, environment); return ( {task.slug} } />
{/* Top bar — title on the left; TimeFilter + pagination on the right. h-10 matches the right-hand sidebar header height. */}
Runs
{(list) => (list ? : null)}
{/* Activity chart */}
}> }> {(result) => }
{/* Runs table */}
}> }> {(list) => list ? (
) : ( ) }
); } function TaskDetailSidebar({ task, testPath, queuesPath, }: { task: TaskDetail; testPath: string; queuesPath: string; }) { const showExportName = task.exportName && task.exportName !== task.slug; const retrySummary = formatRetrySummary(task.retry); return (
{task.slug} Test task
Identifier File path {showExportName ? ( Export name ) : null} {task.description ? ( Description {task.description} ) : null} Type Standard task {task.workerVersion ? ( Version {task.workerVersion} ) : null} {task.queue ? ( Queue
{task.queue.name} Concurrency: {task.queue.concurrencyLimit ?? "Unlimited"} {task.queue.paused ? " · Paused" : ""}
) : null} Machine Max duration {task.maxDurationInSeconds ? `${task.maxDurationInSeconds}s (${formatDurationMilliseconds( task.maxDurationInSeconds * 1000, { style: "short" } )})` : "–"} TTL {task.ttl ?? "–"} Retry {retrySummary} Payload schema {task.hasPayloadSchema ? "Yes" : "–"} Created
); } function formatRetrySummary(retry: TaskDetail["retry"]): string { if (!retry || retry.maxAttempts === undefined) return "–"; if (retry.maxAttempts <= 1) return "Disabled"; return `${retry.maxAttempts} attempts`; } function ActivityChart({ activity }: { activity: TaskActivity }) { const chartConfig: ChartConfig = useMemo(() => { const cfg: ChartConfig = {}; for (const status of activity.statuses) { cfg[status] = { label: status.charAt(0) + status.slice(1).toLowerCase(), color: statusColor(status), }; } return cfg; }, [activity.statuses]); const { tickFormatter, tooltipLabelFormatter } = useMemo( () => buildActivityTimeAxis(activity.data), [activity.data] ); return ( ); } function ActivityChartSkeleton() { return (
{Array.from({ length: 42 }).map((_, i) => (
))}
); } function TableLoading() { return (
); }