diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx index 273fe3f1..4bfebb27 100644 --- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx +++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx @@ -1,14 +1,19 @@ import { AxiosError } from "axios"; -import { PlayIcon, ReloadIcon } from "@radix-ui/react-icons"; -import { useEffect, useState } from "react"; +import { + ExclamationTriangleIcon, + PlayIcon, + ReloadIcon, +} from "@radix-ui/react-icons"; +import { useEffect, useMemo, useState } from "react"; import { type FieldErrors, useForm } from "react-hook-form"; -import { useNavigate, useParams } from "react-router-dom"; +import { Link, useNavigate, useParams } from "react-router-dom"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { z } from "zod"; import { getClient } from "@/api/AxiosClient"; import { ProxyLocation } from "@/api/types"; import { ProxySelector } from "@/components/ProxySelector"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Accordion, @@ -46,11 +51,55 @@ import { runsApiBaseUrl } from "@/util/env"; import { MAX_SCREENSHOT_SCROLLS_DEFAULT } from "./editor/nodes/Taskv2Node/types"; import { getLabelForWorkflowParameterType } from "./editor/workflowEditorUtils"; -import { WorkflowParameter } from "./types/workflowTypes"; +import { + WorkflowApiResponse, + WorkflowBlock, + WorkflowParameter, +} from "./types/workflowTypes"; import { WorkflowParameterInput } from "./WorkflowParameterInput"; import { TestWebhookDialog } from "@/components/TestWebhookDialog"; import * as env from "@/util/env"; +/** + * Recursively finds all login blocks that don't have any credential parameters selected. + * Checks nested blocks inside for_loop blocks as well. + */ +function getLoginBlocksWithoutCredentials( + blocks: Array, +): Array<{ label: string }> { + const result: Array<{ label: string }> = []; + + for (const block of blocks) { + if (block.block_type === "login") { + // Login block requires at least one parameter (credential) to be selected + if (!block.parameters || block.parameters.length === 0) { + result.push({ label: block.label }); + } + } + + // Check nested blocks in for_loop + if (block.block_type === "for_loop" && block.loop_blocks) { + result.push(...getLoginBlocksWithoutCredentials(block.loop_blocks)); + } + } + + return result; +} + +/** + * Validates the workflow for issues that would prevent it from running. + * Returns an array of login block labels that are missing credentials. + */ +function validateWorkflowForRun( + workflow: WorkflowApiResponse | undefined, +): Array<{ label: string }> { + if (!workflow) { + return []; + } + + return getLoginBlocksWithoutCredentials(workflow.workflow_definition.blocks); +} + // Utility function to omit specified keys from an object function omit, K extends keyof T>( obj: T, @@ -238,6 +287,13 @@ function RunWorkflowForm({ const apiCredential = useApiCredential(); const { data: workflow } = useWorkflowQuery({ workflowPermanentId }); + // Validate login blocks have credentials selected + const loginBlocksWithoutCredentials = useMemo( + () => validateWorkflowForRun(workflow), + [workflow], + ); + const hasLoginBlockValidationError = loginBlocksWithoutCredentials.length > 0; + const form = useForm({ mode: "onTouched", reValidateMode: "onChange", @@ -448,6 +504,33 @@ function RunWorkflowForm({ onSubmit={form.handleSubmit(onSubmit, handleInvalid)} className="space-y-8" > + {hasLoginBlockValidationError && ( + + + Cannot run workflow + +

+ The following login block(s) need a credential selected before + running: +

+
    + {loginBlocksWithoutCredentials.map((block) => ( +
  • {block.label}
  • + ))} +
+

+ + Go to the editor + {" "} + to configure credentials for these blocks. +

+
+
+ )} +

Input Parameters

@@ -996,7 +1079,12 @@ function RunWorkflowForm({ } satisfies ApiCommandOptions; }} /> -