Skip to content
Merged
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
1 change: 1 addition & 0 deletions app/api/executions/[executionId]/cancel/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { POST } from "@/keeperhub/api/executions/[executionId]/cancel/route";
18 changes: 14 additions & 4 deletions app/api/internal/executions/[executionId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// start custom keeperhub code //
import { eq } from "drizzle-orm";
import { and, eq, ne } from "drizzle-orm";
import { NextResponse } from "next/server";

import { authenticateInternalService } from "@/keeperhub/lib/internal-service-auth";
Expand Down Expand Up @@ -35,16 +35,21 @@ export async function PATCH(

const typedStatus = status as ExecutionStatus;

// Check execution exists
// Check execution exists and is not already cancelled
const existing = await db.query.workflowExecutions.findFirst({
where: eq(workflowExecutions.id, executionId),
columns: { id: true },
columns: { id: true, status: true },
});

if (!existing) {
return NextResponse.json({ error: "Execution not found" }, { status: 404 });
}

// Don't overwrite cancelled status (user already stopped this execution)
if (existing.status === "cancelled") {
return NextResponse.json({ success: true });
}

// Build update payload
const updateData: {
status: ExecutionStatus;
Expand All @@ -62,7 +67,12 @@ export async function PATCH(
await db
.update(workflowExecutions)
.set(updateData)
.where(eq(workflowExecutions.id, executionId));
.where(
and(
eq(workflowExecutions.id, executionId),
ne(workflowExecutions.status, "cancelled")
)
);

return NextResponse.json({ success: true });
}
Expand Down
2 changes: 1 addition & 1 deletion app/api/workflows/executions/[executionId]/status/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { workflowExecutionLogs, workflowExecutions } from "@/lib/db/schema";

type NodeStatus = {
nodeId: string;
status: "pending" | "running" | "success" | "error";
status: "pending" | "running" | "success" | "error" | "cancelled";
};

export async function GET(
Expand Down
57 changes: 49 additions & 8 deletions components/workflow/workflow-runs.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import { useAtom } from "jotai";
import { useAtom, useAtomValue } from "jotai";
import {
Ban,
Check,
ChevronDown,
ChevronRight,
Expand Down Expand Up @@ -35,6 +36,7 @@ import { getRelativeTime } from "@/lib/utils/time";
import {
currentWorkflowIdAtom,
executionLogsAtom,
runsRefreshTriggerAtom,
selectedExecutionIdAtom,
} from "@/lib/workflow-store";
import { Button } from "../ui/button";
Expand All @@ -45,7 +47,7 @@ type ExecutionLog = {
nodeId: string;
nodeName: string;
nodeType: string;
status: "pending" | "running" | "success" | "error";
status: "pending" | "running" | "success" | "error" | "cancelled";
startedAt: Date;
completedAt: Date | null;
duration: string | null;
Expand Down Expand Up @@ -123,7 +125,7 @@ function createExecutionLogsMap(logs: ExecutionLog[]): Record<
nodeId: string;
nodeName: string;
nodeType: string;
status: "pending" | "running" | "success" | "error";
status: "pending" | "running" | "success" | "error" | "cancelled";
output?: unknown;
}
> {
Expand All @@ -133,7 +135,7 @@ function createExecutionLogsMap(logs: ExecutionLog[]): Record<
nodeId: string;
nodeName: string;
nodeType: string;
status: "pending" | "running" | "success" | "error";
status: "pending" | "running" | "success" | "error" | "cancelled";
output?: unknown;
}
> = {};
Expand Down Expand Up @@ -532,6 +534,9 @@ function getProgressBarColor(status: WorkflowExecution["status"]): string {
if (status === "success") {
return "bg-green-500";
}
if (status === "cancelled") {
return "bg-orange-500";
}
return "bg-red-500";
}

Expand Down Expand Up @@ -902,6 +907,9 @@ export function WorkflowRuns({
selectedExecutionIdAtom
);
const [, setExecutionLogs] = useAtom(executionLogsAtom);
// start custom keeperhub code //
const runsRefreshTrigger = useAtomValue(runsRefreshTriggerAtom);
// end keeperhub code //
const [executions, setExecutions] = useState<WorkflowExecution[]>([]);
const [logs, setLogs] = useState<Record<string, ExecutionLog[]>>({});
const [expandedRuns, setExpandedRuns] = useState<Set<string>>(new Set());
Expand All @@ -911,6 +919,11 @@ export function WorkflowRuns({
// Track which execution we've already auto-expanded to prevent loops
const autoExpandedExecutionRef = useRef<string | null>(null);

// start custom keeperhub code //
// Track terminal executions that have had their final log refresh
const finalizedExecutionsRef = useRef<Set<string>>(new Set());
// end keeperhub code //

const loadExecutions = useCallback(
async (showLoading = true) => {
if (!currentWorkflowId) {
Expand Down Expand Up @@ -947,6 +960,15 @@ export function WorkflowRuns({
loadExecutions();
}, [loadExecutions]);

// start custom keeperhub code //
// Immediate refresh when toolbar signals a new execution started
useEffect(() => {
if (runsRefreshTrigger > 0) {
loadExecutions(false);
}
}, [runsRefreshTrigger, loadExecutions]);
// end keeperhub code //

// Clear expanded runs when workflow changes to prevent stale state
useEffect(() => {
setExpandedRuns(new Set());
Expand All @@ -962,7 +984,7 @@ export function WorkflowRuns({
nodeId: string;
nodeName: string;
nodeType: string;
status: "pending" | "running" | "success" | "error";
status: "pending" | "running" | "success" | "error" | "cancelled";
input: unknown;
output: unknown;
error: string | null;
Expand Down Expand Up @@ -1092,13 +1114,28 @@ export function WorkflowRuns({
const data = await api.workflow.getExecutions(currentWorkflowId);
setExecutions(data as WorkflowExecution[]);

// Also refresh logs for expanded runs (only if they exist in current executions)
const validExecutionIds = new Set(data.map((e) => e.id));
// start custom keeperhub code //
// Refresh logs for expanded runs: always for running, once more for newly-terminal
const terminalStatuses = new Set(["cancelled", "success", "error"]);
const executionMap = new Map(data.map((e) => [e.id, e]));
for (const executionId of expandedRuns) {
if (validExecutionIds.has(executionId)) {
const execution = executionMap.get(executionId);
if (!execution) {
continue;
}
const isTerminal = terminalStatuses.has(execution.status);
const alreadyFinalized =
finalizedExecutionsRef.current.has(executionId);

if (!isTerminal) {
await refreshExecutionLogs(executionId);
} else if (!alreadyFinalized) {
// One final refresh to pick up cancel cleanup, then stop
await refreshExecutionLogs(executionId);
finalizedExecutionsRef.current.add(executionId);
}
}
// end keeperhub code //
} catch (error) {
console.error("Failed to poll executions:", error);
}
Expand Down Expand Up @@ -1154,6 +1191,8 @@ export function WorkflowRuns({
return <X className="h-3 w-3 text-white" />;
case "running":
return <Loader2 className="h-3 w-3 animate-spin text-white" />;
case "cancelled":
return <Ban className="h-3 w-3 text-white" />;
default:
return <Clock className="h-3 w-3 text-white" />;
}
Expand All @@ -1167,6 +1206,8 @@ export function WorkflowRuns({
return "bg-red-600";
case "running":
return "bg-blue-600";
case "cancelled":
return "bg-orange-500";
default:
return "bg-muted-foreground";
}
Expand Down
Loading
Loading