diff --git a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx index db889586c..706a1c117 100644 --- a/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx +++ b/apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx @@ -1,10 +1,13 @@ +import { CheckIcon } from "@heroicons/react/20/solid"; import { DialogClose } from "@radix-ui/react-dialog"; import { Form, useNavigation, useSubmit } from "@remix-run/react"; -import { useCallback, useEffect, useRef } from "react"; +import { type TaskRunStatus } from "@trigger.dev/database"; +import { useCallback, useEffect, useState } from "react"; import { type UseDataFunctionReturn, useTypedFetcher } from "remix-typedjson"; import { JSONEditor } from "~/components/code/JSONEditor"; import { EnvironmentCombo } from "~/components/environments/EnvironmentLabel"; import { Button } from "~/components/primitives/Buttons"; +import { DateTime } from "~/components/primitives/DateTime"; import { DialogContent, DialogHeader } from "~/components/primitives/Dialog"; import { Header3 } from "~/components/primitives/Headers"; import { InputGroup } from "~/components/primitives/InputGroup"; @@ -12,6 +15,7 @@ import { Label } from "~/components/primitives/Label"; import { Paragraph } from "~/components/primitives/Paragraph"; import { Select, SelectItem } from "~/components/primitives/Select"; import { Spinner, SpinnerWhite } from "~/components/primitives/Spinner"; +import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus"; import { type loader } from "~/routes/resources.taskruns.$runParam.replay"; type ReplayRunDialogProps = { @@ -19,9 +23,17 @@ type ReplayRunDialogProps = { failedRedirect: string; }; +type Run = { + id: string; + createdAt: Date; + number: number; + status: TaskRunStatus; + payload: string; +}; + export function ReplayRunDialog({ runFriendlyId, failedRedirect }: ReplayRunDialogProps) { return ( - + ); @@ -60,15 +72,21 @@ function ReplayForm({ payloadType, environment, environments, + runs, failedRedirect, runFriendlyId, }: UseDataFunctionReturn & { failedRedirect: string; runFriendlyId: string }) { const navigation = useNavigation(); const submit = useSubmit(); - const currentJson = useRef(payload); const formAction = `/resources/taskruns/${runFriendlyId}/replay`; const isSubmitting = navigation.formAction === formAction; + // State for managing the payload and selection + const [currentPayload, setCurrentPayload] = useState(payload); + const [selectedRunId, setSelectedRunId] = useState(undefined); + const [isPayloadModified, setIsPayloadModified] = useState(false); + const [isSelectOpen, setIsSelectOpen] = useState(false); + const editablePayload = payloadType === "application/json" || payloadType === "application/super+json"; @@ -81,7 +99,7 @@ function ReplayForm({ }; if (editablePayload) { - data.payload = currentJson.current; + data.payload = currentPayload; } submit(data, { @@ -90,7 +108,40 @@ function ReplayForm({ }); e.preventDefault(); }, - [currentJson] + [currentPayload, editablePayload, formAction, submit] + ); + + const selectedRun = runs.find((r) => r.id === selectedRunId); + + const handlePayloadChange = useCallback( + (newPayload: string) => { + setCurrentPayload(newPayload); + + // Check if the new payload matches any of the runs + const matchingRun = runs.find((r) => r.payload === newPayload); + if (matchingRun) { + setSelectedRunId(matchingRun.id); + setIsPayloadModified(false); + } else { + setSelectedRunId(undefined); + setIsPayloadModified(true); + } + }, + [runs] + ); + + const handleRunSelect = useCallback( + (value: string | string[]) => { + if (Array.isArray(value)) return; + + const run = runs.find((r: Run) => r.id === value); + if (run) { + setSelectedRunId(value); + setCurrentPayload(run.payload); + setIsPayloadModified(false); + } + }, + [runs] ); return ( @@ -101,16 +152,69 @@ function ReplayForm({ Replaying will create a new run using the same or modified payload, executing against the latest version in your selected environment. - Payload +
+ Payload + +
{ - currentJson.current = v; - }} + onChange={handlePayloadChange} showClearButton={false} showCopyButton={false} height="100%" diff --git a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts index 0fcaf0981..00e2d6ed1 100644 --- a/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts +++ b/apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts @@ -22,6 +22,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) { const run = await $replica.taskRun.findFirst({ select: { + id: true, + taskIdentifier: true, payload: true, payloadType: true, runtimeEnvironmentId: true, @@ -69,6 +71,25 @@ export async function loader({ request, params }: LoaderFunctionArgs) { throw new Response("Environment not found", { status: 404 }); } + // Get recent runs for the same task + const recentRuns = await $replica.taskRun.findMany({ + where: { + taskIdentifier: run.taskIdentifier, + runtimeEnvironmentId: run.runtimeEnvironmentId, + }, + select: { + id: true, + createdAt: true, + number: true, + status: true, + payload: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 20, + }); + return typedjson({ payload: await prettyPrintPacket(run.payload, run.payloadType), payloadType: run.payloadType, @@ -76,6 +97,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { environments: sortEnvironments( run.project.environments.map((environment) => displayableEnvironment(environment, userId)) ), + runs: recentRuns, }); }