import { BookOpenIcon, PlusIcon } from "@heroicons/react/20/solid"; import { NoSymbolIcon } from "@heroicons/react/24/solid"; import { Outlet, useParams, type MetaFunction } from "@remix-run/react"; import { type LoaderFunctionArgs } from "@remix-run/server-runtime"; import { tryCatch } from "@trigger.dev/core"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { AdminDebugTooltip } from "~/components/admin/debugTooltip"; import { BulkActionsNone } from "~/components/BlankStatePanels"; import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { DateTime } from "~/components/primitives/DateTime"; import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; import { PaginationControls } from "~/components/primitives/Pagination"; import { Paragraph } from "~/components/primitives/Paragraph"; import { RESIZABLE_PANEL_ANIMATION, ResizableHandle, ResizablePanel, ResizablePanelGroup, collapsibleHandleClassName, } from "~/components/primitives/Resizable"; import { Table, TableBlankRow, TableBody, TableCell, TableCellMenu, TableHeader, TableHeaderCell, TableRow, } from "~/components/primitives/Table"; import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue"; import { AbortBulkActionDialog } from "~/components/runs/v3/AbortBulkActionDialog"; import { BulkActionStatusCombo, BulkActionTypeCombo } from "~/components/runs/v3/BulkAction"; import { UserAvatar } from "~/components/UserProfilePhoto"; 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 { type BulkActionListItem, BulkActionListPresenter, } from "~/presenters/v3/BulkActionListPresenter.server"; import { rbac } from "~/services/rbac.server"; import { checkPermissions } from "~/services/routeBuilders/permissions.server"; import { requireUserId } from "~/services/session.server"; import { cn } from "~/utils/cn"; import { docsPath, EnvironmentParamSchema, v3BulkActionPath, v3CreateBulkActionPath, } from "~/utils/pathBuilder"; export const meta: MetaFunction = () => { return [ { title: `Bulk actions | Trigger.dev`, }, ]; }; const SearchParamsSchema = z.object({ page: z.coerce.number().optional(), }); export const loader = async ({ request, params }: LoaderFunctionArgs) => { const userId = await requireUserId(request); const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params); const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { throw new Response("Not Found", { status: 404 }); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { throw new Response("Not Found", { status: 404 }); } try { const url = new URL(request.url); const { page } = SearchParamsSchema.parse(Object.fromEntries(url.searchParams)); const presenter = new BulkActionListPresenter(); const [error, data] = await tryCatch( presenter.call({ environmentId: environment.id, page, }) ); if (error) { throw new Error(error.message); } // Display flag for the row-menu Abort control. The abort action route // enforces write:runs independently. Permissive in OSS. const bulkAuth = await rbac.authenticateSession(request, { userId, organizationId: project.organizationId, }); const { canAbort } = bulkAuth.ok ? checkPermissions(bulkAuth.ability, { canAbort: { action: "write", resource: { type: "runs" } }, }) : { canAbort: true }; return typedjson({ ...data, canAbort }); } catch (error) { console.error(error); throw new Response(undefined, { status: 400, statusText: "Something went wrong, if this problem persists please contact support.", }); } }; export default function Page() { const { bulkActions, currentPage, totalPages, totalCount: _totalCount, canAbort, } = useTypedLoaderData(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const { bulkActionParam } = useParams(); const isShowingInspector = bulkActionParam !== undefined; return ( Bulk actions docs New bulk action {bulkActions.length === 0 ? ( ) : (
1 ? "grid-rows-[auto_1fr_auto]" : "grid-rows-[1fr]" )} > {totalPages > 1 && (
)} {totalPages > 1 && (
1 && "justify-end border-t border-grid-dimmed px-2 py-3" )} >
)}
{}} collapsedSize="0px" collapseAnimation={RESIZABLE_PANEL_ANIMATION} >
)}
); } function BulkActionsTable({ bulkActions, totalPages, canAbort, }: { bulkActions: BulkActionListItem[]; totalPages: number; canAbort: boolean; }) { const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); const { bulkActionParam } = useParams(); return ( ID Name
The bulk action is currently in progress. They can take some time if there are lots of runs.
The bulk action has completed successfully.
The bulk action was aborted.
} > Status
Bulk action Runs User Created Completed Actions
{bulkActions.length === 0 ? ( There are no matching bulk actions ) : ( bulkActions.map((bulkAction) => { const path = v3BulkActionPath(organization, project, environment, bulkAction); const isSelected = bulkActionParam === bulkAction.friendlyId; return ( {bulkAction.name || "–"} {bulkAction.totalCount} {bulkAction.user ? (
{bulkAction.user.name}
) : ( "–" )}
{bulkAction.completedAt ? : "–"}
); }) )}
); } function BulkActionActionsCell({ bulkAction, path, canAbort, }: { bulkAction: BulkActionListItem; path: string; canAbort: boolean; }) { // Abort is the only action, and only while the bulk action is still running. if (bulkAction.status !== "PENDING") { return {""}; } return ( ) : ( ) } /> ); }