Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions apps/dashboard/src/components/run-action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { cancelWorkflowRunServerFn, rerunWorkflowRunServerFn } from "@/lib/api";
import { isRunCancelableStatus, TERMINAL_RUN_STATUSES } from "@/lib/status";
import { useNavigate } from "@tanstack/react-router";
import type { WorkflowRunStatus } from "openworkflow/internal";
import { useState } from "react";

interface RunActionProps {
action?: "cancel" | "rerun";
fromStep?: string;
runId: string;
status: WorkflowRunStatus;
onDone?: (() => Promise<void>) | (() => void);
}

function getErrorMessage(cause: unknown): string {
if (cause instanceof Error && cause.message) {
return cause.message;
}

return "Unable to update workflow run";
}

export function RunAction({
action = "cancel",
fromStep,
runId,
status,
onDone,
}: RunActionProps) {
const navigate = useNavigate();
const rerun = action === "rerun";
const rerunLabel = fromStep === undefined ? "Rerun" : "Rerun from step";
const actionLabel = rerun ? rerunLabel : "Cancel Run";
const rerunDescription =
fromStep === undefined
? "Creates a new run with the original input and version. All steps will execute again."
: `Creates a new run from "${fromStep}" using earlier saved results. This step and subsequent work will execute again, including child workflows they start.`;
const [isOpen, setIsOpen] = useState(false);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);

if (
rerun ? !TERMINAL_RUN_STATUSES.has(status) : !isRunCancelableStatus(status)
) {
return null;
}

async function performAction() {
setIsPending(true);
setError(null);

try {
if (rerun) {
const run = await rerunWorkflowRunServerFn({
data: { workflowRunId: runId, fromStep },
});
await navigate({ to: "/runs/$runId", params: { runId: run.id } });
} else {
await cancelWorkflowRunServerFn({ data: { workflowRunId: runId } });
}
await onDone?.();
setIsOpen(false);
} catch (caughtError) {
setError(getErrorMessage(caughtError));
} finally {
setIsPending(false);
}
}

return (
<AlertDialog
open={isOpen}
onOpenChange={(nextOpen) => {
setIsOpen(nextOpen);
if (!nextOpen) {
setError(null);
}
}}
>
<Button
type="button"
variant={rerun ? "default" : "destructive"}
onClick={() => {
setIsOpen(true);
}}
disabled={isPending}
>
{actionLabel}
</Button>

<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{rerun ? `${rerunLabel}?` : "Cancel this run?"}
</AlertDialogTitle>
<AlertDialogDescription>
{rerun
? rerunDescription
: "This will stop any future progress for this workflow run."}
</AlertDialogDescription>
</AlertDialogHeader>

{error && <p className="text-destructive text-xs">{error}</p>}

<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>
{rerun ? "Cancel" : "Keep Running"}
</AlertDialogCancel>
<AlertDialogAction
variant={rerun ? "default" : "destructive"}
onClick={(event) => {
event.preventDefault();
void performAction();
}}
disabled={isPending}
>
{isPending ? "Working..." : actionLabel}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
111 changes: 0 additions & 111 deletions apps/dashboard/src/components/run-cancel-action.tsx

This file was deleted.

14 changes: 14 additions & 0 deletions apps/dashboard/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getBackend } from "./backend";
import { createServerFn } from "@tanstack/react-start";
import { rerunWorkflowRun } from "openworkflow/internal";
import type {
PaginatedResponse,
PaginationOptions,
Expand Down Expand Up @@ -90,6 +91,19 @@ export const cancelWorkflowRunServerFn = createServerFn({ method: "POST" })
return backend.cancelWorkflowRun({ workflowRunId: data.workflowRunId });
});

/** Create an independent rerun using the original input and version. */
export const rerunWorkflowRunServerFn = createServerFn({ method: "POST" })
.validator(
z.object({ workflowRunId: z.string(), fromStep: z.string().optional() }),
)
.handler(async ({ data }): Promise<WorkflowRun> => {
return await rerunWorkflowRun(
await getBackend(),
data.workflowRunId,
data.fromStep,
);
});

/**
* List step attempts for a workflow run.
*/
Expand Down
32 changes: 27 additions & 5 deletions apps/dashboard/src/routes/runs/$runId.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppLayout } from "@/components/app-layout";
import { CursorPaginationControls } from "@/components/cursor-pagination-controls";
import { RunCancelAction } from "@/components/run-cancel-action";
import { RunAction } from "@/components/run-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
Expand Down Expand Up @@ -44,7 +44,11 @@ import {
useHydrated,
useRouter,
} from "@tanstack/react-router";
import type { StepAttempt, WorkflowRun } from "openworkflow/internal";
import {
getRerun,
type StepAttempt,
type WorkflowRun,
} from "openworkflow/internal";
import {
type KeyboardEvent,
type ReactNode,
Expand Down Expand Up @@ -220,6 +224,7 @@ function RunDetailsPage() {
);
}

const rerun = getRerun(run.context);
const referenceNowMs = referenceNow.getTime();
const duration = computeDuration(run.startedAt, run.finishedAt);
const startedAt = formatRelativeTime(run.startedAt, referenceNowMs);
Expand Down Expand Up @@ -276,6 +281,13 @@ function RunDetailsPage() {
</h2>
{run.version && <Badge variant="outline">{run.version}</Badge>}
</div>
{rerun && (
<RunRelationRow
label="Rerun of"
runId={rerun.workflowRunId}
className="mt-2"
/>
)}
{parentRun && (
<RunRelationRow
label="Parent Workflow Run"
Expand All @@ -285,11 +297,12 @@ function RunDetailsPage() {
/>
)}
</div>
<div className="sm:shrink-0">
<RunCancelAction
<div className="flex gap-2 sm:shrink-0">
<RunAction action="rerun" runId={run.id} status={run.status} />
<RunAction
runId={run.id}
status={run.status}
onCanceled={async () => {
onDone={async () => {
await router.invalidate();
}}
/>
Expand Down Expand Up @@ -476,6 +489,7 @@ function RunDetailsPage() {
</Card>

<StepInspectorPanel
run={run}
step={selectedStep}
childRun={selectedStepChildRun}
attemptCount={selectedStepAttemptCount}
Expand Down Expand Up @@ -632,13 +646,15 @@ function RunOverviewPanel({
}

interface StepInspectorPanelProps {
run: WorkflowRun;
step: StepAttempt | null;
childRun: WorkflowRun | null;
attemptCount: number;
referenceNow: number;
}

function StepInspectorPanel({
run,
step,
childRun,
attemptCount,
Expand Down Expand Up @@ -672,6 +688,12 @@ function StepInspectorPanel({
<div>
<h3 className="text-base font-semibold">Step Inspector</h3>
<p className="text-muted-foreground mt-1 text-sm">{step.stepName}</p>
<RunAction
action="rerun"
runId={run.id}
status={run.status}
fromStep={step.stepName}
/>
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-2">
<div className="flex min-w-0 items-center gap-2">
<span className="text-muted-foreground shrink-0 text-xs">
Expand Down
Loading