diff --git a/skyvern-frontend/public/404-sad-dragon-md.png b/skyvern-frontend/public/404-sad-dragon-md.png new file mode 100644 index 00000000..8a73dd3c Binary files /dev/null and b/skyvern-frontend/public/404-sad-dragon-md.png differ diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index 5e31688e..9c00de3c 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -1,3 +1,5 @@ +import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; + export const ArtifactType = { Recording: "recording", ActionScreenshot: "screenshot_action", @@ -374,6 +376,32 @@ export type WorkflowRunStatusApiResponse = { max_screenshot_scrolls: number | null; }; +export type WorkflowRunStatusApiResponseWithWorkflow = { + workflow_id: string; + workflow_run_id: string; + status: Status; + proxy_location: ProxyLocation | null; + webhook_callback_url: string | null; + extra_http_headers: Record | null; + created_at: string; + finished_at: string; + modified_at: string; + parameters: Record; + screenshot_urls: Array | null; + recording_url: string | null; + outputs: Record | null; + failure_reason: string | null; + webhook_failure_reason: string | null; + downloaded_file_urls: Array | null; + total_steps: number | null; + total_cost: number | null; + task_v2: TaskV2 | null; + workflow_title: string | null; + browser_session_id: string | null; + max_screenshot_scrolls: number | null; + workflow: WorkflowApiResponse; +}; + export type TaskGenerationApiResponse = { suggested_title: string | null; url: string | null; diff --git a/skyvern-frontend/src/components/Status404.css b/skyvern-frontend/src/components/Status404.css new file mode 100644 index 00000000..9b24bd86 --- /dev/null +++ b/skyvern-frontend/src/components/Status404.css @@ -0,0 +1,63 @@ +@keyframes roll-right-404 { + 0% { + transform: translateX(0rem) translateY(1rem) rotate(-60deg); + opacity: 0; + } + 100% { + transform: translateX(10rem) translateY(1rem) rotate(0deg); + opacity: 1; + } +} + +.animate-roll-right-404 { + animation: roll-right-404 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards; +} + +@keyframes fade-in-404 { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +.animate-fade-in-404 { + animation: fade-in-404 1s ease-out 0.5s forwards; + opacity: 0; +} + +.animate-fade-in-slow-404 { + animation: fade-in-404 1s ease-out 1.7s forwards; + opacity: 0; +} + +@keyframes orbit-404 { + 0% { + transform: rotate(0deg) translateX(6.5rem) rotate(0deg); + } + 100% { + transform: rotate(360deg) translateX(6.5rem) rotate(-360deg); + } +} + +.animate-orbit-404 { + animation: orbit-404 8s linear infinite; + transform-origin: center; +} + +@keyframes pulse-glow-404 { + 0%, + 100% { + filter: brightness(1) saturate(1); + opacity: 1; + } + 50% { + filter: brightness(1.5) saturate(1.3); + opacity: 0.8; + } +} + +.animate-pulse-glow-404 { + animation: pulse-glow-404 2s ease-in-out infinite; +} diff --git a/skyvern-frontend/src/components/Status404.tsx b/skyvern-frontend/src/components/Status404.tsx new file mode 100644 index 00000000..12ceda49 --- /dev/null +++ b/skyvern-frontend/src/components/Status404.tsx @@ -0,0 +1,26 @@ +import "./Status404.css"; + +function Status404() { + return ( +
+
+
+
404
+
Not Found
+
+
+
+
+
+
+ 404 Not Found +
+
+ ); +} + +export { Status404 }; diff --git a/skyvern-frontend/src/hooks/useFirstParam.ts b/skyvern-frontend/src/hooks/useFirstParam.ts new file mode 100644 index 00000000..d57206f5 --- /dev/null +++ b/skyvern-frontend/src/hooks/useFirstParam.ts @@ -0,0 +1,17 @@ +import { useParams } from "react-router-dom"; + +/** + * Given a list of parameter names, returns the value of the first one that exists in the URL parameters. + */ +const useFirstParam = (...paramNames: string[]) => { + const params = useParams(); + for (const name of paramNames) { + const value = params[name]; + if (value) { + return value; + } + } + return null; +}; + +export { useFirstParam }; diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx index e1fc4c52..c19ff24f 100644 --- a/skyvern-frontend/src/router.tsx +++ b/skyvern-frontend/src/router.tsx @@ -28,6 +28,7 @@ import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRun import { WorkflowRunCode } from "@/routes/workflows/workflowRun/WorkflowRunCode"; import { DebugStoreProvider } from "@/store/DebugStoreContext"; import { CredentialsPage } from "@/routes/credentials/CredentialsPage.tsx"; +import { RunRouter } from "@/routes/runs/RunRouter"; const router = createBrowserRouter([ { @@ -42,6 +43,20 @@ const router = createBrowserRouter([ ), children: [ + { + path: "runs", + element: , + children: [ + { + index: true, + element: , + }, + ], + }, + { + path: "runs/:runId/*", + element: , + }, { path: "browser-sessions", element: , diff --git a/skyvern-frontend/src/routes/history/RunHistory.tsx b/skyvern-frontend/src/routes/history/RunHistory.tsx index 5eb1e860..837f3067 100644 --- a/skyvern-frontend/src/routes/history/RunHistory.tsx +++ b/skyvern-frontend/src/routes/history/RunHistory.tsx @@ -183,10 +183,7 @@ function RunHistory() { key={run.workflow_run_id} className="cursor-pointer" onClick={(event) => { - handleNavigate( - event, - `/workflows/${run.workflow_permanent_id}/${run.workflow_run_id}/overview`, - ); + handleNavigate(event, `/runs/${run.workflow_run_id}`); }} > , }, { - label: "History", - to: "/history", + label: "Runs", + to: "/runs", icon: , }, { diff --git a/skyvern-frontend/src/routes/runs/RunRouter.tsx b/skyvern-frontend/src/routes/runs/RunRouter.tsx new file mode 100644 index 00000000..275934c0 --- /dev/null +++ b/skyvern-frontend/src/routes/runs/RunRouter.tsx @@ -0,0 +1,94 @@ +/** + * A router component that handles both workflow runs (wr_xxx) and task runs (tsk_xxx) + * under the /runs/:runId path, discriminating based on ID prefix. + */ + +import { Navigate, Route, Routes, useParams } from "react-router-dom"; + +import { PageLayout } from "@/components/PageLayout"; +import { Status404 } from "@/components/Status404"; +import { StepArtifactsLayout } from "@/routes/tasks/detail/StepArtifactsLayout"; +import { TaskActions } from "@/routes/tasks/detail/TaskActions"; +import { TaskDetails } from "@/routes/tasks/detail/TaskDetails"; +import { TaskParameters } from "@/routes/tasks/detail/TaskParameters"; +import { TaskRecording } from "@/routes/tasks/detail/TaskRecording"; +import { WorkflowRun } from "@/routes/workflows/WorkflowRun"; +import { WorkflowPostRunParameters } from "@/routes/workflows/workflowRun/WorkflowPostRunParameters"; +import { WorkflowRunOutput } from "@/routes/workflows/workflowRun/WorkflowRunOutput"; +import { WorkflowRunOverview } from "@/routes/workflows/workflowRun/WorkflowRunOverview"; +import { WorkflowRunRecording } from "@/routes/workflows/workflowRun/WorkflowRunRecording"; +import { WorkflowRunCode } from "@/routes/workflows/workflowRun/WorkflowRunCode"; +import { WorkflowsPageLayout } from "@/routes/workflows/WorkflowsPageLayout"; +import { useTaskV2Query } from "@/routes/runs/useTaskV2Query"; + +function RunRouter() { + let { runId } = useParams(); + + const { data: task_v2, isLoading } = useTaskV2Query({ + id: runId?.startsWith("tsk_v2") ? runId : undefined, + }); + + if (runId?.startsWith("tsk_v2")) { + if (isLoading) { + return
Fetching task details...
; + } + + if (!task_v2) { + console.error("Task for %s not found", runId); + return ; + } + + const workflowRunId = task_v2.workflow_run_id; + + if (!workflowRunId) { + console.error("Workflow run ID for Task V2 %s not found", runId); + return ; + } + + runId = workflowRunId; + + return ; + } + + if (runId?.startsWith("wr_")) { + return ( + + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } + /> + + + + ); + } + + if (runId?.startsWith("tsk_")) { + return ( + + }> + }> + } /> + } /> + } /> + } /> + } /> + + + + ); + } + + // Fallback (should not reach here due to earlier check) + return ; +} + +export { RunRouter }; diff --git a/skyvern-frontend/src/routes/runs/useTaskV2Query.ts b/skyvern-frontend/src/routes/runs/useTaskV2Query.ts new file mode 100644 index 00000000..adeb34ae --- /dev/null +++ b/skyvern-frontend/src/routes/runs/useTaskV2Query.ts @@ -0,0 +1,23 @@ +import { getClient } from "@/api/AxiosClient"; +import { TaskV2 } from "@/api/types"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { useQuery } from "@tanstack/react-query"; + +type Props = { + id?: string; +}; + +function useTaskV2Query({ id }: Props) { + const credentialGetter = useCredentialGetter(); + + return useQuery({ + queryKey: ["task_v2", id], + queryFn: async () => { + const client = await getClient(credentialGetter, "v2"); + return client.get(`/tasks/${id}`).then((response) => response.data); + }, + enabled: !!id, + }); +} + +export { useTaskV2Query }; diff --git a/skyvern-frontend/src/routes/tasks/detail/StepArtifactsLayout.tsx b/skyvern-frontend/src/routes/tasks/detail/StepArtifactsLayout.tsx index 80636125..a1ca5a9e 100644 --- a/skyvern-frontend/src/routes/tasks/detail/StepArtifactsLayout.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/StepArtifactsLayout.tsx @@ -2,16 +2,17 @@ import { StepNavigation } from "./StepNavigation"; import { StepArtifacts } from "./StepArtifacts"; import { useQuery } from "@tanstack/react-query"; import { StepApiResponse } from "@/api/types"; -import { useParams, useSearchParams } from "react-router-dom"; +import { useSearchParams } from "react-router-dom"; import { getClient } from "@/api/AxiosClient"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { apiPathPrefix } from "@/util/env"; +import { useFirstParam } from "@/hooks/useFirstParam"; function StepArtifactsLayout() { const [searchParams, setSearchParams] = useSearchParams(); const step = Number(searchParams.get("step")) || 0; const credentialGetter = useCredentialGetter(); - const { taskId } = useParams(); + const taskId = useFirstParam("taskId", "runId"); const { data: steps, diff --git a/skyvern-frontend/src/routes/tasks/detail/StepNavigation.tsx b/skyvern-frontend/src/routes/tasks/detail/StepNavigation.tsx index 482cacfc..e770e300 100644 --- a/skyvern-frontend/src/routes/tasks/detail/StepNavigation.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/StepNavigation.tsx @@ -2,11 +2,12 @@ import { getClient } from "@/api/AxiosClient"; import { StepApiResponse } from "@/api/types"; import { cn } from "@/util/utils"; import { useQuery } from "@tanstack/react-query"; -import { useParams, useSearchParams } from "react-router-dom"; +import { useSearchParams } from "react-router-dom"; import { PAGE_SIZE } from "../constants"; import { CheckboxIcon, CrossCircledIcon } from "@radix-ui/react-icons"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { apiPathPrefix } from "@/util/env"; +import { useFirstParam } from "@/hooks/useFirstParam"; type Props = { activeIndex: number; @@ -14,7 +15,7 @@ type Props = { }; function StepNavigation({ activeIndex, onActiveIndexChange }: Props) { - const { taskId } = useParams(); + const taskId = useFirstParam("taskId", "runId"); const [searchParams] = useSearchParams(); const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; const credentialGetter = useCredentialGetter(); diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx index a29bbefe..66907294 100644 --- a/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/TaskActions.tsx @@ -12,7 +12,6 @@ import { useQueryClient, } from "@tanstack/react-query"; import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; import { statusIsFinalized, statusIsNotFinalized, @@ -21,6 +20,7 @@ import { import { ActionScreenshot } from "./ActionScreenshot"; import { useActions } from "./hooks/useActions"; import { ScrollableActionList } from "./ScrollableActionList"; +import { useFirstParam } from "@/hooks/useFirstParam"; const formatter = Intl.NumberFormat("en-US", { style: "currency", @@ -38,7 +38,7 @@ let socket: WebSocket | null = null; const wssBaseUrl = import.meta.env.VITE_WSS_BASE_URL; function TaskActions() { - const { taskId } = useParams(); + const taskId = useFirstParam("taskId", "runId"); const credentialGetter = useCredentialGetter(); const [streamImgSrc, setStreamImgSrc] = useState(""); const [selectedAction, setSelectedAction] = useState< @@ -157,7 +157,7 @@ function TaskActions() { }); const { data: actions, isLoading: actionsIsLoading } = useActions({ - id: taskId, + id: taskId ?? undefined, }); if (taskIsLoading || actionsIsLoading || stepsIsLoading) { diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx index 1048a6ca..d80bc7b6 100644 --- a/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/TaskDetails.tsx @@ -1,3 +1,4 @@ +import { AxiosError } from "axios"; import { getClient } from "@/api/AxiosClient"; import { useState } from "react"; import { @@ -6,6 +7,7 @@ import { TaskApiResponse, WorkflowRunStatusApiResponse, } from "@/api/types"; +import { Status404 } from "@/components/Status404"; import { StatusBadge } from "@/components/StatusBadge"; import { SwitchBarNavigation } from "@/components/SwitchBarNavigation"; import { Button } from "@/components/ui/button"; @@ -33,10 +35,11 @@ import { type ApiCommandOptions } from "@/util/apiCommands"; import { buildTaskRunPayload } from "@/util/taskRunPayload"; import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Link, Outlet, useParams } from "react-router-dom"; +import { Link, Outlet } from "react-router-dom"; import { statusIsFinalized } from "../types"; import { MAX_STEPS_DEFAULT } from "../constants"; import { useTaskQuery } from "./hooks/useTaskQuery"; +import { useFirstParam } from "@/hooks/useFirstParam"; function createTaskRequestObject(values: TaskApiResponse) { return { @@ -52,7 +55,7 @@ function createTaskRequestObject(values: TaskApiResponse) { } function TaskDetails() { - const { taskId } = useParams(); + const taskId = useFirstParam("taskId", "runId"); const credentialGetter = useCredentialGetter(); const queryClient = useQueryClient(); const apiCredential = useApiCredential(); @@ -62,7 +65,7 @@ function TaskDetails() { isLoading: taskIsLoading, isError: taskIsError, error: taskError, - } = useTaskQuery({ id: taskId }); + } = useTaskQuery({ id: taskId ?? undefined }); const { data: workflowRun, isLoading: workflowRunIsLoading } = useQuery({ @@ -132,6 +135,12 @@ function TaskDetails() { const [replayOpen, setReplayOpen] = useState(false); if (taskIsError) { + const status = (taskError as AxiosError | undefined)?.response?.status; + + if (status === 404) { + return ; + } + return
Error: {taskError?.message}
; } @@ -293,9 +302,7 @@ function TaskDetails() { ) : ( workflow && workflowRun && ( - + {workflow.title} ) diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx index b13c5a85..811919f3 100644 --- a/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/TaskParameters.tsx @@ -9,10 +9,10 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "@/routes/workflows/editor/nodes/Taskv2Node/types"; import { useQuery } from "@tanstack/react-query"; -import { useParams } from "react-router-dom"; +import { useFirstParam } from "@/hooks/useFirstParam"; function TaskParameters() { - const { taskId } = useParams(); + const taskId = useFirstParam("taskId", "runId"); const credentialGetter = useCredentialGetter(); const { data: task, diff --git a/skyvern-frontend/src/routes/tasks/detail/TaskRecording.tsx b/skyvern-frontend/src/routes/tasks/detail/TaskRecording.tsx index 05bb8b98..fd0240cb 100644 --- a/skyvern-frontend/src/routes/tasks/detail/TaskRecording.tsx +++ b/skyvern-frontend/src/routes/tasks/detail/TaskRecording.tsx @@ -2,12 +2,12 @@ import { getClient } from "@/api/AxiosClient"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useQuery } from "@tanstack/react-query"; import { getRecordingURL } from "./artifactUtils"; -import { useParams } from "react-router-dom"; import { Skeleton } from "@/components/ui/skeleton"; import { TaskApiResponse } from "@/api/types"; +import { useFirstParam } from "@/hooks/useFirstParam"; function TaskRecording() { - const { taskId } = useParams(); + const taskId = useFirstParam("taskId", "runId"); const credentialGetter = useCredentialGetter(); const { diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx index 0ee81d5f..51c816c2 100644 --- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx +++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx @@ -246,9 +246,7 @@ function RunWorkflowForm({ queryClient.invalidateQueries({ queryKey: ["runs"], }); - navigate( - `/workflows/${workflowPermanentId}/${response.data.workflow_run_id}/overview`, - ); + navigate(`/runs/${response.data.workflow_run_id}`); }, onError: (error: AxiosError) => { const detail = (error.response?.data as { detail?: string })?.detail; diff --git a/skyvern-frontend/src/routes/workflows/WorkflowPage.tsx b/skyvern-frontend/src/routes/workflows/WorkflowPage.tsx index c493e123..7f234a6d 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowPage.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowPage.tsx @@ -189,15 +189,13 @@ function WorkflowPage() { if (event.ctrlKey || event.metaKey) { window.open( window.location.origin + - `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`, + `/runs/${workflowRun.workflow_run_id}`, "_blank", "noopener,noreferrer", ); return; } - navigate( - `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`, - ); + navigate(`/runs/${workflowRun.workflow_run_id}`); }} className="cursor-pointer" > diff --git a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx index bbf106ec..c1e1a2f6 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx @@ -1,3 +1,4 @@ +import { AxiosError } from "axios"; import { useEffect, useState } from "react"; import { getClient } from "@/api/AxiosClient"; import { ProxyLocation, Status } from "@/api/types"; @@ -7,6 +8,7 @@ import { type SwitchBarNavigationOption, } from "@/components/SwitchBarNavigation"; import { Button } from "@/components/ui/button"; +import { Status404 } from "@/components/Status404"; import { Dialog, DialogClose, @@ -32,8 +34,7 @@ import { import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Link, Outlet, useParams, useSearchParams } from "react-router-dom"; import { statusIsFinalized, statusIsRunningOrQueued } from "../tasks/types"; -import { useWorkflowQuery } from "./hooks/useWorkflowQuery"; -import { useWorkflowRunQuery } from "./hooks/useWorkflowRunQuery"; +import { useWorkflowRunWithWorkflowQuery } from "./hooks/useWorkflowRunWithWorkflowQuery"; import { WorkflowRunTimeline } from "./workflowRun/WorkflowRunTimeline"; import { useWorkflowRunTimelineQuery } from "./hooks/useWorkflowRunTimelineQuery"; import { findActiveItem } from "./workflowRun/workflowTimelineUtils"; @@ -54,23 +55,22 @@ function WorkflowRun() { const embed = searchParams.get("embed"); const isEmbedded = embed === "true"; const active = searchParams.get("active"); - const { workflowRunId, workflowPermanentId } = useParams(); + const { workflowRunId } = useParams(); const credentialGetter = useCredentialGetter(); const apiCredential = useApiCredential(); const queryClient = useQueryClient(); - const { data: workflow, isLoading: workflowIsLoading } = useWorkflowQuery({ - workflowPermanentId, - }); - - const cacheKey = workflow?.cache_key ?? ""; - const { data: workflowRun, isLoading: workflowRunIsLoading, isFetched, - } = useWorkflowRunQuery(); + error, + } = useWorkflowRunWithWorkflowQuery(); + const status = (error as AxiosError | undefined)?.response?.status; + const workflow = workflowRun?.workflow; + const workflowPermanentId = workflow?.workflow_permanent_id; + const cacheKey = workflow?.cache_key ?? ""; const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null; const [hasPublishedCode, setHasPublishedCode] = useState(false); @@ -155,7 +155,7 @@ function WorkflowRun() { workflowRun?.proxy_location ?? ProxyLocation.Residential; const maxScreenshotScrolls = workflowRun?.max_screenshot_scrolls ?? null; - const title = workflowIsLoading ? ( + const title = workflowRunIsLoading ? ( ) : (

@@ -292,6 +292,10 @@ function WorkflowRun() { }, ]; + if (status === 404) { + return ; + } + return (
{!isEmbedded && ( diff --git a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunTimelineQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunTimelineQuery.ts index 92d5d632..acca3655 100644 --- a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunTimelineQuery.ts +++ b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunTimelineQuery.ts @@ -4,14 +4,19 @@ import { statusIsNotFinalized } from "@/routes/tasks/types"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import { useParams } from "react-router-dom"; import { WorkflowRunTimelineItem } from "../types/workflowRunTypes"; -import { useWorkflowRunQuery } from "./useWorkflowRunQuery"; +import { useWorkflowRunWithWorkflowQuery } from "./useWorkflowRunWithWorkflowQuery"; import { useGlobalWorkflowsQuery } from "./useGlobalWorkflowsQuery"; +import { useFirstParam } from "@/hooks/useFirstParam"; function useWorkflowRunTimelineQuery() { - const { workflowRunId, workflowPermanentId } = useParams(); + const workflowRunId = useFirstParam("workflowRunId", "runId"); + const { workflowPermanentId: workflowPermanentIdParam } = useParams(); const credentialGetter = useCredentialGetter(); const { data: globalWorkflows } = useGlobalWorkflowsQuery(); - const { data: workflowRun } = useWorkflowRunQuery(); + const { data: workflowRun } = useWorkflowRunWithWorkflowQuery(); + + const workflowPermanentId = + workflowPermanentIdParam ?? workflowRun?.workflow?.workflow_permanent_id; return useQuery>({ queryKey: ["workflowRunTimeline", workflowPermanentId, workflowRunId], diff --git a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunWithWorkflowQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunWithWorkflowQuery.ts new file mode 100644 index 00000000..08359fbe --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowRunWithWorkflowQuery.ts @@ -0,0 +1,51 @@ +import { getClient } from "@/api/AxiosClient"; +import { WorkflowRunStatusApiResponseWithWorkflow } from "@/api/types"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { + statusIsNotFinalized, + statusIsRunningOrQueued, +} from "@/routes/tasks/types"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import { useFirstParam } from "@/hooks/useFirstParam"; + +function useWorkflowRunWithWorkflowQuery() { + const workflowRunId = useFirstParam("workflowRunId", "runId"); + const credentialGetter = useCredentialGetter(); + + return useQuery({ + queryKey: ["workflowRun", workflowRunId], + queryFn: async () => { + const client = await getClient(credentialGetter, "sans-api-v1"); + return client + .get(`/workflows/runs/${workflowRunId}`) + .then((response) => response.data); + }, + refetchInterval: (query) => { + if (!query.state.data) { + return false; + } + if (statusIsNotFinalized(query.state.data)) { + return 5000; + } + return false; + }, + // required for OS-level notifications to work (workflow run completion) + refetchIntervalInBackground: true, + placeholderData: keepPreviousData, + refetchOnMount: (query) => { + if (!query.state.data) { + return false; + } + return statusIsRunningOrQueued(query.state.data) ? "always" : false; + }, + refetchOnWindowFocus: (query) => { + if (!query.state.data) { + return false; + } + return statusIsRunningOrQueued(query.state.data); + }, + enabled: !!workflowRunId, + }); +} + +export { useWorkflowRunWithWorkflowQuery }; diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx index 7aa74177..418bd2c6 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowPostRunParameters.tsx @@ -1,4 +1,4 @@ -import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery"; +import { useWorkflowRunWithWorkflowQuery } from "../hooks/useWorkflowRunWithWorkflowQuery"; import { CodeEditor } from "../components/CodeEditor"; import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea"; import { useActiveWorkflowRunItem } from "./useActiveWorkflowRunItem"; @@ -18,7 +18,7 @@ function WorkflowPostRunParameters() { useWorkflowRunTimelineQuery(); const [activeItem] = useActiveWorkflowRunItem(); const { data: workflowRun, isLoading: workflowRunIsLoading } = - useWorkflowRunQuery(); + useWorkflowRunWithWorkflowQuery(); const parameters = workflowRun?.parameters ?? {}; if (workflowRunIsLoading || workflowRunTimelineIsLoading) { diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx index 53287d78..fd107b42 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx @@ -1,5 +1,4 @@ import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; import { @@ -15,8 +14,7 @@ import { statusIsFinalized } from "@/routes/tasks/types"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery"; import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValuesQuery"; -import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery"; -import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; +import { useWorkflowRunWithWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowRunWithWorkflowQuery"; import { constructCacheKeyValue } from "@/routes/workflows/editor/utils"; import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils"; import { cn } from "@/util/utils"; @@ -30,11 +28,9 @@ interface Props { function WorkflowRunCode(props?: Props) { const showCacheKeyValueSelector = props?.showCacheKeyValueSelector ?? false; const queryClient = useQueryClient(); - const { workflowPermanentId } = useParams(); - const { data: workflowRun } = useWorkflowRunQuery(); - const { data: workflow } = useWorkflowQuery({ - workflowPermanentId, - }); + const { data: workflowRun } = useWorkflowRunWithWorkflowQuery(); + const workflow = workflowRun?.workflow; + const workflowPermanentId = workflow?.workflow_permanent_id; const cacheKey = workflow?.cache_key ?? ""; const [cacheKeyValue, setCacheKeyValue] = useState( cacheKey === "" diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOutput.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOutput.tsx index f615155b..a8c1f1e5 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOutput.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOutput.tsx @@ -1,6 +1,6 @@ import { FileIcon } from "@radix-ui/react-icons"; import { CodeEditor } from "../components/CodeEditor"; -import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery"; +import { useWorkflowRunWithWorkflowQuery } from "../hooks/useWorkflowRunWithWorkflowQuery"; import { useActiveWorkflowRunItem } from "./useActiveWorkflowRunItem"; import { hasExtractedInformation, @@ -17,7 +17,7 @@ function WorkflowRunOutput() { const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } = useWorkflowRunTimelineQuery(); const [activeItem] = useActiveWorkflowRunItem(); - const { data: workflowRun } = useWorkflowRunQuery(); + const { data: workflowRun } = useWorkflowRunWithWorkflowQuery(); if (workflowRunTimelineIsLoading) { return
Loading...
; diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx index 79257887..a9c3c941 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunOverview.tsx @@ -3,8 +3,7 @@ import { BrowserStream } from "@/components/BrowserStream"; import { AspectRatio } from "@/components/ui/aspect-ratio"; import { ActionScreenshot } from "@/routes/tasks/detail/ActionScreenshot"; import { statusIsFinalized } from "@/routes/tasks/types"; -import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery"; -import { useParams } from "react-router-dom"; +import { useWorkflowRunWithWorkflowQuery } from "../hooks/useWorkflowRunWithWorkflowQuery"; import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery"; import { isAction, @@ -37,17 +36,16 @@ export type WorkflowRunOverviewActiveElement = function WorkflowRunOverview() { const [searchParams] = useSearchParams(); const active = searchParams.get("active"); - const { workflowPermanentId } = useParams<{ - workflowPermanentId: string; - }>(); const queryClient = useQueryClient(); const { data: workflowRun, isLoading: workflowRunIsLoading } = - useWorkflowRunQuery(); + useWorkflowRunWithWorkflowQuery(); const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } = useWorkflowRunTimelineQuery(); const workflowRunId = workflowRun?.workflow_run_id; + const workflow = workflowRun?.workflow; + const workflowPermanentId = workflow?.workflow_permanent_id; const invalidateQueries = useCallback(() => { if (workflowRunId) { diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunRecording.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunRecording.tsx index fa6d7766..b57e246a 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunRecording.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunRecording.tsx @@ -1,8 +1,8 @@ -import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery"; +import { useWorkflowRunWithWorkflowQuery } from "../hooks/useWorkflowRunWithWorkflowQuery"; import { artifactApiBaseUrl } from "@/util/env"; function WorkflowRunRecording() { - const { data: workflowRun } = useWorkflowRunQuery(); + const { data: workflowRun } = useWorkflowRunWithWorkflowQuery(); let recordingURL = workflowRun?.recording_url; if (recordingURL?.startsWith("file://")) { recordingURL = `${artifactApiBaseUrl}/artifact/recording?path=${recordingURL.slice(7)}`; diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx index 73e80332..a179a6f9 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimeline.tsx @@ -3,7 +3,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { statusIsFinalized, statusIsNotFinalized } from "@/routes/tasks/types"; import { cn } from "@/util/utils"; import { DotFilledIcon } from "@radix-ui/react-icons"; -import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery"; +import { useWorkflowRunWithWorkflowQuery } from "../hooks/useWorkflowRunWithWorkflowQuery"; import { useWorkflowRunTimelineQuery } from "../hooks/useWorkflowRunTimelineQuery"; import { isBlockItem, @@ -36,7 +36,7 @@ function WorkflowRunTimeline({ onBlockItemSelected, }: Props) { const { data: workflowRun, isLoading: workflowRunIsLoading } = - useWorkflowRunQuery(); + useWorkflowRunWithWorkflowQuery(); const { data: workflowRunTimeline, isLoading: workflowRunTimelineIsLoading } = useWorkflowRunTimelineQuery();