From d34a403c8f815cf1e1507e2864fdd220520540ae Mon Sep 17 00:00:00 2001 From: Shuchang Zheng Date: Tue, 4 Feb 2025 21:40:55 +0800 Subject: [PATCH] UI for workflow templates (#1715) Co-authored-by: Muhammed Salih Altun --- .../src/assets/job-application.png | Bin 0 -> 20520 bytes .../src/components/icons/CompassIcon.tsx | 33 ++ skyvern-frontend/src/hooks/useRunsQuery.ts | 35 +++ skyvern-frontend/src/router.tsx | 26 +- .../src/routes/discover/DiscoverPage.tsx | 13 + .../routes/discover/DiscoverPageLayout.tsx | 13 + .../discover/TemporaryTemplateImages.ts | 6 + .../routes/discover/WorkflowTemplateCard.tsx | 30 ++ .../src/routes/discover/WorkflowTemplates.tsx | 50 +++ .../src/routes/history/HistoryPage.tsx | 11 + .../src/routes/history/HistoryPageLayout.tsx | 13 + .../src/routes/history/RunHistory.tsx | 170 ++++++++++ skyvern-frontend/src/routes/root/SideNav.tsx | 21 +- .../src/routes/tasks/create/PromptBox.tsx | 10 - .../src/routes/workflows/WorkflowActions.tsx | 26 +- .../src/routes/workflows/Workflows.tsx | 296 +++++------------- .../routes/workflows/WorkflowsPageBanner.tsx | 38 +-- .../components/header/NarrativeCard.tsx | 17 + .../routes/workflows/editor/FlowRenderer.tsx | 9 +- .../workflows/editor/WorkflowEditor.tsx | 17 +- .../workflows/editor/WorkflowHeader.tsx | 101 +++--- .../editor/nodes/LoopNode/LoopNode.tsx | 18 +- .../editor/nodes/StartNode/StartNode.tsx | 3 + .../workflows/editor/nodes/StartNode/types.ts | 2 + .../workflows/editor/workflowEditorUtils.ts | 7 +- .../hooks/useCreateWorkflowMutation.ts | 37 +++ .../workflows/hooks/useWorkflowQuery.ts | 1 + 27 files changed, 673 insertions(+), 330 deletions(-) create mode 100644 skyvern-frontend/src/assets/job-application.png create mode 100644 skyvern-frontend/src/components/icons/CompassIcon.tsx create mode 100644 skyvern-frontend/src/hooks/useRunsQuery.ts create mode 100644 skyvern-frontend/src/routes/discover/DiscoverPage.tsx create mode 100644 skyvern-frontend/src/routes/discover/DiscoverPageLayout.tsx create mode 100644 skyvern-frontend/src/routes/discover/TemporaryTemplateImages.ts create mode 100644 skyvern-frontend/src/routes/discover/WorkflowTemplateCard.tsx create mode 100644 skyvern-frontend/src/routes/discover/WorkflowTemplates.tsx create mode 100644 skyvern-frontend/src/routes/history/HistoryPage.tsx create mode 100644 skyvern-frontend/src/routes/history/HistoryPageLayout.tsx create mode 100644 skyvern-frontend/src/routes/history/RunHistory.tsx create mode 100644 skyvern-frontend/src/routes/workflows/components/header/NarrativeCard.tsx create mode 100644 skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts diff --git a/skyvern-frontend/src/assets/job-application.png b/skyvern-frontend/src/assets/job-application.png new file mode 100644 index 0000000000000000000000000000000000000000..beab14da772682dc4897e3673ddd987fc045b100 GIT binary patch literal 20520 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 + + + + ); +} + +export { CompassIcon }; diff --git a/skyvern-frontend/src/hooks/useRunsQuery.ts b/skyvern-frontend/src/hooks/useRunsQuery.ts new file mode 100644 index 00000000..48646e85 --- /dev/null +++ b/skyvern-frontend/src/hooks/useRunsQuery.ts @@ -0,0 +1,35 @@ +import { getClient } from "@/api/AxiosClient"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { useQuery } from "@tanstack/react-query"; +import { Status, TaskApiResponse, WorkflowRunApiResponse } from "@/api/types"; + +type QueryReturnType = Array; +type UseQueryOptions = Omit< + Parameters>[0], + "queryKey" | "queryFn" +>; + +type Props = { + page?: number; + statusFilters?: Array; +} & UseQueryOptions; + +function useRunsQuery({ page = 1, statusFilters }: Props) { + const credentialGetter = useCredentialGetter(); + return useQuery>({ + queryKey: ["runs", { statusFilters }, page], + queryFn: async () => { + const client = await getClient(credentialGetter); + const params = new URLSearchParams(); + params.append("page", String(page)); + if (statusFilters) { + statusFilters.forEach((status) => { + params.append("status", status); + }); + } + return client.get("/runs", { params }).then((res) => res.data); + }, + }); +} + +export { useRunsQuery }; diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx index 74ec6eef..2f289f3d 100644 --- a/skyvern-frontend/src/router.tsx +++ b/skyvern-frontend/src/router.tsx @@ -21,6 +21,10 @@ import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOut import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/WorkflowPostRunParameters"; import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording"; import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview"; +import { DiscoverPageLayout } from "./routes/discover/DiscoverPageLayout"; +import { DiscoverPage } from "./routes/discover/DiscoverPage"; +import { HistoryPageLayout } from "./routes/history/HistoryPageLayout"; +import { HistoryPage } from "./routes/history/HistoryPage"; const router = createBrowserRouter([ { @@ -29,7 +33,7 @@ const router = createBrowserRouter([ children: [ { index: true, - element: , + element: , }, { path: "tasks", @@ -144,6 +148,26 @@ const router = createBrowserRouter([ }, ], }, + { + path: "discover", + element: , + children: [ + { + index: true, + element: , + }, + ], + }, + { + path: "history", + element: , + children: [ + { + index: true, + element: , + }, + ], + }, { path: "settings", element: , diff --git a/skyvern-frontend/src/routes/discover/DiscoverPage.tsx b/skyvern-frontend/src/routes/discover/DiscoverPage.tsx new file mode 100644 index 00000000..5a29b4e5 --- /dev/null +++ b/skyvern-frontend/src/routes/discover/DiscoverPage.tsx @@ -0,0 +1,13 @@ +import { PromptBox } from "../tasks/create/PromptBox"; +import { WorkflowTemplates } from "./WorkflowTemplates"; + +function DiscoverPage() { + return ( +
+ + +
+ ); +} + +export { DiscoverPage }; diff --git a/skyvern-frontend/src/routes/discover/DiscoverPageLayout.tsx b/skyvern-frontend/src/routes/discover/DiscoverPageLayout.tsx new file mode 100644 index 00000000..39492e29 --- /dev/null +++ b/skyvern-frontend/src/routes/discover/DiscoverPageLayout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom"; + +function DiscoverPageLayout() { + return ( +
+
+ +
+
+ ); +} + +export { DiscoverPageLayout }; diff --git a/skyvern-frontend/src/routes/discover/TemporaryTemplateImages.ts b/skyvern-frontend/src/routes/discover/TemporaryTemplateImages.ts new file mode 100644 index 00000000..1c351eb8 --- /dev/null +++ b/skyvern-frontend/src/routes/discover/TemporaryTemplateImages.ts @@ -0,0 +1,6 @@ +import jobApplicationImage from "@/assets/job-application.png"; + +export const TEMPORARY_TEMPLATE_IMAGES: Record = { + wpid_353862309074493424: jobApplicationImage, + wpid_351487857063054716: jobApplicationImage, +}; diff --git a/skyvern-frontend/src/routes/discover/WorkflowTemplateCard.tsx b/skyvern-frontend/src/routes/discover/WorkflowTemplateCard.tsx new file mode 100644 index 00000000..5c38e590 --- /dev/null +++ b/skyvern-frontend/src/routes/discover/WorkflowTemplateCard.tsx @@ -0,0 +1,30 @@ +type Props = { + title: string; + image: string; + onClick: () => void; +}; + +function WorkflowTemplateCard({ title, image, onClick }: Props) { + return ( +
+
+ {title} +
+
+

+ {title} +

+

Template

+
+
+ ); +} + +export { WorkflowTemplateCard }; diff --git a/skyvern-frontend/src/routes/discover/WorkflowTemplates.tsx b/skyvern-frontend/src/routes/discover/WorkflowTemplates.tsx new file mode 100644 index 00000000..6f5b4e25 --- /dev/null +++ b/skyvern-frontend/src/routes/discover/WorkflowTemplates.tsx @@ -0,0 +1,50 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { useGlobalWorkflowsQuery } from "../workflows/hooks/useGlobalWorkflowsQuery"; +import { useNavigate } from "react-router-dom"; +import { WorkflowTemplateCard } from "./WorkflowTemplateCard"; +import testImg from "@/assets/promptBoxBg.png"; +import { TEMPORARY_TEMPLATE_IMAGES } from "./TemporaryTemplateImages"; + +function WorkflowTemplates() { + const { data: workflowTemplates, isLoading } = useGlobalWorkflowsQuery(); + const navigate = useNavigate(); + + if (isLoading) { + return ( +
+ + + +
+ ); + } + + if (!workflowTemplates) { + return null; + } + + return ( +
+

Start Simple

+
+ {workflowTemplates.map((workflow) => { + return ( + { + navigate(`/workflows/${workflow.workflow_permanent_id}/edit`); + }} + /> + ); + })} +
+
+ ); +} + +export { WorkflowTemplates }; diff --git a/skyvern-frontend/src/routes/history/HistoryPage.tsx b/skyvern-frontend/src/routes/history/HistoryPage.tsx new file mode 100644 index 00000000..cc9e7a06 --- /dev/null +++ b/skyvern-frontend/src/routes/history/HistoryPage.tsx @@ -0,0 +1,11 @@ +import { RunHistory } from "./RunHistory"; + +function HistoryPage() { + return ( +
+ +
+ ); +} + +export { HistoryPage }; diff --git a/skyvern-frontend/src/routes/history/HistoryPageLayout.tsx b/skyvern-frontend/src/routes/history/HistoryPageLayout.tsx new file mode 100644 index 00000000..a3d27973 --- /dev/null +++ b/skyvern-frontend/src/routes/history/HistoryPageLayout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom"; + +function HistoryPageLayout() { + return ( +
+
+ +
+
+ ); +} + +export { HistoryPageLayout }; diff --git a/skyvern-frontend/src/routes/history/RunHistory.tsx b/skyvern-frontend/src/routes/history/RunHistory.tsx new file mode 100644 index 00000000..30367aa7 --- /dev/null +++ b/skyvern-frontend/src/routes/history/RunHistory.tsx @@ -0,0 +1,170 @@ +import { Status, TaskApiResponse, WorkflowRunApiResponse } from "@/api/types"; +import { StatusBadge } from "@/components/StatusBadge"; +import { StatusFilterDropdown } from "@/components/StatusFilterDropdown"; +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNext, + PaginationPrevious, +} from "@/components/ui/pagination"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { useRunsQuery } from "@/hooks/useRunsQuery"; +import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat"; +import { cn } from "@/util/utils"; +import { useState } from "react"; +import { useSearchParams, useNavigate } from "react-router-dom"; + +function isTaskApiResponse( + run: TaskApiResponse | WorkflowRunApiResponse, +): run is TaskApiResponse { + return "task_id" in run; +} + +function RunHistory() { + const [searchParams, setSearchParams] = useSearchParams(); + const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; + const [statusFilters, setStatusFilters] = useState>([]); + const { data: runs, isFetching } = useRunsQuery({ page, statusFilters }); + const navigate = useNavigate(); + + function handleNavigate(event: React.MouseEvent, path: string) { + if (event.ctrlKey || event.metaKey) { + window.open( + window.location.origin + path, + "_blank", + "noopener,noreferrer", + ); + } else { + navigate(path); + } + } + return ( +
+
+

