diff --git a/skyvern-frontend/src/assets/job-application.png b/skyvern-frontend/src/assets/job-application.png new file mode 100644 index 00000000..beab14da Binary files /dev/null and b/skyvern-frontend/src/assets/job-application.png differ diff --git a/skyvern-frontend/src/components/icons/CompassIcon.tsx b/skyvern-frontend/src/components/icons/CompassIcon.tsx new file mode 100644 index 00000000..d5538850 --- /dev/null +++ b/skyvern-frontend/src/components/icons/CompassIcon.tsx @@ -0,0 +1,33 @@ +type Props = { + className?: string; +}; + +function CompassIcon({ className }: Props) { + return ( + + + + + ); +} + +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; };