diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx index 1e74be5208..f99fb05c8b 100644 --- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx +++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-overview.tsx @@ -214,7 +214,7 @@ export default function WorkflowOverview({ Involved Services - + )} {!selectedNode?.includes("empty") && !isTrigger && ( - 0 && steps.map((step: any) => (
  • handleDragStart(event, { ...step })} draggable={isDraggable} @@ -125,7 +125,8 @@ const DragAndDropSidebar = ({ isDraggable }: { isDraggable?: boolean }) => { const [searchTerm, setSearchTerm] = useState(""); const [isVisible, setIsVisible] = useState(false); const [open, setOpen] = useState(false); - const { toolboxConfiguration, selectedNode, selectedEdge, nodes } = + const [showinstalled, setShowInstalled] = useState(true); + const { toolboxConfiguration, selectedNode, selectedEdge, nodes, edges } = useStore(); useEffect(() => { @@ -148,17 +149,46 @@ const DragAndDropSidebar = ({ isDraggable }: { isDraggable?: boolean }) => { ); const filteredGroups = useMemo(() => { - return ( + const checkInstalled = (step: V2Step, groupName: string) => { + if (!showinstalled) { + return true; + } + + if (["Conditions", "Misc", "Triggers"].includes(groupName)) { + return true; + } + + return step?.installed; + }; + let finalGroups = toolboxConfiguration?.groups?.map((group: any) => ({ ...group, steps: group?.steps?.filter( (step: any) => + checkInstalled(step, group.name) && step?.name?.toLowerCase().includes(searchTerm?.toLowerCase()) && !triggerNodeMap[step?.id] ), - })) || [] - ); - }, [toolboxConfiguration, searchTerm, nodes?.length]); + })) || []; + const selectedAddEdge = edges.find((edge) => edge.id === selectedEdge); + if (selectedEdge && selectedAddEdge) { + finalGroups = finalGroups.filter( + (group: { name: string }) => + (group?.name == "Triggers" && + selectedAddEdge.source === "trigger_start") || + (selectedAddEdge.source !== "trigger_start" && + group?.name !== "Triggers") + ); + } + return finalGroups; + }, [ + toolboxConfiguration?.groups, + edges, + selectedEdge, + showinstalled, + searchTerm, + triggerNodeMap, + ]); const checkForSearchResults = searchTerm && @@ -178,7 +208,9 @@ const DragAndDropSidebar = ({ isDraggable }: { isDraggable?: boolean }) => {
    {/* Sticky header */}
    -

    Toolbox

    +
    +

    Toolbox

    +
    { {/* Scrollable list */} {(isVisible || checkForSearchResults) && ( -
    - {filteredGroups.length > 0 && - filteredGroups.map((group: Record) => ( - - ))} +
    +
    +
    Installed
    + +
    +
    + {filteredGroups.length > 0 && + filteredGroups.map((group: Record) => ( + + ))} +
    )}
    diff --git a/keep-ui/app/(keep)/workflows/builder/builder-store.tsx b/keep-ui/app/(keep)/workflows/builder/builder-store.tsx index d16d3b2b50..2bfccd9e74 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder-store.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder-store.tsx @@ -47,6 +47,7 @@ export type V2Step = { edgeSource?: string; edgeTarget?: string; notClickable?: boolean; + installed?: boolean; }; export type NodeData = Node["data"] & Record; @@ -74,6 +75,10 @@ export type FlowState = { openGlobalEditor: boolean; stepEditorOpenForNode: string | null; toolboxConfiguration: Record; + stepErrors: Record | null; + globalErrors: Record | null; + setStepErrors: (error: Record | null) => void; + setGlobalErros: (error: Record | null) => void; onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onConnect: OnConnect; @@ -264,6 +269,8 @@ function addNodeBetween( break; } } + //on adding new node. highlight the added node and update the editor + set({selectedNode: newNodeId, stepEditorOpenForNode: newNodeId}) } const useStore = create((set, get) => ({ @@ -282,6 +289,10 @@ const useStore = create((set, get) => ({ errorNode: null, synced: true, canDeploy: false, + stepErrors: null, + globalErrors: null, + setGlobalErros: (errors:Record|null)=>set({globalErrors: errors}), + setStepErrors: (errors:Record|null)=>set({stepErrors: errors}), setCanDeploy: (deploy) => set({ canDeploy: deploy }), setSynced: (sync) => set({ synced: sync }), setErrorNode: (id) => set({ errorNode: id }), diff --git a/keep-ui/app/(keep)/workflows/builder/builder-validators.tsx b/keep-ui/app/(keep)/workflows/builder/builder-validators.tsx index fc04f87eab..b49b92bdeb 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder-validators.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder-validators.tsx @@ -2,79 +2,71 @@ import { Definition as FlowDefinition, ReactFlowDefinition, V2Step, + V2Properties, } from "@/app/(keep)/workflows/builder/types"; +import { + getSchemaByStepType, + getWorkflowPropertiesSchema, +} from "./utils"; export function globalValidatorV2( definition: FlowDefinition, - setGlobalValidationError: (id: string | null, error: string | null) => void + setGlobalValidationError: ( + id: string | null, + error: Record | null + ) => void ): boolean { - const workflowName = definition?.properties?.name; - const workflowDescription = definition?.properties?.description; - if (!workflowName) { - setGlobalValidationError(null, "Workflow name cannot be empty."); - return false; - } - if (!workflowDescription) { - setGlobalValidationError(null, "Workflow description cannot be empty."); - return false; - } - - if ( - !!definition?.properties && - !definition.properties["manual"] && - !definition.properties["interval"] && - !definition.properties["alert"] && - !definition.properties["incident"] - ) { - setGlobalValidationError( - "trigger_start", - "Workflow Should at least have one trigger." - ); - return false; - } - - if ( - definition?.properties && - "interval" in definition.properties && - !definition.properties.interval - ) { - setGlobalValidationError("interval", "Workflow interval cannot be empty."); - return false; - } + const properties = definition?.properties; + const result = getWorkflowPropertiesSchema(properties).safeParse(properties); + const errors = result?.error?.errors; + const errorMap = + errors?.reduce>((obj, error) => { + const path = error.path; + if (path && path[0] && !obj[path[0]]) { + obj[path[0]] = error?.message?.toString(); + } + return obj; + }, {}) || null; - const alertSources = Object.values(definition.properties.alert || {}).filter( - Boolean - ); - if ( - definition?.properties && - definition.properties["alert"] && - alertSources.length == 0 - ) { - setGlobalValidationError( - "alert", - "Workflow alert trigger cannot be empty." - ); + if (!result.success && errorMap) { + switch (true) { + case "interval" in errorMap: + setGlobalValidationError("interval", errorMap); + break; + case "alert" in errorMap: + setGlobalValidationError("alert", errorMap); + break; + case "incident" in errorMap: + setGlobalValidationError("incident", errorMap); + break; + case "manual" in errorMap: + setGlobalValidationError("manual", errorMap); + break; + default: + setGlobalValidationError(null, errorMap); + } return false; } - const incidentActions = Object.values( - definition.properties.incident || {} - ).filter(Boolean); if ( - definition?.properties && - definition.properties["incident"] && - incidentActions.length == 0 + !!properties && + !properties["manual"] && + !properties["interval"] && + !properties["alert"] && + !properties["incident"] ) { - setGlobalValidationError( - "incident", - "Workflow incident trigger cannot be empty." - ); + setGlobalValidationError("trigger_start", { + rule_error: "Workflow Should at least have one trigger.", + }); return false; } const anyStepOrAction = definition?.sequence?.length > 0; if (!anyStepOrAction) { - setGlobalValidationError(null, "At least 1 step/action is required."); + setGlobalValidationError(null, { + rule_error: "At least 1 step/action is required.", + }); + return false; } const anyActionsInMainSequence = ( definition.sequence[0] as V2Step @@ -88,10 +80,9 @@ export function globalValidatorV2( const sequence = definition?.sequence?.[0]?.sequence || []; for (let i = actionIndex + 1; i < sequence.length; i++) { if (sequence[i]?.type?.includes("step-")) { - setGlobalValidationError( - sequence[i].id, - "Steps cannot be placed after actions." - ); + setGlobalValidationError(sequence[i].id, { + rule_error: "Steps cannot be placed after actions.", + }); return false; } } @@ -102,55 +93,135 @@ export function globalValidatorV2( return valid; } +export const getUniqueKeysFromStep = (properties: V2Properties) => { + return [ + ...new Set([ + ...(properties.stepParams || []), + ...(properties.actionParams || []), + ]), + ].filter((val) => val); +}; + +export const getDefaultWith = (uniqueKeys: string[]) => { + return ( + uniqueKeys?.reduce((obj, key) => { + obj[key] = ""; + return obj; + }, {}) || {} + ); +}; + export function stepValidatorV2( step: V2Step, - setStepValidationError: (step: V2Step, error: string | null) => void, + setStepValidationError: ( + step: V2Step, + error: null | Record + ) => void, parentSequence?: V2Step, definition?: ReactFlowDefinition ): boolean { - if (step.type.includes("condition-")) { - if (!step.name) { - setStepValidationError(step, "Step/action name cannot be empty."); + const schema = getSchemaByStepType(step.type); + + if (schema) { + const unqiuekeys = getUniqueKeysFromStep(step.properties); + const defaultWith = getDefaultWith(unqiuekeys); + const result = schema.safeParse({ + ...step, + //Property keys are temporarily created to ensure proper validation and meaningful error messages. + properties: { + ...step.properties, + with: { ...defaultWith, ...(step.properties.with || {}) }, + config: step.properties.config || "", + }, + }); + if (!result.success) { + const errorMap = result.error.errors.reduce>( + (obj, err) => { + const path = err.path.join("."); + if (path && !(path in obj)) { + obj[path] = err.message?.toString(); + } + return obj; + }, + {} + ); + setStepValidationError(step, errorMap); return false; } + } + + if (step.type === "foreach") { + let valid = true; + const sequences = step.sequence || []; + console.log("enterign thsi foreach", sequences); + + for (let sequence of sequences) { + valid = stepValidatorV2(sequence, setStepValidationError); + if (!valid) { + return false; + } + } + return valid; + } + + //TO DO: move this to zod validations + if (step.type.includes("condition-")) { const branches = (step?.branches || { true: [], false: [], }) as V2Step["branches"]; + + const trueBranches = branches?.true || []; + const falseBranches = branches?.false || []; const onlyActions = branches?.true?.every((step: V2Step) => step.type.includes("action-") ); if (!onlyActions) { - setStepValidationError(step, "Conditions can only contain actions."); + setStepValidationError(step, { + rule_error: "Conditions can only contain actions.", + }); return false; } + const conditionHasActions = branches?.true ? branches?.true.length > 0 : false; if (!conditionHasActions) - setStepValidationError( - step, - "Conditions must contain at least one action." - ); - const valid = conditionHasActions && onlyActions; + setStepValidationError(step, { + rule_error: "Conditions must contain at least one action.", + }); + let valid = conditionHasActions && onlyActions; if (valid) setStepValidationError(step, null); + + for (let branch of trueBranches) { + valid = stepValidatorV2(branch, setStepValidationError); + if (!valid) { + return false; + } + } + + for (let branch of falseBranches) { + valid = stepValidatorV2(branch, setStepValidationError); + if (!valid) { + return false; + } + } return valid; } + if (step?.componentType === "task") { - const valid = step?.name !== ""; - if (!valid) setStepValidationError(step, "Step name cannot be empty."); if (!step?.properties?.with) { - setStepValidationError( - step, - "There is step/action with no parameters configured!" - ); + setStepValidationError(step, { + rule_error: "Conditions must contain at least one action.", + }); return false; } - if (valid && step?.properties?.with) { + if (step?.properties?.with) { setStepValidationError(step, null); } - return valid; + return true; } + setStepValidationError(step, null); return true; } diff --git a/keep-ui/app/(keep)/workflows/builder/builder.tsx b/keep-ui/app/(keep)/workflows/builder/builder.tsx index 8f6553f068..53649826ee 100644 --- a/keep-ui/app/(keep)/workflows/builder/builder.tsx +++ b/keep-ui/app/(keep)/workflows/builder/builder.tsx @@ -97,11 +97,23 @@ function Builder({ const [compiledAlert, setCompiledAlert] = useState(null); const searchParams = useSearchParams(); - const { errorNode, setErrorNode, canDeploy, synced } = useStore(); - - const setStepValidationErrorV2 = (step: V2Step, error: string | null) => { - setStepValidationError(error); - if (error && step) { + const { errorNode, + setErrorNode, + canDeploy, + synced, + setSelectedNode, + setStepErrors, + setGlobalErros, } = useStore(); + const setStepValidationErrorV2 = ( + step: V2Step, + error: Record | null + ) => { + const finalmessage = error ? Object.values(error).join(",") : null; + //though it is redundant. for now keeping it + setStepValidationError(finalmessage); + setStepErrors(error); + if (finalmessage && step) { + setSelectedNode(step.id); return setErrorNode(step.id); } setErrorNode(null); @@ -109,10 +121,15 @@ function Builder({ const setGlobalValidationErrorV2 = ( id: string | null, - error: string | null + error: Record | null ) => { - setGlobalValidationError(error); - if (error && id) { + const finalmessage = error ? Object.values(error).join(",") : null; + //though it is redundant. for now keeping it + setGlobalValidationError(finalmessage); + setGlobalErros(error); + + if (finalmessage && id) { + setSelectedNode(id); return setErrorNode(id); } setErrorNode(null); @@ -277,7 +294,7 @@ function Builder({ stepValidationError, globalValidationError, enableGenerate, - definition.isValid, + definition?.isValid, ]); if (isLoading) { diff --git a/keep-ui/app/(keep)/workflows/builder/editors.tsx b/keep-ui/app/(keep)/workflows/builder/editors.tsx index 8351f7693b..10a2b96c60 100644 --- a/keep-ui/app/(keep)/workflows/builder/editors.tsx +++ b/keep-ui/app/(keep)/workflows/builder/editors.tsx @@ -19,13 +19,53 @@ import { } from "@heroicons/react/24/outline"; import React from "react"; import useStore from "./builder-store"; -import { useEffect, useRef, useState } from "react"; -import { V2Properties } from "@/app/(keep)/workflows/builder/types"; +import { useMemo, useEffect, useRef, useState } from "react"; +import { V2Properties, FlowNode } from "@/app/(keep)/workflows/builder/types"; +import { + useForm, + Controller, + FieldValues, + SubmitHandler, +} from "react-hook-form"; +import { toast } from "react-toastify"; +import { + methodOptions, + requiredMap, + getSchemaByStepType, + FormData, +} from "./utils"; +import debounce from "lodash.debounce"; +import Loading from "../../loading"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +interface keepEditorProps { + properties: V2Properties; + updateProperty: (key: string, value: any, onlySync?: boolean) => void; + providers?: Provider[] | null | undefined; + installedProviders?: Provider[] | null | undefined; + providerType?: string; + type?: string; + isV2?: boolean; +} +type keepEditorPropsV2 = keepEditorProps & { + control: any; + errors: any; + register: any; +}; + +interface KeyValue { + key: string; + value: string; +} + function EditorLayout({ children }: { children: React.ReactNode }) { return
    {children}
    ; } + + export function GlobalEditorV2({ synced, saveRef, @@ -63,269 +103,108 @@ export function GlobalEditorV2({ ); } -interface keepEditorProps { - properties: V2Properties; - updateProperty: (key: string, value: any) => void; - providers?: Provider[] | null | undefined; - installedProviders?: Provider[] | null | undefined; - providerType?: string; - type?: string; - isV2?: boolean; -} -function KeepStepEditor({ +function KeepThresholdConditionEditorV2({ properties, updateProperty, - installedProviders, - providers, - providerType, - type, -}: keepEditorProps) { - const stepParams = - ((type?.includes("step-") - ? properties.stepParams - : properties.actionParams) as string[]) ?? []; - const existingParams = Object.keys((properties.with as object) ?? {}); - const params = [...stepParams, ...existingParams]; - const uniqueParams = params.filter( - (item, pos) => params.indexOf(item) === pos - ); - - function propertyChanged(e: any) { - const currentWith = (properties.with as object) ?? {}; - updateProperty("with", { ...currentWith, [e.target.id]: e.target.value }); - } - - const providerConfig = (properties.config as string)?.trim(); - const installedProviderByType = installedProviders?.filter( - (p) => p.type === providerType - ); - const isThisProviderNeedsInstallation = - providers?.some( - (p) => - p.type === providerType && p.config && Object.keys(p.config).length > 0 - ) ?? false; - - const DynamicIcon = (props: any) => ( - - {" "} - - - ); - + errors, + register, +}: keepEditorPropsV2 & {}) { + const currentValueValue = (properties.value as string) ?? ""; + const currentCompareToValue = (properties.compare_to as string) ?? ""; return ( <> - Provider Name - - Or - updateProperty("config", e.target.value)} - className="my-2.5" - value={providerConfig || ""} - error={ - providerConfig !== "" && - providerConfig !== undefined && - isThisProviderNeedsInstallation && - installedProviderByType?.find( - (p) => p.details?.name === providerConfig - ) === undefined - } - errorMessage={`${ - providerConfig && - isThisProviderNeedsInstallation && - installedProviderByType?.find( - (p) => p.details?.name === providerConfig - ) === undefined - ? "Please note this provider is not installed and you'll need to install it before executing this workflow." - : "" - }`} - /> - Provider Parameters
    - If + Value updateProperty("if", value)} + {...register("properties.value")} + placeholder="Value" className="mb-2.5" - value={properties?.if || ("" as string)} + onValueChange={(value) => { + updateProperty("refresh", value, true); + }} /> + {errors?.properties?.value && ( +
    + {errors.properties.value.message?.toString()} +
    + )}
    - Vars - {Object.entries(properties?.vars || {}).map(([varKey, varValue]) => ( -
    - { - const updatedVars = { - ...(properties.vars as { [key: string]: string }), - }; - delete updatedVars[varKey]; - updatedVars[e.target.value] = varValue as string; - updateProperty("vars", updatedVars); - }} - /> - { - const updatedVars = { - ...(properties.vars as { [key: string]: string }), - }; - updatedVars[varKey] = e.target.value; - updateProperty("vars", updatedVars); - }} - /> - { - const updatedVars = { - ...(properties.vars as { [key: string]: string }), - }; - delete updatedVars[varKey]; - updateProperty("vars", updatedVars); - }} - /> -
    - ))} - + /> + {errors?.properties?.compare_to && ( +
    + {errors.properties.compare_to.message?.toString()} +
    + )}
    - {uniqueParams - ?.filter((key) => key !== "kwargs") - .map((key, index) => { - let currentPropertyValue = ((properties.with as any) ?? {})[key]; - if (typeof currentPropertyValue === "object") { - currentPropertyValue = JSON.stringify(currentPropertyValue); - } - return ( -
    - {key} - -
    - ); - })} - - ); -} - -function KeepThresholdConditionEditor({ - properties, - updateProperty, -}: keepEditorProps) { - const currentValueValue = (properties.value as string) ?? ""; - const currentCompareToValue = (properties.compare_to as string) ?? ""; - return ( - <> - Value - updateProperty("value", e.target.value)} - className="mb-2.5" - value={currentValueValue} - /> - Compare to - updateProperty("compare_to", e.target.value)} - className="mb-2.5" - value={currentCompareToValue} - /> ); } -function KeepAssertConditionEditor({ +function KeepAssertConditionEditorV2({ properties, updateProperty, -}: keepEditorProps) { + control, + errors, + register, +}: keepEditorPropsV2) { const currentAssertValue = (properties.assert as string) ?? ""; return ( - <> +
    Assert updateProperty("assert", e.target.value)} className="mb-2.5" - value={currentAssertValue} + onValueChange={(value) => { + updateProperty("refresh", value, true); + }} /> - + + {errors?.properties?.assert && ( +
    + {errors.properties.assert.message?.toString()} +
    + )} +
    ); } -function KeepForeachEditor({ properties, updateProperty }: keepEditorProps) { +function KeepForeachEditorV2({ + properties, + updateProperty, + control, + errors, + register, +}: keepEditorPropsV2) { const currentValueValue = (properties.value as string) ?? ""; return ( - <> +
    Foreach Value updateProperty("value", e.target.value)} + onValueChange={(value) => { + updateProperty("refresh", value, true); + }} className="mb-2.5" - value={currentValueValue} /> - + {errors?.properties?.value && ( +
    + {errors.properties.value.message?.toString()} +
    + )} +
    ); } @@ -340,6 +219,7 @@ function WorkflowEditorV2({ selectedNode: string | null; saveRef: React.MutableRefObject; }) { + const { globalErrors } = useStore(); const addNewConstant = () => { const updatedConsts = { ...(properties["consts"] as { [key: string]: string }), @@ -387,6 +267,7 @@ function WorkflowEditorV2({ (k) => k !== "isLocked" && k !== "id" ); let renderDivider = false; + const errorMap = globalErrors; return ( <> Workflow Settings @@ -426,6 +307,11 @@ function WorkflowEditorV2({ } disabled={true} /> + {errorMap?.manual && ( +
    + {errorMap?.manual} +
    + )}
    ) ); @@ -477,6 +363,11 @@ function WorkflowEditorV2({ ); })} + {errorMap?.alert && ( +
    + {errorMap?.alert} +
    + )} ) ); @@ -521,6 +412,11 @@ function WorkflowEditorV2({
    ))} + {errorMap?.incident && ( +
    + {errorMap?.incident} +
    + )} ) ); @@ -531,6 +427,8 @@ function WorkflowEditorV2({ placeholder={`Set the ${key}`} onChange={(e: any) => handleChange(key, e.target.value)} value={properties[key] || ("" as string)} + error={!!errorMap?.interval} + errorMessage={errorMap?.interval} /> ) ); @@ -544,6 +442,11 @@ function WorkflowEditorV2({ handleChange(key, e.target.checked ? "true" : "false") } /> + {errorMap?.disabled && ( +
    + {errorMap?.disabled} +
    + )}
    ); case "consts": @@ -619,6 +522,8 @@ function WorkflowEditorV2({ placeholder={`Set the ${key}`} onChange={(e: any) => handleChange(key, e.target.value)} value={properties[key] || ("" as string)} + error={!!errorMap?.[key]} + errorMessage={errorMap?.[key]} /> ); } @@ -630,7 +535,79 @@ function WorkflowEditorV2({ ); } -export function StepEditorV2({ +const useCustomForm = () => { + const { selectedNode, updateSelectedNodeData, getNodeById } = useStore(); + const [nodeData, setNodeData] = useState(null); + + const { + control, + handleSubmit, + setValue, + getValues, + register, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(getSchemaByStepType(nodeData?.data?.type)), // Default standard schema + defaultValues: { + name: "", + properties: { + with: {}, + vars: {}, + stepParams: [], + actionParams: [], + if: "", + } as V2Properties, + type: "", + }, + }); + + useEffect(() => { + const node = getNodeById(selectedNode); + + if (node) { + const { name, type, properties } = node?.data || {}; + setNodeData(node); + + // Get the new schema based on the node type + const newSchema = getSchemaByStepType(type as string) || z.object({}); + + // Reset the form with the new resolver and values + reset( + { + name: name || "", + type: type || "", + properties: properties || { + with: {}, + vars: {}, + stepParams: [], + actionParams: [], + if: "", + }, + }, + { keepDefaultValues: false, keepDirty: false } + ); + + // Dynamically set the new resolver + control._options.resolver = zodResolver(newSchema); + } + }, [selectedNode, getNodeById, reset, control]); + + return { + control, + errors, + setValue, + handleSubmit, + getValues, + nodeData, + selectedNode, + updateSelectedNodeData, + register, + loading: selectedNode !== nodeData?.id, + }; +}; + +export function StepEditorV3({ providers, installedProviders, setSynced, @@ -641,103 +618,549 @@ export function StepEditorV2({ setSynced: (sync: boolean) => void; saveRef: React.MutableRefObject; }) { - const [formData, setFormData] = useState<{ - name?: string; - properties?: V2Properties; - type?: string; - }>({}); - const { selectedNode, updateSelectedNodeData, getNodeById } = useStore(); - const deployRef = useRef(null); - - useEffect(() => { - if (selectedNode) { - const { data } = getNodeById(selectedNode) || {}; - const { name, type, properties } = data || {}; - setFormData({ name, type, properties }); - } - }, [selectedNode, getNodeById]); + const { + control, + handleSubmit, + setValue, + getValues, + errors, + selectedNode, + updateSelectedNodeData, + register, + loading, + } = useCustomForm(); if (!selectedNode) return null; - - const providerType = formData?.type?.split("-")[1]; + if (loading) { + return ; + } const handleInputChange = (e: React.ChangeEvent) => { - setFormData({ ...formData, [e.target.name]: e.target.value }); + setValue(e.target.name, e.target.value); setSynced(false); }; - const handlePropertyChange = (key: string, value: any) => { - setFormData({ - ...formData, - properties: { ...formData.properties, [key]: value }, - }); + const handlePropertyChange = ( + key: string, + value: any, + onlySync?: boolean + ) => { + if (!onlySync) { + const values = getValues(); + setValue("properties", { ...values.properties, [key]: value }); + } setSynced(false); if (saveRef.current) { saveRef.current = false; } }; - const handleSubmit = () => { + const values = getValues(); + + const type = values.type; + const providerType = type?.split("-")[1]; + const properties = values.properties; + + const onSubmit: SubmitHandler = async (data) => { + const { name, properties } = data || {}; // Finalize the changes before saving - updateSelectedNodeData("name", formData.name); - updateSelectedNodeData("properties", formData.properties); + updateSelectedNodeData("name", name || ""); + updateSelectedNodeData("properties", properties); setSynced(false); if (saveRef && deployRef?.current?.checked) { + toast("Deploying the Changes", { + position: "top-right", + type: "success", + }); saveRef.current = true; + } else { + toast("Properties Successfully Saved ", { + position: "top-right", + type: "success", + }); } }; - const type = formData - ? formData.type?.includes("step-") || formData.type?.includes("action-") - : ""; + const isStepOrAction = + type?.includes("step-") || type?.includes("action-") || ""; return ( {providerType}1 Editor - Unique Identifier - +
    + Unique Identifier + { + handlePropertyChange("refresh", value, true); + }} + /> + {errors?.name && ( +
    + {errors?.name.message?.toString()} +
    + )} +
    + {isStepOrAction ? ( + + ) : type === "condition-threshold" ? ( + + ) : type?.includes("foreach") ? ( + + ) : type === "condition-assert" ? ( + + ) : null} +
    + Deploy + +
    + + +
    + ); +} + +export function KeyValueForm({ + properties, + initialPairs, + updateProperty, +}: { + properties: any; + initialPairs?: KeyValue[]; + updateProperty: (value: any) => void; +}) { + const [pairs, setPairs] = useState( + initialPairs || ([] as KeyValue[]) + ); + const [error, setError] = useState(""); + + const handleKeyChange = (index: number, newKey: string) => { + const updatedPairs = [...pairs]; + setError(""); + updatedPairs[index].key = newKey; + setPairs(updatedPairs); + }; + + const handleValueChange = (index: number, newValue: string) => { + const updatedPairs = [...pairs]; + updatedPairs[index].value = newValue; + setPairs(updatedPairs); + }; + + const handleAddField = () => { + const lastPair = pairs[pairs.length - 1]; + + // Ensure the last key is filled before adding a new field + if (pairs.length && lastPair?.key?.trim() === "") { + setError("Key cannot be empty."); + return; + } + + setError(""); + setPairs([...pairs, { key: "", value: "" }]); + }; + + useEffect(() => { + const vars = + pairs?.reduce>((obj, pair) => { + if (pair.key) { + obj[pair.key] = pair.value; + } + return obj; + }, {}) || {}; + + const debouncedUpdate = debounce(() => { + updateProperty(vars); + }, 300); + + if ( + properties && + JSON.stringify(properties.vars) !== JSON.stringify(vars) + ) { + debouncedUpdate(); + } + // Cleanup the debounce on unmount or if dependencies change + return () => { + debouncedUpdate.cancel(); + }; + }, [pairs, properties, updateProperty]); + + return pairs ? ( +
    + Vars + {pairs?.map((pair, index) => ( +
    + handleKeyChange(index, value)} + /> + handleValueChange(index, value)} + /> + { + e.preventDefault(); + setPairs([...pairs.slice(0, index), ...pairs.slice(index + 1)]); + }} + /> +
    + ))} + {error &&
    {error}
    } + +
    + ) : null; +} + +function KeepStepEditorV3({ + properties, + updateProperty, + installedProviders, + providers, + type, + control, + errors, + providerType, + register, +}: keepEditorPropsV2 & { register: any }) { + const { stepErrors, selectedNode, errorNode } = useStore(); + const errorKeys = Object.keys(errors || {})?.toString(); + const pickInitialErros = useMemo(()=>{ + if(errorKeys) { + return false; + } + return errorNode === selectedNode; + }, [errorKeys, selectedNode, errorNode]); + + function propertyChanged(key: string, value: any) { + const currentWith = (properties.with as object) ?? {}; + updateProperty("with", { ...currentWith, [key]: value }); + } + const stepParams = + ((type?.includes("step-") + ? properties.stepParams + : properties.actionParams) as string[]) ?? []; + const existingParams = Object.keys((properties.with as object) ?? {}); + const uniqueParams = [...new Set([...stepParams, ...existingParams])]; + + const providerConfig = properties?.config?.trim(); + const installedProviderByType = installedProviders?.filter( + (p) => p.type === providerType + ); + + const isHttpAction = type === "action-http"; + const isThisProviderNeedsInstallation = + (!isHttpAction && + providers?.some( + (p) => + p.type === providerType && + p.config && + Object.keys(p.config).length > 0 + )) ?? + false; + + const tempRequiredKeys = type ? [...(requiredMap[type] || [])] : []; + + const DynamicIcon = (props: any) => ( + + {" "} + - {type && formData.properties ? ( - - ) : formData.type === "condition-threshold" ? ( - - ) : formData.type?.includes("foreach") ? ( - - ) : formData.type === "condition-assert" ? ( - + ); + + const varPairs = Object.entries(properties?.vars || {}).map( + ([key, value]) => ({ key, value }) + ) as KeyValue[]; + return ( + <> + {!isHttpAction && ( + <> +
    + Provider Name + { + const providerConfig = value?.trim() || ""; + return ( +
    + + Or +
    + + { + updateProperty("refresh", value, true); + onChange(value); + }} + //TODO: move it to zod validations. + error={ + providerConfig !== "" && + providerConfig !== undefined && + isThisProviderNeedsInstallation && + installedProviderByType?.find( + (p) => p.details?.name === providerConfig + ) === undefined + } + errorMessage={`${ + providerConfig && + isThisProviderNeedsInstallation && + installedProviderByType?.find( + (p) => p.details?.name === providerConfig + ) === undefined + ? "Please note this provider is not installed and you'll need to install it before executing this workflow." + : "" + }`} + /> +
    +
    + ); + }} + /> + {errors?.properties?.config && ( +
    + {errors.properties.config.message?.toString()} +
    + )} +
    + Provider Parameters + + )} +
    + + { + updateProperty("refresh", value, true); + }} /> - ) : null} +
    - Deploy - + {/* fixed the vars key value issue.(key is not getting updated properly)*/} + ( + field.onChange(value)} + /> + )} + />
    - - + {uniqueParams + ?.filter((key) => key !== "kwargs") + .map((key, index) => { + let currentPropertyValue = ((properties.with as any) ?? {})[key]; + if (typeof currentPropertyValue === "object") { + currentPropertyValue = JSON.stringify(currentPropertyValue); + } + let requiredKeys = tempRequiredKeys || []; + if (key === "method" && isHttpAction) { + return ( +
    + + {key}{" "} + {requiredKeys?.includes(key) ? ( + * + ) : ( + "" + )} + + ( + + )} + /> +
    + ); + } + if (key === "body" && isHttpAction) { + if (["POST", "PUT", "PATCH"].includes(properties?.with?.method)) { + requiredKeys = [...tempRequiredKeys, "body"]; + } + } + return ( +
    + + { + let currentPropertyValue = ((properties.with as any) ?? {})[ + key + ]; + if (typeof currentPropertyValue === "object") { + currentPropertyValue = JSON.stringify(currentPropertyValue); + } + return ( + + updateProperty("refresh", value, true) + } + error={ + !!errors?.properties?.with?.[key] || + (pickInitialErros && !!stepErrors?.[`properties.with.${key}`]) + } + errorMessage={ + errors?.properties?.with?.[key]?.message?.toString() || + (pickInitialErros ? stepErrors?.[`properties.with.${key}`] : "") + } + /> + ); + }} + /> +
    + ); + })} + ); } diff --git a/keep-ui/app/(keep)/workflows/builder/types.ts b/keep-ui/app/(keep)/workflows/builder/types.ts index 593da16deb..ae83f46dd9 100644 --- a/keep-ui/app/(keep)/workflows/builder/types.ts +++ b/keep-ui/app/(keep)/workflows/builder/types.ts @@ -76,6 +76,7 @@ export type V2Step = { edgeSource?: string; edgeTarget?: string; notClickable?: boolean; + installed? :boolean; }; export type NodeData = Node["data"] & Record; export type NodeStepMeta = { id: string; label?: string }; @@ -157,6 +158,10 @@ export type FlowState = { setSynced: (synced: boolean) => void; canDeploy: boolean; setCanDeploy: (deploy: boolean) => void; + stepErrors: Record | null; + setStepErrors: (errors:Record|null)=>void; + globalErrors: Record | null; + setGlobalErros: (errors:Record|null)=>void; }; export type StoreGet = () => FlowState; export type StoreSet = ( diff --git a/keep-ui/app/(keep)/workflows/builder/utils.tsx b/keep-ui/app/(keep)/workflows/builder/utils.tsx index 2b7eb7c216..c04f26c37e 100644 --- a/keep-ui/app/(keep)/workflows/builder/utils.tsx +++ b/keep-ui/app/(keep)/workflows/builder/utils.tsx @@ -2,139 +2,234 @@ import { load, JSON_SCHEMA } from "js-yaml"; import { Provider } from "../../providers/providers"; import { Action, Alert } from "./legacy-workflow.types"; import { v4 as uuidv4 } from "uuid"; +import { z, ZodObject } from "zod"; import { Definition, V2Properties, V2Step, } from "@/app/(keep)/workflows/builder/types"; -export function getToolboxConfiguration(providers: Provider[]) { +export const contentTypeOptions = [ + { + key: "application/json", + value: "application/json", + label: "application/json", + }, + { + key: "application/x-www-form-urlencoded", + value: "application/x-www-form-urlencoded", + label: "application/x-www-form-urlencoded", + }, + { + key: "multipart/form-data", + value: "multipart/form-data", + label: "multipart/form-data", + }, + { + key: "text/plain", + value: "text/plain", + label: "text/plain", + }, +]; + +export const methodOptions = [ + { + value: "GET", + key: "GET", + }, + { + key: "POST", + value: "POST", + }, + { + value: "PUT", + key: "PUT", + }, + { + value: "DELETE", + key: "DELETE", + }, + { + key: "PATCH", + value: "PATCH", + }, +]; + +export const requiredMap = { + global: ["name", "description"], + "action-http": ["url", "method"], +} as { [key: string]: string[] }; + +const triggers = [ + { + type: "manual", + componentType: "trigger", + name: "Manual", + id: "manual", + properties: { + manual: "true", + }, + }, + { + type: "interval", + componentType: "trigger", + name: "Interval", + id: "interval", + properties: { + interval: "", + }, + }, + { + type: "alert", + componentType: "trigger", + name: "Alert", + id: "alert", + properties: { + alert: { + source: "", + }, + }, + }, + { + type: "incident", + componentType: "trigger", + name: "Incident", + id: "incident", + properties: { + incident: { + events: [], + }, + }, + }, +]; + +const conditions = [ + { + type: "condition-threshold", + componentType: "switch", + name: "Threshold", + properties: { + value: "", + compare_to: "", + }, + branches: { + true: [], + false: [], + }, + }, + { + type: "condition-assert", + componentType: "switch", + name: "Assert", + properties: { + value: "", + compare_to: "", + }, + branches: { + true: [], + false: [], + }, + }, +]; + +const miscs = [ + { + type: "foreach", + componentType: "container", + name: "Foreach", + properties: {}, + sequence: [], + }, +]; + +const toolsWithoutConfigState = [ + "action-http", + ...triggers.map((trigger) => trigger.type), + ...conditions.map((cond) => cond.type), + ...miscs.map((misc) => misc.type), +]; + +const getStepsActionsFromProviders = ( + providers: Provider[], + installed?: boolean +) => { + return ( + providers.reduce( + ([steps, actions], provider) => { + const step = { + componentType: "task", + properties: { + stepParams: provider.query_params!, + actionParams: provider.notify_params!, + }, + installed: installed, + } as Partial; + if (installed) { + step.properties = { + ...step.properties, + config: provider?.details?.name || provider.id, + }; + } + if (provider.can_query) + steps.push({ + ...step, + type: `step-${provider.type}`, + name: installed + ? provider?.details?.name || provider.id + : `${provider.type}-step`, + id: provider.id, // to identify the provider. + }); + if (provider.can_notify) + actions.push({ + ...step, + type: `action-${provider.type}`, + name: installed + ? provider?.details?.name || provider.id + : `${provider.type}-action`, + }); + return [steps, actions]; + }, + [[] as Partial[], [] as Partial[]] + ) || [[], []] + ); +}; + +export function getToolboxConfiguration( + providers: Provider[], + installedProviders?: Provider[] +) { /** * Generates the toolbox items */ - const [steps, actions] = providers.reduce( - ([steps, actions], provider) => { - const step = { - componentType: "task", - properties: { - stepParams: provider.query_params!, - actionParams: provider.notify_params!, - }, - } as Partial; - if (provider.can_query) - steps.push({ - ...step, - type: `step-${provider.type}`, - name: `${provider.type}-step`, - }); - if (provider.can_notify) - actions.push({ - ...step, - type: `action-${provider.type}`, - name: `${provider.type}-action`, - }); - return [steps, actions]; - }, - [[] as Partial[], [] as Partial[]] + const [steps, actions] = getStepsActionsFromProviders(providers); + const [installedSteps, installedActions] = getStepsActionsFromProviders( + installedProviders || [], + true ); + const finalSteps = [...steps, ...installedSteps]; + const finalActionsSteps = [...actions, ...installedActions]; return { groups: [ { name: "Triggers", - steps: [ - { - type: "manual", - componentType: "trigger", - name: "Manual", - id: "manual", - properties: { - manual: "true", - }, - }, - { - type: "interval", - componentType: "trigger", - name: "Interval", - id: "interval", - properties: { - interval: "", - }, - }, - { - type: "alert", - componentType: "trigger", - name: "Alert", - id: "alert", - properties: { - alert: { - source: "", - }, - }, - }, - { - type: "incident", - componentType: "trigger", - name: "Incident", - id: "incident", - properties: { - incident: { - events: [], - }, - }, - }, - ], + steps: triggers, }, { name: "Steps", - steps: steps, + steps: finalSteps, }, { name: "Actions", - steps: actions, + steps: finalActionsSteps, }, { name: "Misc", - steps: [ - { - type: "foreach", - componentType: "container", - name: "Foreach", - properties: {}, - sequence: [], - }, - ], + steps: miscs, }, // TODO: get conditions from API { name: "Conditions", - steps: [ - { - type: "condition-threshold", - componentType: "switch", - name: "Threshold", - properties: { - value: "", - compare_to: "", - }, - branches: { - true: [], - false: [], - }, - }, - { - type: "condition-assert", - componentType: "switch", - name: "Assert", - properties: { - value: "", - compare_to: "", - }, - branches: { - true: [], - false: [], - }, - }, - ], + steps: conditions, }, ], }; @@ -159,7 +254,8 @@ export function getActionOrStepObj( config: (actionOrStep.provider?.config as string) ?.replaceAll("{{", "") .replaceAll("}}", "") - .replaceAll("providers.", ""), + .replaceAll("providers.", "") + .trim(), with: actionOrStep.provider?.with, stepParams: provider?.query_params!, actionParams: provider?.notify_params!, @@ -379,7 +475,7 @@ function getActionsFromCondition( const withParams = getWithParams(a); const providerType = a?.type?.replace("action-", ""); const providerName = - (a?.properties?.config as string)?.trim() || `default-${providerType}`; + (a?.properties?.config as string) || `default-${providerType}`; const provider = { type: a.type.replace("action-", ""), config: `{{ providers.${providerName} }}`, @@ -443,6 +539,7 @@ export function buildAlert(definition: Definition): Alert { } return step; }); + // Actions let actions = alert.sequence .filter((s) => s.type.startsWith("action-")) @@ -580,3 +677,279 @@ export function wrapDefinitionV2({ isValid: !!isValid, }; } + +const checkValidJson = (value?: string | object, allowEmpty?: boolean) => { + try { + if (value && Array.isArray(value)) { + return false; + } + if (value && typeof value === "object") { + return true; + } + if (allowEmpty && !value?.trim()) return true; + const result = JSON.parse(value || ""); + if (value && Array.isArray(result)) { + return false; + } + if (typeof result === "object") { + return true; + } + return false; + } catch { + return false; + } +}; + +const bodyBasedMethodSchema = z.object({ + method: z.enum(["POST", "PUT", "PATCH"]), + body: z.union([z.string(), z.object({}).passthrough()]).refine( + (value) => { + const valid = checkValidJson(value, false); + return valid; + }, + { message: "Body must be valid JSON" } + ), + params: z + .union([z.string(), z.object({}).passthrough()]) + .optional() + .refine( + (value) => { + const valid = checkValidJson(value, true); + return valid; + }, + { message: "Params must be valid JSON" } + ), + headers: z + .union([z.string(), z.object({}).passthrough()]) + .optional() + .refine( + (value) => { + const valid = checkValidJson(value, true); + return valid; + }, + { message: "Headers must be valid JSON" } + ), +}); + +// Schema for `GET` and `DELETE` methods +const paramsBasedMethodSchema = z.object({ + method: z.enum(["GET", "DELETE"]), + params: z + .union([z.string(), z.object({}).passthrough()]) + .optional() + .refine( + (value) => { + const valid = checkValidJson(value, true); + return valid; + }, + { message: "Params must be valid JSON" } + ), + headers: z + .union([z.string(), z.object({}).passthrough()]) + .optional() + .refine( + (value) => { + const valid = checkValidJson(value, true); + return valid; + }, + { message: "Headers must be valid JSON" } + ), +}); + +export const httpmethodSchema = z.discriminatedUnion("method", [ + bodyBasedMethodSchema, + paramsBasedMethodSchema, +]); + +const standardPropertiestSchema = z + .object({ + name: z.string().min(1, { message: "Unique Identifier is mandatory" }), + type: z.string().min(1, { message: "type is mandatory" }), + properties: z + .object({ + vars: z.object({}).passthrough().optional(), + stepParams: z.array(z.string()).optional().nullable(), + actionParams: z.array(z.string()).optional().nullable(), + with: z + .object({ + message: z.string().optional().nullable(), + description: z.string().optional().nullable(), + }) + .passthrough() + .optional() + .nullable(), + }) + .passthrough(), + }) + .passthrough(); + +const getUrlSchema = (optional?: boolean) => { + if (optional) + return z.object({ + url: z + .string({ message: "Invalid url" }) + .url({ message: "Invalid url" }) + .optional(), + }); + return z.object({ + url: z + .string() + .min(4, { message: "Invalid url" }) + .url({ message: "Invalid url" }), + }); +}; + +export type FormData = z.infer>; + +const getConfigSchema = (type?: string) => { + //we might need add some key to identify the provider stateless tools. for now doing it like this. + + if (type && toolsWithoutConfigState.includes(type)) { + return z.object({ config: z.string().optional() }); + } + return z.object({ config: z.string().min(2, "Provider is mandatory!") }); +}; + +const customSchemaByType = (type?: string) => { + let schema: ZodObject = z.object({}); + switch (type) { + case "action-http": + schema = z + .object({ + with: getUrlSchema().and(httpmethodSchema), + }) + .passthrough(); + break; + case "action-slack": + schema = z.object({ + with: z.object({ + message: z.string().min(4, "Message should not be empty"), + }), + }); + break; + case "condition-threshold": + schema = z.object({ + value: z.string().min(1, "Value is required"), + compare_to: z.string().min(1, "Compare to is required"), + }); + break; + case "condition-assert": + schema = z.object({ + assert: z.string().min(4, "assert is required(eg:200==200)"), + }); + break; + case "foreach": + schema = z.object({ + value: z.string().min(1, "value is required"), + }); + break; + default: + break; //do nothing + } + + return schema.passthrough(); +}; + +export const getSchemaByStepType = (type?: string) => { + // Validation based on type + return z + .object({ + properties: customSchemaByType(type).and(getConfigSchema(type)), + }) + .passthrough() + .and(standardPropertiestSchema); +}; + +export const standardWorkflowPropertiesSchema = z + .object({ + name: z + .string() + .min(3, "Name is required and should be alteast 3 characters"), + id: z.string().min(3, "id is required"), + description: z + .string() + .min(4, "description is required and should be alteast 3 characters"), + disbaled: z.enum(["true", "false"]).optional().nullable(), + consts: z.object({}).passthrough().optional().nullable(), + }) + .passthrough(); + +export const intervalSchema = z + .object({ + interval: z + .union([z.string(), z.number()]) + .optional() + .nullable() + .refine( + (val) => { + if (!val || !Number(val)) { + return false; + } + return true; + }, + { message: "Interval should be number" } + ), + }) + .passthrough(); + +export const alertSchema = z + .object({ + alert: z + .object({}) + .passthrough() + .optional() + .nullable() + .refine( + (data) => { + // Check if the object is not empty + return Object.values(data || {}).filter((val) => !!val).length > 0; + }, + { + message: "Workflow alert trigger cannot be empty.", + } + ), + }) + .passthrough(); + +export const incidentSchema = z + .object({ + incident: z + .object({ + events: z + .array( + z.enum(["created", "updated", "deleted"], { + message: "Workflow incident trigger cannot be empty.", + }) + ) + .optional() + .nullable(), + }) + .optional() + .nullable() + .refine( + (val) => { + if (val && val?.events?.[0]) { + return true; + } + return false; + }, + { message: "Workflow incident trigger cannot be empty." } + ), + }) + .passthrough(); + +export const getWorkflowPropertiesSchema = (properties: V2Properties) => { + let schema: ZodObject = standardWorkflowPropertiesSchema; + + if ("interval" in properties) { + schema = standardWorkflowPropertiesSchema.merge(intervalSchema); + } + if ("alert" in properties) { + schema = standardWorkflowPropertiesSchema.merge(alertSchema); + } + if ("incident" in properties) { + schema = standardWorkflowPropertiesSchema.merge(incidentSchema); + } + + return schema; +}; diff --git a/keep-ui/app/(keep)/workflows/mockworkflows.tsx b/keep-ui/app/(keep)/workflows/mockworkflows.tsx index 1fef3e2650..7288e229ce 100644 --- a/keep-ui/app/(keep)/workflows/mockworkflows.tsx +++ b/keep-ui/app/(keep)/workflows/mockworkflows.tsx @@ -164,11 +164,11 @@ export default function MockWorkflowCardSection({ const workflow = template.workflow; return (
    - +

    {workflow.name || getNameFromId(workflow.id)}

    diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index cc408c7177..7892093088 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -23,6 +23,7 @@ "@headlessui/react": "^1.7.14", "@headlessui/tailwindcss": "^0.2.1", "@heroicons/react": "^2.1.5", + "@hookform/resolvers": "^3.9.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^1.0.0", "@radix-ui/react-popover": "^1.1.2", @@ -3445,6 +3446,14 @@ "react": ">= 16" } }, + "node_modules/@hookform/resolvers": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index f397841e47..ce828b9021 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -24,6 +24,7 @@ "@headlessui/react": "^1.7.14", "@headlessui/tailwindcss": "^0.2.1", "@heroicons/react": "^2.1.5", + "@hookform/resolvers": "^3.9.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^1.0.0", "@radix-ui/react-popover": "^1.1.2", diff --git a/keep/providers/http_provider/http_provider.py b/keep/providers/http_provider/http_provider.py index 07f25521be..8c4a7438ed 100644 --- a/keep/providers/http_provider/http_provider.py +++ b/keep/providers/http_provider/http_provider.py @@ -88,11 +88,15 @@ def _query( if headers is None: headers = {} if isinstance(headers, str): - headers = json.loads(headers) + headers = json.loads(headers) if len(headers) > 0 else {} if body is None: body = {} + if isinstance(body, str): + body = json.loads(body) if len(body) > 0 else {} if params is None: params = {} + if isinstance(params, str): + params = json.loads(params) if len(params) > 0 else {} # todo: this might be problematic if params/body/headers contain sensitive data # think about changing those debug messages or adding a flag to enable/disable them diff --git a/keep/providers/webhook_provider/webhook_provider.py b/keep/providers/webhook_provider/webhook_provider.py index b08e302a06..d44822bcdd 100644 --- a/keep/providers/webhook_provider/webhook_provider.py +++ b/keep/providers/webhook_provider/webhook_provider.py @@ -176,11 +176,15 @@ def _query( if headers is None: headers = {} if isinstance(headers, str): - headers = json.loads(headers) + headers = json.loads(headers) if len(headers) > 0 else {} if body is None: body = {} + if isinstance(body, str): + body = json.loads(body) if len(body) > 0 else {} if params is None: params = {} + if isinstance(params, str): + params = json.loads(params) if len(params) > 0 else {} if http_basic_authentication_username and http_basic_authentication_password: credentials = f"{http_basic_authentication_username}:{http_basic_authentication_password}"