import { ExclamationCircleIcon } from "@heroicons/react/20/solid"; import { BookOpenIcon } from "@heroicons/react/24/solid"; import { type MetaFunction, Outlet, useLocation, useNavigation, useParams } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { formatDuration } from "@trigger.dev/core/v3/utils/durations"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { BatchesNone } from "~/components/BlankStatePanels"; import { ListPagination } from "~/components/ListPagination"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { Paragraph } from "~/components/primitives/Paragraph"; import { collapsibleHandleClassName, RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "~/components/primitives/Resizable"; import { Spinner } from "~/components/primitives/Spinner"; import { Table, TableBlankRow, TableBody, TableCell, TableCellMenu, TableHeader, TableHeaderCell, TableRow, CopyableTableCell, } from "~/components/primitives/Table"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { BatchFilters, BatchListFilters } from "~/components/runs/v3/BatchFilters"; import { allBatchStatuses, BatchStatusCombo, descriptionForBatchStatus, } from "~/components/runs/v3/BatchStatus"; import { LiveTimer } from "~/components/runs/v3/LiveTimer"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithErrorMessage } from "~/models/message.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { type BatchList, BatchListPresenter } from "~/presenters/v3/BatchListPresenter.server"; import { requireUserId } from "~/services/session.server"; import { $replica, runOpsNewReplicaClient, runOpsLegacyReplica, runOpsSplitReadEnabled, type PrismaClientOrTransaction, } from "~/db.server"; import { docsPath, EnvironmentParamSchema, v3BatchPath, v3BatchRunsPath, } from "~/utils/pathBuilder"; import { throwNotFound } from "~/utils/httpErrors"; export const meta: MetaFunction = () => { return [ { title: `Batches | Trigger.dev`, }, ]; }; export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { return redirectWithErrorMessage("/", request, "Project not found"); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throwNotFound("Environment not found"); } const url = new URL(request.url); const s = { cursor: url.searchParams.get("cursor") ?? undefined, direction: url.searchParams.get("direction") ?? undefined, statuses: url.searchParams.getAll("statuses"), period: url.searchParams.get("period") ?? undefined, from: url.searchParams.get("from") ?? undefined, to: url.searchParams.get("to") ?? undefined, id: url.searchParams.get("id") ?? undefined, }; const filters = BatchListFilters.parse(s); const presenter = new BatchListPresenter(undefined, undefined, { runOpsNew: runOpsNewReplicaClient as unknown as PrismaClientOrTransaction, runOpsLegacyReplica: runOpsLegacyReplica as unknown as PrismaClientOrTransaction, controlPlaneReplica: $replica as unknown as PrismaClientOrTransaction, splitEnabled: runOpsSplitReadEnabled, }); const list = await presenter.call({ userId, projectId: project.id, ...filters, friendlyId: filters.id, environmentId: environment.id, }); return typedjson(list); }; export default function Page() { const { batches, hasFilters, hasAnyBatches, filters, pagination } = useTypedLoaderData(); const { batchParam } = useParams(); const isShowingInspector = batchParam !== undefined; return ( Batches docs {!hasAnyBatches ? ( ) : (
{}} collapsedSize="0px" collapseAnimation={RESIZABLE_PANEL_ANIMATION} >
)}
); } function BatchesTable({ batches, hasFilters, filters }: BatchList) { const navigation = useNavigation(); const location = useLocation(); const isLoading = navigation.state !== "idle" && navigation.location?.pathname === location.pathname; const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const { batchParam } = useParams(); return ( ID {allBatchStatuses.map((status) => (
{descriptionForBatchStatus(status)}
))} } > Status
Runs Duration Created Finished Go to batch
{batches.length === 0 ? (
No batches match these filters
) : ( batches.map((batch) => { const basePath = v3BatchPath(organization, project, environment, batch); const inspectorPath = `${basePath}${location.search}`; const runsPath = v3BatchRunsPath(organization, project, environment, batch); const isSelected = batchParam === batch.friendlyId; return ( {batch.friendlyId} {batch.batchVersion === "v1" ? ( Legacy batch } /> ) : ( } /> )} {batch.runCount} {batch.finishedAt ? ( formatDuration(new Date(batch.createdAt), new Date(batch.finishedAt), { style: "short", }) ) : ( )} {batch.finishedAt ? : "–"} ); }) )} {isLoading && ( Loading… )}
); } function BatchActionsCell({ runsPath }: { runsPath: string }) { return ( View runs } /> ); }