import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react"; import { parseWithZod } from "@conform-to/zod"; import { RectangleStackIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useActionData, useNavigation, useParams, useSubmit } from "@remix-run/react"; import { MachinePresetName } from "@trigger.dev/core/v3"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson"; import { TaskIcon } from "~/assets/icons/TaskIcon"; import { JSONEditor } from "~/components/code/JSONEditor"; import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Badge } from "~/components/primitives/Badge"; import { Button } from "~/components/primitives/Buttons"; import { DialogContent, DialogHeader } from "~/components/primitives/Dialog"; import { DurationPicker } from "~/components/primitives/DurationPicker"; import { Fieldset } from "~/components/primitives/Fieldset"; import { FormError } from "~/components/primitives/FormError"; import { Hint } from "~/components/primitives/Hint"; import { Input } from "~/components/primitives/Input"; import { InputGroup } from "~/components/primitives/InputGroup"; import { Label } from "~/components/primitives/Label"; import { Paragraph } from "~/components/primitives/Paragraph"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "~/components/primitives/Resizable"; import { Select, SelectItem } from "~/components/primitives/Select"; import { Spinner, SpinnerWhite } from "~/components/primitives/Spinner"; import { TabButton, TabContainer } from "~/components/primitives/Tabs"; import { TextLink } from "~/components/primitives/TextLink"; import { InfoIconTooltip } from "~/components/primitives/Tooltip"; import { type loader as queuesLoader } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues"; import { type loader } from "~/routes/resources.taskruns.$runParam.replay"; import { docsPath } from "~/utils/pathBuilder"; import { ReplayRunData } from "~/v3/replayTask"; import { RunTagInput } from "./RunTagInput"; type ReplayRunDialogProps = { runFriendlyId: string; failedRedirect: string; }; export function ReplayRunDialog({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) { return ( ); } function ReplayContent({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) { const replayDataFetcher = useTypedFetcher(); const isLoading = replayDataFetcher.state === "loading"; const queueFetcher = useTypedFetcher(); const [environmentIdOverride, setEnvironmentIdOverride] = useState(undefined); useEffect(() => { const searchParams = new URLSearchParams(); if (environmentIdOverride) { searchParams.set("environmentIdOverride", environmentIdOverride); } replayDataFetcher.load( `/resources/taskruns/${runFriendlyId}/replay?${searchParams.toString()}` ); }, [runFriendlyId, environmentIdOverride]); const params = useParams(); useEffect(() => { if (params.organizationSlug && params.projectParam && params.envParam) { const searchParams = new URLSearchParams(); searchParams.set("type", "custom"); searchParams.set("per_page", "100"); let envSlug = params.envParam; if (environmentIdOverride) { const environmentOverride = replayDataFetcher.data?.environments.find( (env) => env.id === environmentIdOverride ); envSlug = environmentOverride?.slug ?? envSlug; } queueFetcher.load( `/resources/orgs/${params.organizationSlug}/projects/${ params.projectParam }/env/${envSlug}/queues?${searchParams.toString()}` ); } }, [params.organizationSlug, params.projectParam, params.envParam, environmentIdOverride]); const customQueues = useMemo(() => { return queueFetcher.data?.queues ?? []; }, [queueFetcher.data?.queues]); return (
Replay this run {isLoading && !replayDataFetcher.data ? (
) : replayDataFetcher.data ? ( ) : ( <>Failed to get run data )}
); } const startingJson = "{\n\n}"; const machinePresets = Object.values(MachinePresetName.enum); function ReplayForm({ failedRedirect, runFriendlyId, replayData, customQueues, environmentIdOverride, setEnvironmentIdOverride, }: { failedRedirect: string; runFriendlyId: string; replayData: UseDataFunctionReturn; customQueues: UseDataFunctionReturn["queues"]; environmentIdOverride: string | undefined; setEnvironmentIdOverride: (environment: string) => void; }) { const navigation = useNavigation(); const submit = useSubmit(); const [defaultPayloadJson, setDefaultPayloadJson] = useState( replayData.payload ?? startingJson ); const setPayload = useCallback((code: string) => { setDefaultPayloadJson(code); }, []); const currentPayloadJson = useRef(replayData.payload ?? startingJson); const [defaultMetadataJson, setDefaultMetadataJson] = useState( replayData.metadata ?? startingJson ); const setMetadata = useCallback((code: string) => { setDefaultMetadataJson(code); }, []); const currentMetadataJson = useRef(replayData.metadata ?? startingJson); const formAction = `/resources/taskruns/${runFriendlyId}/replay`; const isSubmitting = navigation.formAction === formAction; const editablePayload = replayData.payloadType === "application/json" || replayData.payloadType === "application/super+json"; const [tab, setTab] = useState<"payload" | "metadata">(editablePayload ? "payload" : "metadata"); const { defaultTaskQueue } = replayData; const queues = defaultTaskQueue && !customQueues.some((q) => q.id === defaultTaskQueue.id) ? [defaultTaskQueue, ...customQueues] : customQueues; const queueItems = queues.map((q) => ({ value: q.type === "task" ? `task/${q.name}` : q.name, label: q.name, type: q.type, paused: q.paused, })); const lastSubmission = useActionData(); const [form, fields] = useForm({ id: "replay-task", lastResult: lastSubmission as any, onSubmit(event, { formData }) { event.preventDefault(); if (editablePayload) { formData.set(fields.payload.name, currentPayloadJson.current); } formData.set(fields.metadata.name, currentMetadataJson.current); submit(formData, { method: "POST", action: formAction }); }, onValidate({ formData }) { return parseWithZod(formData, { schema: ReplayRunData }); }, }); const { environment, payload: _payload, metadata: _metadata, delaySeconds, ttlSeconds, idempotencyKey, idempotencyKeyTTLSeconds, queue, concurrencyKey, maxAttempts, maxDurationSeconds, tags, version, machine, region, prioritySeconds, } = fields; return (
Replaying will create a new run in the selected environment. You can modify the payload, metadata and run options.
{ if (tab === "payload") { currentPayloadJson.current = v; setPayload(v); } else { currentMetadataJson.current = v; setMetadata(v); } }} height="100%" min-height="100%" max-height="100%" additionalActions={
{ setTab("payload"); }} > Payload {!editablePayload && ( Payload is not editable for runs with{" "} large payloads. } /> )}
{ setTab("metadata"); }} > Metadata
} />
Options enable you to control the execution behavior of your task.{" "} Read the docs. Overrides the machine preset. {machine.errors} {replayData.disableVersionSelection ? ( Only the latest version is available in the development environment. ) : ( Runs task on a specific version. )} {version.errors} {replayData.regions.length > 1 && ( {replayData.disableVersionSelection ? ( Region is not available in the development environment. ) : ( Overrides the region for this run. )} {region.errors} )} {replayData.allowArbitraryQueues ? ( ) : ( )} Assign run to a specific queue. {queue.errors} Add tags to easily filter runs. {tags.errors} { // only allow entering integers > 1 if (["-", "+", ".", "e", "E"].includes(e.key)) { e.preventDefault(); } }} onBlur={(e) => { const value = parseInt(e.target.value); if (value < 1 && e.target.value !== "") { e.target.value = "1"; } }} /> Retries failed runs up to the specified number of attempts. {maxAttempts.errors} Overrides the maximum compute time limit for the run. {maxDurationSeconds.errors} {idempotencyKey.errors} Specify an idempotency key to ensure that a task is only triggered once with the same key. Keys expire after 30 days by default. {idempotencyKeyTTLSeconds.errors} Limits concurrency by creating a separate queue for each value of the key. {concurrencyKey.errors} Delays run by a specific duration. {delaySeconds.errors} Sets the priority of the run. Higher values mean higher priority. {prioritySeconds.errors} Expires the run if it hasn't started within the TTL. {ttlSeconds.errors} {form.errors}
); }