2024-07-11 03:08:52 -07:00
|
|
|
import { getClient } from "@/api/AxiosClient";
|
2025-03-20 13:11:43 -07:00
|
|
|
import { ProxyLocation, Status } from "@/api/types";
|
2024-07-11 03:08:52 -07:00
|
|
|
import { StatusBadge } from "@/components/StatusBadge";
|
2024-12-23 23:44:47 -08:00
|
|
|
import { SwitchBarNavigation } from "@/components/SwitchBarNavigation";
|
2024-09-05 18:40:58 +03:00
|
|
|
import { Button } from "@/components/ui/button";
|
2024-11-19 10:54:38 -08:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogClose,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogTrigger,
|
|
|
|
|
} from "@/components/ui/dialog";
|
2024-07-11 03:08:52 -07:00
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
2024-10-14 13:07:54 -07:00
|
|
|
import { toast } from "@/components/ui/use-toast";
|
|
|
|
|
import { useApiCredential } from "@/hooks/useApiCredential";
|
2024-07-11 03:08:52 -07:00
|
|
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
2024-10-14 13:07:54 -07:00
|
|
|
import { copyText } from "@/util/copyText";
|
2024-12-11 09:23:07 -08:00
|
|
|
import { apiBaseUrl } from "@/util/env";
|
2024-10-14 13:07:54 -07:00
|
|
|
import {
|
|
|
|
|
CopyIcon,
|
2025-03-20 13:11:43 -07:00
|
|
|
FileIcon,
|
2024-10-14 13:07:54 -07:00
|
|
|
Pencil2Icon,
|
|
|
|
|
PlayIcon,
|
2024-11-14 11:30:11 -08:00
|
|
|
ReloadIcon,
|
2024-10-14 13:07:54 -07:00
|
|
|
} from "@radix-ui/react-icons";
|
2024-12-11 09:23:07 -08:00
|
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
2024-10-14 13:07:54 -07:00
|
|
|
import fetchToCurl from "fetch-to-curl";
|
2025-01-03 13:42:01 -08:00
|
|
|
import { Link, Outlet, useParams, useSearchParams } from "react-router-dom";
|
2024-12-11 09:23:07 -08:00
|
|
|
import { statusIsFinalized, statusIsRunningOrQueued } from "../tasks/types";
|
2024-10-08 06:00:19 -07:00
|
|
|
import { useWorkflowQuery } from "./hooks/useWorkflowQuery";
|
2024-12-11 09:23:07 -08:00
|
|
|
import { useWorkflowRunQuery } from "./hooks/useWorkflowRunQuery";
|
2025-01-03 13:42:01 -08:00
|
|
|
import { WorkflowRunTimeline } from "./workflowRun/WorkflowRunTimeline";
|
|
|
|
|
import { useWorkflowRunTimelineQuery } from "./hooks/useWorkflowRunTimelineQuery";
|
|
|
|
|
import { findActiveItem } from "./workflowRun/workflowTimelineUtils";
|
2025-03-20 13:11:43 -07:00
|
|
|
import { getAggregatedExtractedInformation } from "./workflowRun/workflowRunUtils";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import { CodeEditor } from "./components/CodeEditor";
|
2025-03-21 12:02:20 -07:00
|
|
|
import { cn } from "@/util/utils";
|
2025-03-25 07:12:30 -07:00
|
|
|
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
2024-07-11 03:08:52 -07:00
|
|
|
|
|
|
|
|
function WorkflowRun() {
|
2025-01-03 13:42:01 -08:00
|
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
|
|
|
const active = searchParams.get("active");
|
2024-07-11 03:08:52 -07:00
|
|
|
const { workflowRunId, workflowPermanentId } = useParams();
|
|
|
|
|
const credentialGetter = useCredentialGetter();
|
2024-10-08 06:00:19 -07:00
|
|
|
const apiCredential = useApiCredential();
|
2024-11-11 07:47:31 -08:00
|
|
|
const queryClient = useQueryClient();
|
2024-10-08 06:00:19 -07:00
|
|
|
|
|
|
|
|
const { data: workflow, isLoading: workflowIsLoading } = useWorkflowQuery({
|
|
|
|
|
workflowPermanentId,
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-21 06:44:40 -07:00
|
|
|
const {
|
|
|
|
|
data: workflowRun,
|
|
|
|
|
isLoading: workflowRunIsLoading,
|
|
|
|
|
isFetched,
|
|
|
|
|
} = useWorkflowRunQuery();
|
2024-07-11 03:08:52 -07:00
|
|
|
|
2025-01-03 13:42:01 -08:00
|
|
|
const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery();
|
|
|
|
|
|
2024-11-14 11:30:11 -08:00
|
|
|
const cancelWorkflowMutation = useMutation({
|
|
|
|
|
mutationFn: async () => {
|
|
|
|
|
const client = await getClient(credentialGetter);
|
|
|
|
|
return client
|
|
|
|
|
.post(`/workflows/runs/${workflowRunId}/cancel`)
|
|
|
|
|
.then((response) => response.data);
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ["workflowRun", workflowRunId],
|
|
|
|
|
});
|
|
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ["workflowRun", workflowPermanentId, workflowRunId],
|
|
|
|
|
});
|
|
|
|
|
toast({
|
|
|
|
|
variant: "success",
|
|
|
|
|
title: "Workflow Canceled",
|
|
|
|
|
description: "The workflow has been successfully canceled.",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onError: (error) => {
|
|
|
|
|
toast({
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
title: "Error",
|
|
|
|
|
description: error.message,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2024-09-30 11:36:24 -07:00
|
|
|
const workflowRunIsRunningOrQueued =
|
|
|
|
|
workflowRun && statusIsRunningOrQueued(workflowRun);
|
|
|
|
|
|
2024-11-14 11:30:11 -08:00
|
|
|
const workflowRunIsFinalized = workflowRun && statusIsFinalized(workflowRun);
|
2025-01-03 13:42:01 -08:00
|
|
|
const selection = findActiveItem(
|
|
|
|
|
workflowRunTimeline ?? [],
|
|
|
|
|
active,
|
|
|
|
|
!!workflowRunIsFinalized,
|
|
|
|
|
);
|
2024-07-11 03:08:52 -07:00
|
|
|
const parameters = workflowRun?.parameters ?? {};
|
2024-12-16 07:50:13 -08:00
|
|
|
const proxyLocation =
|
|
|
|
|
workflowRun?.proxy_location ?? ProxyLocation.Residential;
|
2024-07-11 03:08:52 -07:00
|
|
|
|
2024-11-19 10:54:38 -08:00
|
|
|
const title = workflowIsLoading ? (
|
|
|
|
|
<Skeleton className="h-9 w-48" />
|
|
|
|
|
) : (
|
2024-12-19 10:38:44 -08:00
|
|
|
<h1 className="text-3xl">
|
|
|
|
|
<Link
|
|
|
|
|
className="hover:underline hover:underline-offset-2"
|
|
|
|
|
to={`/workflows/${workflowPermanentId}/runs`}
|
|
|
|
|
>
|
|
|
|
|
{workflow?.title}
|
|
|
|
|
</Link>
|
|
|
|
|
</h1>
|
2024-11-19 10:54:38 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const workflowFailureReason = workflowRun?.failure_reason ? (
|
|
|
|
|
<div
|
|
|
|
|
className="space-y-2 rounded-md border border-red-600 p-4"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: "rgba(220, 38, 38, 0.10)",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="font-bold">Workflow Failure Reason</div>
|
|
|
|
|
<div className="text-sm">{workflowRun.failure_reason}</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : null;
|
|
|
|
|
|
2025-01-03 13:42:01 -08:00
|
|
|
function handleSetActiveItem(id: string) {
|
|
|
|
|
searchParams.set("active", id);
|
|
|
|
|
setSearchParams(searchParams, {
|
|
|
|
|
replace: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-23 22:40:52 -08:00
|
|
|
const isTaskv2Run = workflowRun && workflowRun.task_v2 !== null;
|
2025-01-27 22:12:02 +08:00
|
|
|
|
2025-03-20 13:11:43 -07:00
|
|
|
const outputs = workflowRun?.outputs;
|
|
|
|
|
const aggregatedExtractedInformation = getAggregatedExtractedInformation(
|
|
|
|
|
outputs ?? {},
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-21 06:44:40 -07:00
|
|
|
const hasSomeExtractedInformation = Object.values(
|
|
|
|
|
aggregatedExtractedInformation,
|
|
|
|
|
).some((value) => value !== null);
|
|
|
|
|
|
|
|
|
|
const hasFileUrls =
|
|
|
|
|
isFetched &&
|
|
|
|
|
workflowRun &&
|
|
|
|
|
workflowRun.downloaded_file_urls &&
|
|
|
|
|
workflowRun.downloaded_file_urls.length > 0;
|
|
|
|
|
const fileUrls = hasFileUrls
|
|
|
|
|
? (workflowRun.downloaded_file_urls as string[])
|
|
|
|
|
: [];
|
|
|
|
|
|
2025-03-21 12:02:20 -07:00
|
|
|
const showBoth = hasSomeExtractedInformation && hasFileUrls;
|
|
|
|
|
|
2025-03-21 06:44:40 -07:00
|
|
|
const showOutputSection =
|
|
|
|
|
workflowRunIsFinalized &&
|
|
|
|
|
(hasSomeExtractedInformation || hasFileUrls) &&
|
|
|
|
|
workflowRun.status === Status.Completed;
|
2025-03-20 13:11:43 -07:00
|
|
|
|
2024-07-11 03:08:52 -07:00
|
|
|
return (
|
|
|
|
|
<div className="space-y-8">
|
2024-09-05 16:58:38 +03:00
|
|
|
<header className="flex justify-between">
|
2024-10-08 06:00:19 -07:00
|
|
|
<div className="space-y-3">
|
2024-10-14 13:07:54 -07:00
|
|
|
<div className="flex items-center gap-5">
|
2024-11-19 10:54:38 -08:00
|
|
|
{title}
|
2024-10-08 06:00:19 -07:00
|
|
|
{workflowRunIsLoading ? (
|
|
|
|
|
<Skeleton className="h-8 w-28" />
|
|
|
|
|
) : workflowRun ? (
|
|
|
|
|
<StatusBadge status={workflowRun?.status} />
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2024-11-19 10:54:38 -08:00
|
|
|
<h2 className="text-2xl text-slate-400">{workflowRunId}</h2>
|
2024-09-05 16:58:38 +03:00
|
|
|
</div>
|
2024-10-08 06:00:19 -07:00
|
|
|
|
2024-10-01 07:16:46 -07:00
|
|
|
<div className="flex gap-2">
|
2024-10-08 06:00:19 -07:00
|
|
|
<Button
|
|
|
|
|
variant="secondary"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (!workflowRun) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const curl = fetchToCurl({
|
|
|
|
|
method: "POST",
|
|
|
|
|
url: `${apiBaseUrl}/workflows/${workflowPermanentId}/run`,
|
|
|
|
|
body: {
|
|
|
|
|
data: workflowRun?.parameters,
|
|
|
|
|
proxy_location: "RESIDENTIAL",
|
|
|
|
|
},
|
|
|
|
|
headers: {
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
"x-api-key": apiCredential ?? "<your-api-key>",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
copyText(curl).then(() => {
|
|
|
|
|
toast({
|
|
|
|
|
variant: "success",
|
|
|
|
|
title: "Copied to Clipboard",
|
|
|
|
|
description:
|
|
|
|
|
"The cURL command has been copied to your clipboard.",
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<CopyIcon className="mr-2 h-4 w-4" />
|
2024-10-09 08:53:40 -07:00
|
|
|
cURL
|
2024-10-08 06:00:19 -07:00
|
|
|
</Button>
|
2024-10-01 07:16:46 -07:00
|
|
|
<Button asChild variant="secondary">
|
|
|
|
|
<Link to={`/workflows/${workflowPermanentId}/edit`}>
|
|
|
|
|
<Pencil2Icon className="mr-2 h-4 w-4" />
|
2024-10-09 07:02:59 -07:00
|
|
|
Edit
|
2024-10-01 07:16:46 -07:00
|
|
|
</Link>
|
|
|
|
|
</Button>
|
2024-11-14 11:30:11 -08:00
|
|
|
{workflowRunIsRunningOrQueued && (
|
|
|
|
|
<Dialog>
|
|
|
|
|
<DialogTrigger asChild>
|
|
|
|
|
<Button variant="destructive">Cancel</Button>
|
|
|
|
|
</DialogTrigger>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Are you sure?</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Are you sure you want to cancel this workflow run?
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<DialogClose asChild>
|
|
|
|
|
<Button variant="secondary">Back</Button>
|
|
|
|
|
</DialogClose>
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
cancelWorkflowMutation.mutate();
|
|
|
|
|
}}
|
|
|
|
|
disabled={cancelWorkflowMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{cancelWorkflowMutation.isPending && (
|
|
|
|
|
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
)}
|
|
|
|
|
Cancel Workflow Run
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)}
|
2025-01-27 22:12:02 +08:00
|
|
|
{workflowRunIsFinalized && !isTaskv2Run && (
|
2024-11-14 11:30:11 -08:00
|
|
|
<Button asChild>
|
|
|
|
|
<Link
|
|
|
|
|
to={`/workflows/${workflowPermanentId}/run`}
|
|
|
|
|
state={{
|
|
|
|
|
data: parameters,
|
2024-12-16 07:50:13 -08:00
|
|
|
proxyLocation,
|
2024-11-14 11:30:11 -08:00
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<PlayIcon className="mr-2 h-4 w-4" />
|
|
|
|
|
Rerun
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2024-10-01 07:16:46 -07:00
|
|
|
</div>
|
2024-07-11 03:08:52 -07:00
|
|
|
</header>
|
2025-03-21 06:44:40 -07:00
|
|
|
{showOutputSection && (
|
2025-03-21 12:02:20 -07:00
|
|
|
<div
|
|
|
|
|
className={cn("grid gap-4 rounded-lg bg-slate-elevation1 p-4", {
|
|
|
|
|
"grid-cols-2": showBoth,
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
{hasSomeExtractedInformation && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<Label>Extracted Information</Label>
|
|
|
|
|
<CodeEditor
|
|
|
|
|
language="json"
|
|
|
|
|
value={JSON.stringify(aggregatedExtractedInformation, null, 2)}
|
|
|
|
|
readOnly
|
|
|
|
|
maxHeight="250px"
|
|
|
|
|
/>
|
2025-03-20 13:11:43 -07:00
|
|
|
</div>
|
2025-03-21 12:02:20 -07:00
|
|
|
)}
|
|
|
|
|
{hasFileUrls && (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<Label>Downloaded Files</Label>
|
2025-03-25 07:12:30 -07:00
|
|
|
<ScrollArea>
|
|
|
|
|
<ScrollAreaViewport className="max-h-[250px] space-y-2">
|
|
|
|
|
{fileUrls.length > 0 ? (
|
|
|
|
|
fileUrls.map((url, index) => {
|
|
|
|
|
return (
|
|
|
|
|
<div key={url} title={url} className="flex gap-2">
|
|
|
|
|
<FileIcon className="size-6" />
|
|
|
|
|
<a
|
|
|
|
|
href={url}
|
|
|
|
|
className="underline underline-offset-4"
|
|
|
|
|
>
|
|
|
|
|
<span>{`File ${index + 1}`}</span>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
) : (
|
|
|
|
|
<div className="text-sm">No files downloaded</div>
|
|
|
|
|
)}
|
|
|
|
|
</ScrollAreaViewport>
|
|
|
|
|
</ScrollArea>
|
2025-03-21 12:02:20 -07:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-03-20 13:11:43 -07:00
|
|
|
</div>
|
|
|
|
|
)}
|
2024-11-19 10:54:38 -08:00
|
|
|
{workflowFailureReason}
|
2024-12-23 23:44:47 -08:00
|
|
|
<SwitchBarNavigation
|
|
|
|
|
options={[
|
|
|
|
|
{
|
|
|
|
|
label: "Overview",
|
|
|
|
|
to: "overview",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Output",
|
|
|
|
|
to: "output",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Parameters",
|
|
|
|
|
to: "parameters",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: "Recording",
|
|
|
|
|
to: "recording",
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
|
/>
|
2025-01-03 13:42:01 -08:00
|
|
|
<div className="flex h-[42rem] gap-6">
|
|
|
|
|
<div className="w-2/3">
|
|
|
|
|
<Outlet />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-1/3">
|
|
|
|
|
<WorkflowRunTimeline
|
|
|
|
|
activeItem={selection}
|
|
|
|
|
onActionItemSelected={(item) => {
|
|
|
|
|
handleSetActiveItem(item.action.action_id);
|
|
|
|
|
}}
|
|
|
|
|
onBlockItemSelected={(item) => {
|
|
|
|
|
handleSetActiveItem(item.workflow_run_block_id);
|
|
|
|
|
}}
|
|
|
|
|
onLiveStreamSelected={() => {
|
|
|
|
|
handleSetActiveItem("stream");
|
|
|
|
|
}}
|
|
|
|
|
onObserverThoughtCardSelected={(item) => {
|
2025-01-15 11:41:31 -08:00
|
|
|
handleSetActiveItem(item.thought_id);
|
2025-01-03 13:42:01 -08:00
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2024-07-11 03:08:52 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { WorkflowRun };
|