expose block runs for debug sessions (#3740)
This commit is contained in:
@@ -37,26 +37,7 @@ import { useCreateBrowserSessionMutation } from "@/routes/browserSessions/hooks/
|
||||
import { type BrowserSession } from "@/routes/workflows/types/browserSessionTypes";
|
||||
import { CopyText } from "@/routes/workflows/editor/Workspace";
|
||||
import { basicTimeFormat } from "@/util/timeFormat";
|
||||
import { cn, formatMs } from "@/util/utils";
|
||||
|
||||
function toDate(
|
||||
time: string,
|
||||
defaultDate: Date | null = new Date(0),
|
||||
): Date | null {
|
||||
time = time.replace(/\.(\d{3})\d*/, ".$1");
|
||||
|
||||
if (!time.endsWith("Z")) {
|
||||
time += "Z";
|
||||
}
|
||||
|
||||
const date = new Date(time);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return defaultDate;
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
import { cn, formatMs, toDate } from "@/util/utils";
|
||||
|
||||
function sessionIsOpen(browserSession: BrowserSession): boolean {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* THe debugger has an underlying debug_session_id. Block runs that occur within
|
||||
* same debug session are grouped together. We will show them with this component.
|
||||
*/
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { Tip } from "@/components/Tip";
|
||||
import { statusIsFinalized } from "@/routes/tasks/types";
|
||||
import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery";
|
||||
import { cn, formatMs, toDate } from "@/util/utils";
|
||||
|
||||
import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery";
|
||||
import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
|
||||
import {
|
||||
useDebugSessionRunsQuery,
|
||||
type DebugSessionRun,
|
||||
} from "../hooks/useDebugSessionRunsQuery";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
|
||||
function DebuggerBlockRuns() {
|
||||
const { workflowPermanentId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { data: workflowRun } = useWorkflowRunQuery();
|
||||
const { data: workflow } = useWorkflowQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
const { data: debugSession } = useDebugSessionQuery({
|
||||
workflowPermanentId,
|
||||
});
|
||||
const { data: debugSessionRuns } = useDebugSessionRunsQuery({
|
||||
debugSessionId: debugSession?.debug_session_id,
|
||||
});
|
||||
|
||||
const numRuns = debugSessionRuns?.runs.length ?? 0;
|
||||
const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null;
|
||||
const isRunning = isFinalized !== null && !isFinalized;
|
||||
|
||||
const handleClick = (run: DebugSessionRun) => {
|
||||
if (isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const blockLabel = run.block_label;
|
||||
const workflowDefinition = workflow?.workflow_definition;
|
||||
const blocks = workflowDefinition?.blocks ?? [];
|
||||
const block = blocks.find((b) => b.label === blockLabel);
|
||||
|
||||
if (!block) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Block not found",
|
||||
description: `The block with label '${blockLabel}' is no longer found in the workflow.`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(
|
||||
`/workflows/${run.workflow_permanent_id}/${run.workflow_run_id}/${blockLabel}/debug`,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["debug-session-runs"],
|
||||
});
|
||||
// We only want to run this when the workflowRun changes, not on every render
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workflowRun]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollLeft =
|
||||
scrollContainerRef.current.scrollWidth;
|
||||
}
|
||||
}, [debugSessionRuns]);
|
||||
|
||||
if (numRuns <= 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative flex w-full items-center justify-center gap-2 opacity-80 hover:opacity-100">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex max-w-[7rem] gap-2 overflow-x-auto rounded-full bg-[#020617] p-2 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{[...(debugSessionRuns?.runs ?? [])].reverse().map((run) => {
|
||||
const dt = toDate(run.created_at ?? "", null);
|
||||
const ago = dt ? formatMs(Date.now() - dt.getTime()).ago : null;
|
||||
return (
|
||||
<Tip
|
||||
key={run.workflow_run_id}
|
||||
content={
|
||||
ago
|
||||
? `${run.block_label} [${run.status}] (${ago})`
|
||||
: `${run.block_label} [${run.status}]`
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-[1rem] w-[1rem] flex-shrink-0 rounded-full border border-white/50 hover:border-white/80",
|
||||
{
|
||||
"cursor-pointer": !isRunning,
|
||||
},
|
||||
{
|
||||
"animate-spin border-dashed [animation-duration:_2s]":
|
||||
run.status === "running" && isRunning,
|
||||
"border-[red] opacity-50 hover:border-[red] hover:opacity-100":
|
||||
run.status === "failed",
|
||||
},
|
||||
)}
|
||||
onClick={() => handleClick(run)}
|
||||
/>
|
||||
</Tip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { DebuggerBlockRuns };
|
||||
@@ -62,7 +62,7 @@ import {
|
||||
useWorkflowSave,
|
||||
} from "@/store/WorkflowHasChangesStore";
|
||||
import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils";
|
||||
|
||||
import { DebuggerBlockRuns } from "@/routes/workflows/debugger/DebuggerBlockRuns";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
import { FlowRenderer, type FlowRendererProps } from "./FlowRenderer";
|
||||
@@ -1245,7 +1245,7 @@ function Workspace({
|
||||
split={{ left: workflowWidth }}
|
||||
onResize={() => setContainerResizeTrigger((prev) => prev + 1)}
|
||||
>
|
||||
{/* code and infinite canvas */}
|
||||
{/* code, infinite canvas, and block runs */}
|
||||
<div className="relative h-full w-full">
|
||||
<div
|
||||
className={cn(
|
||||
@@ -1305,6 +1305,10 @@ function Workspace({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* block runs history for current debug session id*/}
|
||||
<div className="absolute bottom-[0.5rem] left-[0.75rem] flex w-full items-start justify-center">
|
||||
<DebuggerBlockRuns />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="skyvern-split-right relative flex h-full items-end justify-center bg-[#020617] p-4 pl-6">
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
type Props = {
|
||||
debugSessionId?: string;
|
||||
};
|
||||
|
||||
const debugSessionStatuses = ["created", "completed"] as const;
|
||||
|
||||
type DebugSessionStatus = (typeof debugSessionStatuses)[number];
|
||||
|
||||
interface DebugSession {
|
||||
debug_session_id: string;
|
||||
browser_session_id: string;
|
||||
vnc_streaming_supported: boolean | null;
|
||||
workflow_permanent_id: string | null;
|
||||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
status: DebugSessionStatus;
|
||||
}
|
||||
|
||||
interface DebugSessionRun {
|
||||
ai_fallback: boolean | null;
|
||||
block_label: string;
|
||||
browser_session_id: string;
|
||||
code_gen: boolean | null;
|
||||
debug_session_id: string;
|
||||
failure_reason: string | null;
|
||||
output_parameter_id: string;
|
||||
run_with: string | null;
|
||||
script_run_id: string | null;
|
||||
status: string;
|
||||
workflow_id: string;
|
||||
workflow_permanent_id: string;
|
||||
workflow_run_id: string;
|
||||
created_at: string;
|
||||
queued_at: string | null;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
}
|
||||
|
||||
interface DebugSessionRuns {
|
||||
debug_session: DebugSession;
|
||||
runs: DebugSessionRun[];
|
||||
}
|
||||
|
||||
function useDebugSessionRunsQuery({ debugSessionId }: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
||||
return useQuery<DebugSessionRuns>({
|
||||
queryKey: ["debug-session-runs", debugSessionId],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
const result = await client
|
||||
.get(`/debug-session/${debugSessionId}/runs`)
|
||||
.then((response) => response.data);
|
||||
return result;
|
||||
},
|
||||
enabled: !!debugSessionId,
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
useDebugSessionRunsQuery,
|
||||
type DebugSession,
|
||||
type DebugSessionRun,
|
||||
type DebugSessionStatus,
|
||||
};
|
||||
@@ -36,3 +36,22 @@ export const formatMs = (elapsed: number) => {
|
||||
day: days,
|
||||
};
|
||||
};
|
||||
|
||||
export function toDate(
|
||||
time: string,
|
||||
defaultDate: Date | null = new Date(0),
|
||||
): Date | null {
|
||||
time = time.replace(/\.(\d{3})\d*/, ".$1");
|
||||
|
||||
if (!time.endsWith("Z")) {
|
||||
time += "Z";
|
||||
}
|
||||
|
||||
const date = new Date(time);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return defaultDate;
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user