Run History

+ +
+
+ + + + + Type + + Run ID + Status + + Created At + + + + + {isFetching + ? Array.from({ length: 10 }).map((_, index) => ( + + + + + + )) + : null} + {!isFetching && runs?.length === 0 ? ( + + +
No runs found
+
+
+ ) : null} + {runs?.map((run) => { + if (isTaskApiResponse(run)) { + return ( + { + handleNavigate(event, `/tasks/${run.task_id}/actions`); + }} + > + Task + {run.task_id} + + + + + {basicLocalTimeFormat(run.created_at)} + + + ); + } + return ( + { + handleNavigate( + event, + `/workflows/${run.workflow_permanent_id}/${run.workflow_run_id}/overview`, + ); + }} + > + Workflow + {run.workflow_run_id} + + + + + {basicLocalTimeFormat(run.created_at)} + + + ); + })} +
+
+ + + + { + if (page === 1) { + return; + } + const params = new URLSearchParams(); + params.set("page", String(Math.max(1, page - 1))); + setSearchParams(params, { replace: true }); + }} + /> + + + {page} + + + { + const params = new URLSearchParams(); + params.set("page", String(page + 1)); + setSearchParams(params, { replace: true }); + }} + /> + + + +
+
+ ); +} + +export { RunHistory }; diff --git a/skyvern-frontend/src/routes/root/SideNav.tsx b/skyvern-frontend/src/routes/root/SideNav.tsx index 9a6be348..2d5cd09b 100644 --- a/skyvern-frontend/src/routes/root/SideNav.tsx +++ b/skyvern-frontend/src/routes/root/SideNav.tsx @@ -1,8 +1,12 @@ -import { RobotIcon } from "@/components/icons/RobotIcon"; +import { CompassIcon } from "@/components/icons/CompassIcon"; import { NavLinkGroup } from "@/components/NavLinkGroup"; import { useSidebarStore } from "@/store/SidebarStore"; import { cn } from "@/util/utils"; -import { GearIcon, LightningBoltIcon } from "@radix-ui/react-icons"; +import { + CounterClockwiseClockIcon, + GearIcon, + LightningBoltIcon, +} from "@radix-ui/react-icons"; function SideNav() { const { collapsed } = useSidebarStore(); @@ -14,18 +18,23 @@ function SideNav() { })} > , + label: "Discover", + to: "/discover", + icon: , }, { label: "Workflows", to: "/workflows", icon: , }, + { + label: "History", + to: "/history", + icon: , + }, ]} />
-
{ - navigate("/tasks/create/blank"); - }} - > - - Build Your Own -
{exampleCases.map((example) => { return ( { - const client = await getClient(credentialGetter); - const yaml = convertToYAML(workflow); - return client.post( - "/workflows", - yaml, - { - headers: { - "Content-Type": "text/plain", - }, - }, - ); - }, - onSuccess: (response) => { - queryClient.invalidateQueries({ - queryKey: ["workflows"], - }); - navigate(`/workflows/${response.data.workflow_permanent_id}/edit`); - }, - }); + const createWorkflowMutation = useCreateWorkflowMutation(); const deleteWorkflowMutation = useMutation({ mutationFn: async (id: string) => { diff --git a/skyvern-frontend/src/routes/workflows/Workflows.tsx b/skyvern-frontend/src/routes/workflows/Workflows.tsx index 8bda5a35..638d2752 100644 --- a/skyvern-frontend/src/routes/workflows/Workflows.tsx +++ b/skyvern-frontend/src/routes/workflows/Workflows.tsx @@ -1,7 +1,6 @@ import { getClient } from "@/api/AxiosClient"; -import { Status, WorkflowRunApiResponse } from "@/api/types"; -import { StatusBadge } from "@/components/StatusBadge"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Pagination, PaginationContent, @@ -25,46 +24,51 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; -import { downloadBlob } from "@/util/downloadBlob"; import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat"; import { cn } from "@/util/utils"; import { - DownloadIcon, + LightningBoltIcon, MagnifyingGlassIcon, Pencil2Icon, PlayIcon, + PlusIcon, + ReloadIcon, } from "@radix-ui/react-icons"; import { useQuery } from "@tanstack/react-query"; -import { useNavigate, useSearchParams } from "react-router-dom"; -import { WorkflowApiResponse } from "./types/workflowTypes"; -import { WorkflowActions } from "./WorkflowActions"; -import { WorkflowsPageBanner } from "./WorkflowsPageBanner"; -import { WorkflowTitle } from "./WorkflowTitle"; import { useState } from "react"; -import { StatusFilterDropdown } from "@/components/StatusFilterDropdown"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { useDebounce } from "use-debounce"; -import { Input } from "@/components/ui/input"; +import { NarrativeCard } from "./components/header/NarrativeCard"; +import { useCreateWorkflowMutation } from "./hooks/useCreateWorkflowMutation"; +import { ImportWorkflowButton } from "./ImportWorkflowButton"; +import { WorkflowApiResponse } from "./types/workflowTypes"; +import { WorkflowCreateYAMLRequest } from "./types/workflowYamlTypes"; +import { WorkflowActions } from "./WorkflowActions"; + +const emptyWorkflowRequest: WorkflowCreateYAMLRequest = { + title: "New Workflow", + description: "", + workflow_definition: { + blocks: [], + parameters: [], + }, +}; function Workflows() { const credentialGetter = useCredentialGetter(); const navigate = useNavigate(); + const createWorkflowMutation = useCreateWorkflowMutation(); const [searchParams, setSearchParams] = useSearchParams(); - const [statusFilters, setStatusFilters] = useState>([]); const [search, setSearch] = useState(""); const [debouncedSearch] = useDebounce(search, 500); - const workflowsPage = searchParams.get("workflowsPage") - ? Number(searchParams.get("workflowsPage")) - : 1; - const workflowRunsPage = searchParams.get("workflowRunsPage") - ? Number(searchParams.get("workflowRunsPage")) - : 1; + const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1; const { data: workflows, isLoading } = useQuery>({ - queryKey: ["workflows", debouncedSearch, workflowsPage], + queryKey: ["workflows", debouncedSearch, page], queryFn: async () => { const client = await getClient(credentialGetter); const params = new URLSearchParams(); - params.append("page", String(workflowsPage)); + params.append("page", String(page)); params.append("only_workflows", "true"); params.append("title", debouncedSearch); return client @@ -75,52 +79,6 @@ function Workflows() { }, }); - const { data: workflowRuns, isLoading: workflowRunsIsLoading } = useQuery< - Array - >({ - queryKey: ["workflowRuns", { statusFilters }, workflowRunsPage], - queryFn: async () => { - const client = await getClient(credentialGetter); - const params = new URLSearchParams(); - params.append("page", String(workflowRunsPage)); - statusFilters.forEach((status) => { - params.append("status", status); - }); - return client - .get("/workflows/runs", { - params, - }) - .then((response) => response.data); - }, - refetchOnMount: "always", - }); - - function handleExport() { - if (!workflowRuns) { - return; // should never happen - } - const data = ["workflow_run_id,workflow_id,status,created,failure_reason"]; - workflowRuns.forEach((workflowRun) => { - const row = [ - workflowRun.workflow_run_id, - workflowRun.workflow_permanent_id, - workflowRun.status, - workflowRun.created_at, - workflowRun.failure_reason ?? "", - ]; - data.push( - row - .map(String) // convert every value to String - .map((v) => v.replace(new RegExp('"', "g"), '""')) // escape double quotes - .map((v) => `"${v}"`) // quote it - .join(","), // comma-separated - ); - }); - const contents = data.join("\r\n"); - - downloadBlob(contents, "export.csv", "data:text/csv;charset=utf-8;"); - } - function handleRowClick( event: React.MouseEvent, workflowPermanentId: string, @@ -152,11 +110,38 @@ function Workflows() { } return ( -
- +
+
+
+
+ +

Workflows

+
+

+ Create your own complex workflows by connecting web agents together. + Define a series of actions, set it, and forget it. +

+
+
+ + + +
+
-
-

Workflows

+
+

My Flows

+
+
@@ -170,15 +155,35 @@ function Workflows() { className="w-48 pl-9 lg:w-72" />
-
-
+
+ + +
+
+
- + - ID - Title - Created At - + + ID + + Title + + Created At + + @@ -272,152 +277,25 @@ function Workflows() { { - if (workflowsPage === 1) { + if (page === 1) { return; } const params = new URLSearchParams(); - params.set( - "workflowsPage", - String(Math.max(1, workflowsPage - 1)), - ); + params.set("page", String(Math.max(1, page - 1))); setSearchParams(params, { replace: true }); }} /> - {workflowsPage} + {page} { const params = new URLSearchParams(); - params.set("workflowsPage", String(workflowsPage + 1)); - setSearchParams(params, { replace: true }); - }} - /> - - - - - -
-
-
-

Workflow Runs

-
- - -
-
-
-
-
- - - Workflow Run ID - Workflow ID - Workflow Title - Status - Created At - - - - {workflowRunsIsLoading ? ( - - Loading... - - ) : workflowRuns?.length === 0 ? ( - - No workflow runs found - - ) : ( - workflowRuns?.map((workflowRun) => { - return ( - { - if (event.ctrlKey || event.metaKey) { - window.open( - window.location.origin + - `/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}/overview`, - "_blank", - "noopener,noreferrer", - ); - return; - } - navigate( - `/workflows/${workflowRun.workflow_permanent_id}/${workflowRun.workflow_run_id}/overview`, - ); - }} - className="cursor-pointer" - > - - {workflowRun.workflow_run_id} - - - {workflowRun.workflow_permanent_id} - - - - - - - - - {basicLocalTimeFormat(workflowRun.created_at)} - - - ); - }) - )} - -
- - - - { - if (workflowRunsPage === 1) { - return; - } - const params = new URLSearchParams(); - params.set( - "workflowRunsPage", - String(Math.max(1, workflowRunsPage - 1)), - ); - setSearchParams(params, { replace: true }); - }} - /> - - - {workflowRunsPage} - - - { - const params = new URLSearchParams(); - params.set( - "workflowRunsPage", - String(workflowRunsPage + 1), - ); + params.set("page", String(page + 1)); setSearchParams(params, { replace: true }); }} /> diff --git a/skyvern-frontend/src/routes/workflows/WorkflowsPageBanner.tsx b/skyvern-frontend/src/routes/workflows/WorkflowsPageBanner.tsx index 30cf12ff..902e909b 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowsPageBanner.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowsPageBanner.tsx @@ -1,13 +1,8 @@ import { Button } from "@/components/ui/button"; -import { ImportWorkflowButton } from "./ImportWorkflowButton"; -import { useNavigate } from "react-router-dom"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { getClient } from "@/api/AxiosClient"; -import { useCredentialGetter } from "@/hooks/useCredentialGetter"; -import { WorkflowCreateYAMLRequest } from "./types/workflowYamlTypes"; -import { stringify as convertToYAML } from "yaml"; -import { WorkflowApiResponse } from "./types/workflowTypes"; import { PlusIcon, ReloadIcon } from "@radix-ui/react-icons"; +import { useCreateWorkflowMutation } from "./hooks/useCreateWorkflowMutation"; +import { ImportWorkflowButton } from "./ImportWorkflowButton"; +import { WorkflowCreateYAMLRequest } from "./types/workflowYamlTypes"; const emptyWorkflowRequest: WorkflowCreateYAMLRequest = { title: "New Workflow", @@ -19,30 +14,7 @@ const emptyWorkflowRequest: WorkflowCreateYAMLRequest = { }; function WorkflowsPageBanner() { - const navigate = useNavigate(); - const credentialGetter = useCredentialGetter(); - const queryClient = useQueryClient(); - - const createNewWorkflowMutation = useMutation({ - mutationFn: async () => { - const client = await getClient(credentialGetter); - const yaml = convertToYAML(emptyWorkflowRequest); - return client.post< - typeof emptyWorkflowRequest, - { data: WorkflowApiResponse } - >("/workflows", yaml, { - headers: { - "Content-Type": "text/plain", - }, - }); - }, - onSuccess: (response) => { - queryClient.invalidateQueries({ - queryKey: ["workflows"], - }); - navigate(`/workflows/${response.data.workflow_permanent_id}/edit`); - }, - }); + const createNewWorkflowMutation = useCreateWorkflowMutation(); return (
@@ -54,7 +26,7 @@ function WorkflowsPageBanner() { - - Save - - - - + {isGlobalWorkflow ? ( + + ) : ( + <> + + + + + + Save + + + + + + )}
); diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx index c4aa4e48..e183096d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoopNode/LoopNode.tsx @@ -55,6 +55,14 @@ function LoopNode({ id, data }: NodeProps) { (furthestDownChild?.position.y ?? 0) + 24; + function handleChange(key: string, value: unknown) { + if (!data.editable) { + return; + } + setInputs({ ...inputs, [key]: value }); + updateNodeData(id, { [key]: value }); + } + return (
) { nodeId={id} value={inputs.loopVariableReference} onChange={(value) => { - setInputs({ - ...inputs, - loopVariableReference: value, - }); - updateNodeData(id, { loopVariableReference: value }); + handleChange("loopVariableReference", value); }} />
@@ -139,9 +143,7 @@ function LoopNode({ id, data }: NodeProps) { checked={data.completeIfEmpty} disabled={!data.editable} onCheckedChange={(checked) => { - updateNodeData(id, { - completeIfEmpty: checked, - }); + handleChange("completeIfEmpty", checked); }} />
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx index 6b6f01a5..7cd673d2 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx @@ -30,6 +30,9 @@ function StartNode({ id, data }: NodeProps) { }); function handleChange(key: string, value: unknown) { + if (!data.editable) { + return; + } setInputs({ ...inputs, [key]: value }); updateNodeData(id, { [key]: value }); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts index c9a495be..f0c4ef20 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/types.ts @@ -7,10 +7,12 @@ export type WorkflowStartNodeData = { webhookCallbackUrl: string; proxyLocation: ProxyLocation; persistBrowserSession: boolean; + editable: boolean; }; export type OtherStartNodeData = { withWorkflowSettings: false; + editable: boolean; }; export type StartNodeData = WorkflowStartNodeData | OtherStartNodeData; diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index deb3ebde..7e186337 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -172,6 +172,7 @@ function layout( function convertToNode( identifiers: { id: string; parentId?: string }, block: WorkflowBlock, + editable: boolean, ): AppNode { const common = { draggable: false, @@ -181,7 +182,7 @@ function convertToNode( const commonData: NodeBaseData = { label: block.label, continueOnFailure: block.continue_on_failure, - editable: true, + editable, }; switch (block.block_type) { case "task": { @@ -590,6 +591,7 @@ export function nodeAdderNode(id: string, parentId?: string): NodeAdderNode { function getElements( blocks: Array, settings: WorkflowSettings, + editable: boolean, ): { nodes: Array; edges: Array; @@ -605,6 +607,7 @@ function getElements( persistBrowserSession: settings.persistBrowserSession, proxyLocation: settings.proxyLocation ?? ProxyLocation.Residential, webhookCallbackUrl: settings.webhookCallbackUrl ?? "", + editable, }), ); @@ -615,6 +618,7 @@ function getElements( parentId: d.parentId ?? undefined, }, d.block, + editable, ); nodes.push(node); if (d.previous) { @@ -633,6 +637,7 @@ function getElements( startNodeId, { withWorkflowSettings: false, + editable, }, block.id, ), diff --git a/skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts b/skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts new file mode 100644 index 00000000..c129b241 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/hooks/useCreateWorkflowMutation.ts @@ -0,0 +1,37 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useMutation } from "@tanstack/react-query"; +import { WorkflowCreateYAMLRequest } from "../types/workflowYamlTypes"; +import { useCredentialGetter } from "@/hooks/useCredentialGetter"; +import { getClient } from "@/api/AxiosClient"; +import { useNavigate } from "react-router-dom"; +import { stringify as convertToYAML } from "yaml"; +import { WorkflowApiResponse } from "../types/workflowTypes"; + +function useCreateWorkflowMutation() { + const queryClient = useQueryClient(); + const credentialGetter = useCredentialGetter(); + const navigate = useNavigate(); + return useMutation({ + mutationFn: async (workflow: WorkflowCreateYAMLRequest) => { + const client = await getClient(credentialGetter); + const yaml = convertToYAML(workflow); + return client.post( + "/workflows", + yaml, + { + headers: { + "Content-Type": "text/plain", + }, + }, + ); + }, + onSuccess: (response) => { + queryClient.invalidateQueries({ + queryKey: ["workflows"], + }); + navigate(`/workflows/${response.data.workflow_permanent_id}/edit`); + }, + }); +} + +export { useCreateWorkflowMutation }; diff --git a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowQuery.ts index cb850790..9563bba6 100644 --- a/skyvern-frontend/src/routes/workflows/hooks/useWorkflowQuery.ts +++ b/skyvern-frontend/src/routes/workflows/hooks/useWorkflowQuery.ts @@ -3,6 +3,7 @@ import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { useQuery } from "@tanstack/react-query"; import { WorkflowApiResponse } from "../types/workflowTypes"; import { useGlobalWorkflowsQuery } from "./useGlobalWorkflowsQuery"; + type Props = { workflowPermanentId?: string; };