import { ArrowPathIcon } from "@heroicons/react/20/solid"; import { NoSymbolIcon } from "@heroicons/react/24/solid"; import { tryCatch } from "@trigger.dev/core"; import type { BulkActionType } from "@trigger.dev/database"; import { motion } from "framer-motion"; import { useState } from "react"; import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { z } from "zod"; import { ExitIcon } from "~/assets/icons/ExitIcon"; import { RunsIcon } from "~/assets/icons/RunsIcon"; import { BulkActionFilterSummary } from "~/components/BulkActionFilterSummary"; import { Button, LinkButton } from "~/components/primitives/Buttons"; import { CopyableText } from "~/components/primitives/CopyableText"; import { DateTime } from "~/components/primitives/DateTime"; import { Dialog, DialogTrigger } from "~/components/primitives/Dialog"; import { Header2 } from "~/components/primitives/Headers"; import { Paragraph } from "~/components/primitives/Paragraph"; import * as Property from "~/components/primitives/PropertyTable"; import { AbortBulkActionDialog } from "~/components/runs/v3/AbortBulkActionDialog"; import { BulkActionStatusCombo, BulkActionTypeCombo } from "~/components/runs/v3/BulkAction"; import { UserAvatar } from "~/components/UserProfilePhoto"; import { env } from "~/env.server"; import { useAutoRevalidate } from "~/hooks/useAutoRevalidate"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server"; import { resolveOrgIdFromSlug } from "~/models/organization.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { BulkActionPresenter } from "~/presenters/v3/BulkActionPresenter.server"; import { logger } from "~/services/logger.server"; import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder"; import { checkPermissions } from "~/services/routeBuilders/permissions.server"; import { cn } from "~/utils/cn"; import { formatNumber } from "~/utils/numberFormatter"; import { EnvironmentParamSchema, v3BulkActionPath, v3BulkActionsPath, v3CreateBulkActionPath, v3RunsPath, } from "~/utils/pathBuilder"; import { BulkActionService } from "~/v3/services/bulk/BulkActionV2.server"; const BulkActionParamSchema = EnvironmentParamSchema.extend({ bulkActionParam: z.string(), }); export const loader = dashboardLoader( { params: BulkActionParamSchema, context: async (params) => { const organizationId = await resolveOrgIdFromSlug(params.organizationSlug); return organizationId ? { organizationId } : {}; }, authorization: { action: "read", resource: { type: "runs" } }, }, async ({ params, user, ability }) => { const { organizationSlug, projectParam, envParam, bulkActionParam } = params; const project = await findProjectBySlug(organizationSlug, projectParam, user.id); if (!project) { throw new Response("Not Found", { status: 404 }); } const environment = await findEnvironmentBySlug(project.id, envParam, user.id); if (!environment) { throw new Response("Not Found", { status: 404 }); } try { const presenter = new BulkActionPresenter(); const [error, data] = await tryCatch( presenter.call({ environmentId: environment.id, bulkActionId: bulkActionParam, }) ); if (error) { throw new Error(error.message); } const autoReloadPollIntervalMs = env.BULK_ACTION_AUTORELOAD_POLL_INTERVAL_MS; // Display flag for the Abort button — the action enforces write:runs. const { canAbort } = checkPermissions(ability, { canAbort: { action: "write", resource: { type: "runs" } }, }); return typedjson({ bulkAction: data, autoReloadPollIntervalMs, canAbort }); } catch (error) { console.error(error); throw new Response(undefined, { status: 400, statusText: "Something went wrong, if this problem persists please contact support.", }); } } ); export const action = dashboardAction( { params: BulkActionParamSchema, context: async (params) => { const organizationId = await resolveOrgIdFromSlug(params.organizationSlug); return organizationId ? { organizationId } : {}; }, authorization: { action: "write", resource: { type: "runs" } }, }, async ({ request, params, user }) => { const { organizationSlug, projectParam, envParam, bulkActionParam } = params; const project = await findProjectBySlug(organizationSlug, projectParam, user.id); if (!project) { throw new Response("Not Found", { status: 404 }); } const environment = await findEnvironmentBySlug(project.id, envParam, user.id); if (!environment) { throw new Response("Not Found", { status: 404 }); } const service = new BulkActionService(); const [error, _result] = await tryCatch(service.abort(bulkActionParam, environment.id)); if (error) { logger.error("Failed to abort bulk action", { error, }); return redirectWithErrorMessage( v3BulkActionPath( { slug: organizationSlug }, { slug: projectParam }, { slug: envParam }, { friendlyId: bulkActionParam } ), request, `Failed to abort bulk action: ${error.message}` ); } return redirectWithSuccessMessage( v3BulkActionPath( { slug: organizationSlug }, { slug: projectParam }, { slug: envParam }, { friendlyId: bulkActionParam } ), request, "Bulk action aborted" ); } ); export default function Page() { const { bulkAction, autoReloadPollIntervalMs, canAbort } = useTypedLoaderData(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); useAutoRevalidate({ interval: autoReloadPollIntervalMs, onFocus: true, disabled: bulkAction.status !== "PENDING", }); return (
{bulkAction.name || bulkAction.friendlyId}
{bulkAction.status === "PENDING" ? ( ) : null}
ID Bulk action User {bulkAction.user ? (
{bulkAction.user.name}
) : ( "–" )}
Created Completed {bulkAction.completedAt ? : "–"} Summary
Replay runs View runs
); } type MeterProps = { type: BulkActionType; successCount: number; failureCount: number; totalCount: number; }; function Meter({ type, successCount, failureCount, totalCount }: MeterProps) { const successPercentage = totalCount === 0 ? 0 : (successCount / totalCount) * 100; const failurePercentage = totalCount === 0 ? 0 : (failureCount / totalCount) * 100; return (
Runs {formatNumber(successCount + failureCount)}/{formatNumber(totalCount)}
{formatNumber(successCount)} {typeText(type)} successfully
{formatNumber(failureCount)} {typeText(type)} failed{" "} {type === "CANCEL" ? " (already finished)" : ""}
); } function typeText(type: BulkActionType) { switch (type) { case "CANCEL": return "canceled"; case "REPLAY": return "replayed"; } } function ControlledAbortBulkActionDialog({ canAbort, formAction, }: { canAbort: boolean; formAction: string; }) { const [open, setOpen] = useState(false); return ( setOpen(false)} /> ); }