From 296943336088f8eba51d39736f9f357db48bb1ae Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Fri, 21 Feb 2025 00:08:14 +0400 Subject: [PATCH 01/14] wip: more accurate steps --- .../model/__tests__/workflow-store.test.tsx | 4 +- keep-ui/entities/workflows/model/types.ts | 14 +- .../workflows/model/workflow-store.ts | 15 +- .../builder/ui/BuilderChat/AddStepUI.tsx | 228 ++---- .../builder/ui/BuilderChat/AddTriggerUI.tsx | 6 +- .../builder/ui/BuilderChat/StepPreview.tsx | 61 +- .../builder/ui/BuilderChat/builder-chat.tsx | 737 +++++++++++++----- .../workflows/builder/ui/WorkflowToolbox.tsx | 6 +- .../workflows/builder/ui/workflow-status.tsx | 4 +- 9 files changed, 638 insertions(+), 437 deletions(-) diff --git a/keep-ui/entities/workflows/model/__tests__/workflow-store.test.tsx b/keep-ui/entities/workflows/model/__tests__/workflow-store.test.tsx index ec4203d55..f9b2f2e37 100644 --- a/keep-ui/entities/workflows/model/__tests__/workflow-store.test.tsx +++ b/keep-ui/entities/workflows/model/__tests__/workflow-store.test.tsx @@ -66,7 +66,7 @@ describe("useWorkflowStore", () => { // Add a trigger node act(() => { - result.current.addNodeBetween( + result.current.addNodeBetweenSafe( "edge-1", { id: "interval", @@ -109,7 +109,7 @@ describe("useWorkflowStore", () => { // Try to add another trigger act(() => { const edges = result.current.edges; - result.current.addNodeBetween( + result.current.addNodeBetweenSafe( edges[1].id, { id: "interval", diff --git a/keep-ui/entities/workflows/model/types.ts b/keep-ui/entities/workflows/model/types.ts index 0973c91e3..750c9e0b2 100644 --- a/keep-ui/entities/workflows/model/types.ts +++ b/keep-ui/entities/workflows/model/types.ts @@ -170,6 +170,13 @@ export type V2StepConditionThreshold = z.infer< typeof V2StepConditionThresholdSchema >; +export const V2StepConditionSchema = z.union([ + V2StepConditionAssertSchema, + V2StepConditionThresholdSchema, +]); + +export type V2StepCondition = z.infer; + export const V2StepForeachSchema = z.object({ id: z.string(), name: z.string(), @@ -347,6 +354,11 @@ export interface FlowState extends FlowStateValues { step: V2StepTemplate | V2StepTrigger, type: "node" | "edge" ) => string | null; + addNodeBetweenSafe: ( + nodeOrEdgeId: string, + step: V2StepTemplate | V2StepTrigger, + type: "node" | "edge" + ) => string | null; setToolBoxConfig: (config: ToolboxConfiguration) => void; setEditorOpen: (open: boolean) => void; updateSelectedNodeData: (key: string, value: any) => void; @@ -361,7 +373,7 @@ export interface FlowState extends FlowStateValues { setEdges: (edges: Edge[]) => void; getNodeById: (id: string) => FlowNode | undefined; getEdgeById: (id: string) => Edge | undefined; - deleteNodes: (ids: string | string[]) => void; + deleteNodes: (ids: string | string[]) => string[]; getNextEdge: (nodeId: string) => Edge | undefined; reset: () => void; setDefinition: (def: DefinitionV2) => void; diff --git a/keep-ui/entities/workflows/model/workflow-store.ts b/keep-ui/entities/workflows/model/workflow-store.ts index 04fbd93a4..6e4be6bce 100644 --- a/keep-ui/entities/workflows/model/workflow-store.ts +++ b/keep-ui/entities/workflows/model/workflow-store.ts @@ -260,6 +260,15 @@ export const useWorkflowStore = create()( nodeOrEdgeId: string, step: V2StepTemplate | V2StepTrigger, type: "node" | "edge" + ) => { + const newNodeId = addNodeBetween(nodeOrEdgeId, step, type, set, get); + set({ selectedNode: newNodeId, selectedEdge: null }); + return newNodeId ?? null; + }, + addNodeBetweenSafe: ( + nodeOrEdgeId: string, + step: V2StepTemplate | V2StepTrigger, + type: "node" | "edge" ) => { try { const newNodeId = addNodeBetween(nodeOrEdgeId, step, type, set, get); @@ -474,12 +483,12 @@ export const useWorkflowStore = create()( deleteNodes: (ids) => { //for now handling only single node deletion. can later enhance to multiple deletions if (typeof ids !== "string") { - return; + return []; } const nodes = get().nodes; const nodeStartIndex = nodes.findIndex((node) => ids == node.id); if (nodeStartIndex === -1) { - return; + return []; } let idArray = Array.isArray(ids) ? ids : [ids]; @@ -561,6 +570,8 @@ export const useWorkflowStore = create()( }); get().onLayout({ direction: "DOWN" }); get().updateDefinition(); + + return [ids]; }, getNextEdge: (nodeId: string) => { const node = get().getNodeById(nodeId); diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx index ad0f9f27f..4213f0a63 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx @@ -1,157 +1,64 @@ import { Button } from "@/components/ui"; -import { useWorkflowStore, V2Step } from "@/entities/workflows"; -import { WF_DEBUG_INFO } from "../debug-settings"; -import { DebugArgs } from "./debug-args"; import { StepPreview } from "./StepPreview"; import { SuggestionResult, SuggestionStatus } from "./SuggestionStatus"; import clsx from "clsx"; -import { DebugJSON } from "@/shared/ui"; -import { useCallback } from "react"; -import { triggerTypes } from "../../lib/utils"; -import { Edge } from "@xyflow/react"; +import { V2Step } from "@/entities/workflows/model/types"; +import { useWorkflowStore } from "@/entities/workflows"; -type AddStepUIProps = - | { - status: "complete"; - args: { - stepDefinitionJSON?: string; - addAfterNodeName?: string; - addAfterEdgeId?: string; - isStart?: boolean; - }; - respond: undefined; - result: SuggestionResult; - } - | { - status: "executing"; - args: { - stepDefinitionJSON?: string; - addAfterNodeName?: string; - addAfterEdgeId?: string; - isStart?: boolean; - }; - respond: (response: SuggestionResult) => void; - result: undefined; - }; +type AddStepUIPropsCommon = { + step: V2Step; + addAfterEdgeId: string; +}; -function getComponentType(stepType: string) { - if (stepType.startsWith("step-")) return "task"; - if (stepType.startsWith("action-")) return "task"; - if (stepType.startsWith("condition-")) return "switch"; - if (stepType === "foreach") return "container"; - if (triggerTypes.includes(stepType)) return "trigger"; - return "task"; -} +type AddStepUIPropsComplete = AddStepUIPropsCommon & { + status: "complete"; + result: SuggestionResult; + respond: undefined; +}; -function parseAndAutoCorrectStepDefinition(stepDefinitionJSON: string) { - const step = JSON.parse(stepDefinitionJSON); - return { - ...step, - componentType: getComponentType(step.type), - }; -} +type AddStepUIPropsExecuting = AddStepUIPropsCommon & { + status: "executing"; + result: undefined; + respond: (response: SuggestionResult) => void; +}; + +type AddStepUIProps = AddStepUIPropsComplete | AddStepUIPropsExecuting; export const AddStepUI = ({ status, - args, - respond, + step, + addAfterEdgeId, result, + respond, }: AddStepUIProps) => { - const { - definition, - nodes, - getNodeById, - getNextEdge, - addNodeBetween, - getEdgeById, - } = useWorkflowStore(); - let { - stepDefinitionJSON, - addAfterNodeName: addAfterNodeIdOrName, - addAfterEdgeId, - isStart, - } = args; + const { addNodeBetween } = useWorkflowStore(); - const addNodeAfterNode = useCallback( - ( - nodeToAddAfterId: string, - step: V2Step, - isStart: boolean, - respond: (response: any) => void, - addAfterEdgeId?: string - ) => { - if ( - nodeToAddAfterId === "alert" || - nodeToAddAfterId === "incident" || - nodeToAddAfterId === "interval" || - nodeToAddAfterId === "manual" - ) { - nodeToAddAfterId = "trigger_end"; - } - let node = getNodeById(isStart ? "trigger_end" : nodeToAddAfterId); - if (!node) { - const nodeByName = definition?.value.sequence.find( - (s) => s.name === nodeToAddAfterId - ); - if (nodeByName) { - node = getNodeById(nodeByName.id); - } - if (!node) { - respond?.({ - status: "error", - error: new Error("Can't find the node to add the step after"), - message: "Step not added due to error", - }); - return; - } - } - let nextEdge: Edge | null = null; - if (addAfterEdgeId) { - nextEdge = getEdgeById(addAfterEdgeId) ?? null; - } - if (!nextEdge) { - nextEdge = getNextEdge(node.id) ?? null; - } - if (!nextEdge) { - respond?.({ - status: "error", - error: new Error("Can't find the edge to add the step after"), - message: "Step not added due to error", - }); - return; - } - try { - addNodeBetween(nextEdge.id, step, "edge"); - respond?.({ - status: "complete", - stepId: step.id, - message: "Step added", - }); - } catch (e) { - respond?.({ - status: "error", - error: e, - message: "Step not added due to error", - }); - } - }, - [addNodeBetween, definition?.value.sequence, getNextEdge, getNodeById] - ); + const onAdd = () => { + try { + addNodeBetween(addAfterEdgeId, step, "edge"); + respond?.({ + status: "complete", + message: "Step added", + }); + } catch (e) { + respond?.({ + status: "error", + error: e, + message: "Step not added", + }); + } + }; - if (!stepDefinitionJSON) { - return
Step definition not found
; - } - if (definition?.value.sequence.length === 0) { - isStart = true; - } - let step = parseAndAutoCorrectStepDefinition(stepDefinitionJSON); + const onCancel = () => { + respond?.({ + status: "complete", + message: "User cancelled adding step", + }); + }; if (status === "complete") { return (
- {WF_DEBUG_INFO && ( - - )}
-
- Do you want to add this step after {addAfterNodeIdOrName} -
{step.name}
- {WF_DEBUG_INFO && ( - - )} - {WF_DEBUG_INFO && ( - - )} -
+ {/* TODO: add the place where the action will be added in text */} +
Do you want to add this action?
- -
diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/AddTriggerUI.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/AddTriggerUI.tsx index 328cec3da..087c5b7b1 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/AddTriggerUI.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/AddTriggerUI.tsx @@ -56,7 +56,11 @@ export const AddTriggerUI = ({ result, }: AddTriggerUIProps) => { const [isAddingTrigger, setIsAddingTrigger] = useState(false); - const { nodes, addNodeBetween, getNextEdge } = useWorkflowStore(); + const { + nodes, + addNodeBetweenSafe: addNodeBetween, + getNextEdge, + } = useWorkflowStore(); const { triggerType, triggerProperties } = args; const triggerDefinition = useMemo(() => { diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/StepPreview.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/StepPreview.tsx index 4fd23267e..3f56ab9f4 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/StepPreview.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/StepPreview.tsx @@ -12,7 +12,6 @@ import { getYamlStepFromStep, getYamlActionFromAction, } from "@/entities/workflows/lib/parser"; -import { YamlStep, YamlAction } from "@/entities/workflows/model/yaml.types"; import { Editor } from "@monaco-editor/react"; import { stringify } from "yaml"; import { getTriggerDescriptionFromStep } from "@/entities/workflows/lib/getTriggerDescription"; @@ -26,6 +25,28 @@ function getStepIconUrl(data: V2Step | V2StepTrigger) { return `/icons/${normalizeStepType(type)}-icon.png`; } +function getYamlFromStep(step: V2Step | V2StepTrigger) { + try { + if (step.componentType === "task" && step.type.startsWith("step-")) { + return getYamlStepFromStep(step as V2StepStep); + } + if (step.componentType === "task" && step.type.startsWith("action-")) { + return getYamlActionFromAction(step as V2ActionStep); + } + if (step.componentType === "trigger") { + return { + type: step.type, + ...step.properties, + }; + } + // TODO: add other types + return null; + } catch (error) { + console.error(error); + return null; + } +} + export const StepPreview = ({ step, className, @@ -33,42 +54,8 @@ export const StepPreview = ({ step: V2Step | V2StepTrigger; className?: string; }) => { - const type = normalizeStepType(step?.type); - let yamlOfStep: - | YamlStep - | YamlAction - | ({ type: string } & Record) - | null = null; - if ( - step.componentType === "task" && - step.type.startsWith("step-") && - "stepParams" in step.properties - ) { - try { - yamlOfStep = getYamlStepFromStep(step as V2StepStep); - } catch (error) { - console.error(error); - } - } - if ( - step.componentType === "task" && - step.type.startsWith("action-") && - "actionParams" in step.properties - ) { - try { - yamlOfStep = getYamlActionFromAction(step as V2ActionStep); - } catch (error) { - console.error(error); - } - } - if (step.componentType === "trigger") { - yamlOfStep = { - type: step.type, - ...step.properties, - }; - } - - const yaml = yamlOfStep ? stringify(yamlOfStep) : null; + const yamlDefinition = getYamlFromStep(step); + const yaml = yamlDefinition ? stringify(yamlDefinition) : null; const displayName = step.name; const subtitle = getTriggerDescriptionFromStep(step as V2StepTrigger); diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx index add8db5ea..4c1f4182f 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Provider } from "@/app/(keep)/providers/providers"; import { V2Step, @@ -7,6 +7,14 @@ import { V2StepStep, DefinitionV2, IncidentEventEnum, + V2ActionStep, + V2ActionSchema, + V2StepStepSchema, + V2StepConditionAssert, + V2StepConditionThreshold, + V2StepConditionAssertSchema, + V2StepCondition, + V2StepConditionSchema, } from "@/entities/workflows/model/types"; import { CopilotChat, @@ -25,7 +33,6 @@ import { GENERAL_INSTRUCTIONS } from "@/app/(keep)/workflows/builder/_constants" import { showSuccessToast } from "@/shared/ui/utils/showSuccessToast"; import { WF_DEBUG_INFO } from "../debug-settings"; import { AddTriggerUI } from "./AddTriggerUI"; -import { AddStepUI } from "./AddStepUI"; import { useTestStep } from "../Editor/StepTest"; import { useConfig } from "@/utils/hooks/useConfig"; import { Title, Text } from "@tremor/react"; @@ -37,6 +44,8 @@ import { SuggestionResult } from "./SuggestionStatus"; import { useSearchAlerts } from "@/utils/hooks/useSearchAlerts"; import "@copilotkit/react-ui/styles.css"; import "./chat.css"; +import Skeleton from "react-loading-skeleton"; +import { AddStepUI } from "./AddStepUI"; const useAlertKeys = () => { const defaultQuery = { @@ -95,6 +104,31 @@ function getWorkflowSummaryForCopilot(nodes: FlowNode[], edges: Edge[]) { }; } +function isProtectedStep(stepId: string) { + return ( + stepId === "start" || + stepId === "end" || + stepId === "trigger_start" || + stepId === "trigger_end" + ); +} + +const AddTriggerSkeleton = () => { + return ( +
+
+ +
+
+ +
+
+ +
+
+ ); +}; + export function BuilderChat({ definition, installedProviders, @@ -256,18 +290,32 @@ export function BuilderChat({ required: true, }, ], - handler: ({ stepId }: { stepId: string }) => { - if ( - stepId === "start" || - stepId === "end" || - stepId === "trigger_start" || - stepId === "trigger_end" - ) { - return; + renderAndWaitForResponse: ({ status, args, respond }) => { + if (status === "inProgress") { + return
Loading...
; + } + const stepId = args.stepId; + if (isProtectedStep(stepId)) { + respond?.( + "Cannot remove start, end, trigger_start or trigger_end steps" + ); + return ( +

Cannot remove start, end, trigger_start or trigger_end steps

+ ); } // TODO: nice UI for this if (confirm(`Are you sure you want to remove ${stepId} step?`)) { - deleteNodes(stepId); + const deletedNodeIds = deleteNodes(stepId); + if (deletedNodeIds.length > 0) { + respond?.("Step removed"); + return

Step {stepId} removed

; + } else { + respond?.("Step removal failed"); + return

Step removal failed

; + } + } else { + respond?.("User cancelled the step removal"); + return

Step removal cancelled

; } }, }); @@ -284,80 +332,40 @@ export function BuilderChat({ required: true, }, ], - handler: ({ triggerNodeId }: { triggerNodeId: string }) => { + renderAndWaitForResponse: ({ status, args, respond }) => { + if (status === "inProgress") { + return
Loading...
; + } + const triggerNodeId = args.triggerNodeId; + + if (isProtectedStep(triggerNodeId)) { + respond?.( + "Cannot remove start, end, trigger_start or trigger_end steps" + ); + return ( +

Cannot remove start, end, trigger_start or trigger_end steps

+ ); + } + // TODO: nice UI for this if ( confirm(`Are you sure you want to remove ${triggerNodeId} trigger?`) ) { - deleteNodes(triggerNodeId); + const deletedNodeIds = deleteNodes(triggerNodeId); + if (deletedNodeIds.length > 0) { + respond?.("Trigger removed"); + return

Trigger {triggerNodeId} removed

; + } else { + respond?.("Trigger removal failed"); + return

Trigger removal failed

; + } + } else { + respond?.("User cancelled the trigger removal"); + return

Trigger removal cancelled

; } }, }); - // TODO: simplify this action, e.g. params: componentType, name, properties - useCopilotAction( - { - name: "generateStepDefinition", - description: "Generate a workflow step definition", - parameters: [ - { - name: "stepType", - description: - "The type of step to add e.g. action-slack, step-python, condition-assert, etc", - type: "string", - required: true, - }, - { - name: "name", - description: "The short name of the step", - type: "string", - required: true, - }, - { - name: "aim", - description: - "The detailed description of the step's purpose and proposed solution", - type: "string", - required: true, - }, - ], - handler: async ({ - stepType, - name, - aim, - }: { - stepType: string; - name: string; - aim: string; - }) => { - const step = steps?.find((step: any) => step.type === stepType); - if (!step) { - return; - } - try { - const stepDefinition = await generateStepDefinition({ - name, - stepType, - stepProperties: { ...step.properties }, - aim, - }); - return { - ...step, - name: name ?? step.name, - properties: { - ...step.properties, - with: stepDefinition, - }, - }; - } catch (e) { - console.error(e); - return; - } - }, - }, - [steps] - ); - useCopilotAction({ name: "addManualTrigger", description: @@ -365,35 +373,38 @@ export function BuilderChat({ parameters: [], renderAndWaitForResponse: (args) => { if (args.status === "inProgress") { - // TODO: skeleton loader - return
Loading...
; + return ; } if (args.status === "complete" && "result" in args) { - return AddTriggerUI({ - status: "complete", - args: { + return ( + + ); + } + + return ( + + ); }, }); @@ -405,53 +416,72 @@ export function BuilderChat({ return keys?.map((key) => key.split(".").pop()); }, [keys]); - useCopilotAction( - { - name: "addAlertTrigger", - description: - "Add an alert trigger to the workflow. There could be only one alert trigger in the workflow, if you need more combine them into one alert trigger.", - parameters: [ - { - name: "alertFilters", - description: `The filters of the alert trigger, this should be a JSON object, there keys are one of: ${possibleAlertProperties.join( - ", " - )}, values are strings. This cannot be empty (undefined!)`, - type: "string", - required: true, - }, - ], - renderAndWaitForResponse: (args) => { - if (args.status === "inProgress") { - // TODO: skeleton loader - return
Loading...
; - } + useCopilotReadable({ + description: "Possible alert properties", + value: possibleAlertProperties, + }); - const argsToPass = { - triggerType: "alert", - triggerProperties: JSON.stringify({ - alert: JSON.parse(args.args.alertFilters), - }), - }; - - if (args.status === "complete" && "result" in args) { - return AddTriggerUI({ - status: "complete", - args: argsToPass, - respond: undefined, - result: args.result as SuggestionResult, - }); - } + useCopilotAction({ + name: "addAlertTrigger", + description: + "Add an alert trigger to the workflow. There could be only one alert trigger in the workflow, if you need more combine them into one alert trigger.", + parameters: [ + { + name: "alertFilters", + description: "The filters of the alert trigger", + type: "object[]", + required: true, + attributes: [ + { + name: "attribute", + description: `One of alert properties`, + type: "string", + required: true, + }, + { + name: "value", + description: "The value of the alert filter", + type: "string", + required: true, + }, + ], + }, + ], + renderAndWaitForResponse: (args) => { + if (args.status === "inProgress") { + return ; + } + + const argsToPass = { + triggerType: "alert", + triggerProperties: JSON.stringify({ + alert: args.args.alertFilters.reduce( + (acc, filter) => { + acc[filter.attribute] = filter.value; + return acc; + }, + {} as Record + ), + }), + }; + if (args.status === "complete" && "result" in args) { return AddTriggerUI({ - status: "executing", + status: "complete", args: argsToPass, - respond: args.respond, - result: undefined, + respond: undefined, + result: args.result as SuggestionResult, }); - }, + } + + return AddTriggerUI({ + status: "executing", + args: argsToPass, + respond: args.respond, + result: undefined, + }); }, - [possibleAlertProperties] - ); + }); useCopilotAction({ name: "addIntervalTrigger", @@ -467,8 +497,7 @@ export function BuilderChat({ ], renderAndWaitForResponse: (args) => { if (args.status === "inProgress") { - // TODO: skeleton loader - return
Loading...
; + return ; } const argsToPass = { @@ -477,20 +506,24 @@ export function BuilderChat({ }; if (args.status === "complete" && "result" in args) { - return AddTriggerUI({ - status: "complete", - args: argsToPass, - respond: undefined, - result: args.result as SuggestionResult, - }); + return ( + + ); } - return AddTriggerUI({ - status: "executing", - args: argsToPass, - respond: args.respond, - result: undefined, - }); + return ( + + ); }, }); @@ -508,8 +541,7 @@ export function BuilderChat({ ], renderAndWaitForResponse: (args) => { if (args.status === "inProgress") { - // TODO: skeleton loader - return
Loading...
; + return ; } const argsToPass = { @@ -520,69 +552,354 @@ export function BuilderChat({ }; if (args.status === "complete" && "result" in args) { - return AddTriggerUI({ - status: "complete", - args: argsToPass, - respond: undefined, - result: args.result as SuggestionResult, + return ( + + ); + } + + return ( + + ); + }, + }); + + function getActionStepFromCopilotAction(args: { + actionId: string; + actionType: string; + actionName: string; + providerName: string; + withActionParams: { name: string; value: string }[]; + }) { + const template = steps.find( + (step): step is V2ActionStep => + step.type === args.actionType && + step.componentType === "task" && + "actionParams" in step.properties + ); + if (!template) { + return null; + } + const action: V2ActionStep = { + ...template, + id: args.actionId, + name: args.actionName, + properties: { + ...template.properties, + with: args.withActionParams.reduce( + (acc, param) => { + acc[param.name] = param.value; + return acc; + }, + {} as Record + ), + }, + }; + return V2ActionSchema.parse(action); + } + + useCopilotAction({ + name: "addAction", + description: + "Add an action to the workflow. Actions are sending notifications to a provider.", + parameters: [ + { + name: "withActionParams", + description: "The parameters of the action to add", + type: "object[]", + required: true, + attributes: [ + { + name: "name", + description: "The name of the action parameter", + type: "string", + required: true, + }, + { + name: "value", + description: "The value of the action parameter", + type: "string", + required: true, + }, + ], + }, + { + name: "actionId", + description: "The id of the action to add", + type: "string", + required: true, + }, + { + name: "actionType", + description: "The type of the action to add", + type: "string", + required: true, + }, + { + name: "actionName", + description: "The kebab-case name of the action to add", + type: "string", + required: true, + }, + { + name: "providerName", + description: "The name of the provider to add", + type: "string", + required: true, + }, + { + name: "addAfterEdgeId", + description: + "The id of the edge to add the action after. If you're adding action in condition branch, make sure the edge id ends with '-true' or '-false' according to the desired branch.", + type: "string", + required: true, + }, + ], + renderAndWaitForResponse: ({ status, args, respond, result }) => { + if (status === "inProgress") { + return ; + } + const action = getActionStepFromCopilotAction(args); + if (!action) { + respond?.({ + status: "error", + error: "Action definition is invalid", }); + return
Action definition is invalid
; } - return AddTriggerUI({ - status: "executing", - args: argsToPass, - respond: args.respond, - result: undefined, - }); + return ( + + ); }, }); - // TODO: split this action into: addAction, addStep, addCondition, addForeach so parameters are more accurate - useCopilotAction( - { - name: "addStep", - description: - "Add a step to the workflow. After adding a step ensure you have the updated workflow definition.", - parameters: [ - { - name: "stepDefinitionJSON", - description: - "The step definition to add, use the 'generateStepDefinition' action to generate a step definition.", - type: "string", - required: true, - }, - { - name: "addAfterNodeName", - description: - "The 'name' of the step to add the new step after, get it from the workflow definition. If workflow is empty, use 'trigger_end'.", - type: "string", - required: true, - }, - { - name: "addAfterEdgeId", - description: - "If you want to add the step after specific edge, use the edgeId.", - type: "string", - required: false, - }, - { - // TODO: replace with more accurate nodeOrEdgeId description - name: "isStart", - description: "Whether the step is the start of the workflow", - type: "boolean", - required: false, - }, - ], - renderAndWaitForResponse: (args) => { - if (args.status === "inProgress") { - return
Loading...
; - } - return AddStepUI(args); + function getStepStepFromCopilotAction(args: { + stepId: string; + stepType: string; + stepName: string; + providerName: string; + withStepParams: { name: string; value: string }[]; + }) { + const template = steps.find( + (step): step is V2StepStep => step.type === args.stepType + ); + if (!template) { + return null; + } + + const step: V2StepStep = { + ...template, + id: args.stepId, + name: args.stepName, + properties: { + ...template.properties, + with: args.withStepParams.reduce( + (acc, param) => { + acc[param.name] = param.value; + return acc; + }, + {} as Record + ), + }, + }; + return V2StepStepSchema.parse(step); + } + + useCopilotAction({ + name: "addStep", + description: + "Add a step to the workflow. Steps are fetching data from a provider.", + parameters: [ + { + name: "withStepParams", + description: "The parameters of the step to add", + type: "object[]", + required: true, + attributes: [ + { + name: "name", + description: "The name of the step parameter", + type: "string", + required: true, + }, + { + name: "value", + description: "The value of the step parameter", + type: "string", + required: true, + }, + ], + }, + { + name: "stepId", + description: "The id of the step to add", + type: "string", + required: true, + }, + { + name: "stepType", + description: "The type of the step to add, should start with 'step-'", + type: "string", + required: true, }, + { + name: "stepName", + description: "The kebab-case name of the step to add", + type: "string", + required: true, + }, + { + name: "providerName", + description: "The name of the provider to add", + type: "string", + required: true, + }, + { + name: "addAfterEdgeId", + description: "The id of the edge to add the action after", + type: "string", + required: true, + }, + ], + renderAndWaitForResponse: ({ status, args, respond, result }) => { + if (status === "inProgress") { + return ; + } + const step = getStepStepFromCopilotAction(args); + if (!step) { + respond?.({ + status: "error", + error: "Step definition is invalid", + }); + return
Step definition is invalid
; + } + + return ( + + ); }, - [steps, selectedNode, selectedEdge, addNodeBetween] - ); + }); + + function getConditionStepFromCopilotAction(args: { + conditionId: string; + conditionType: string; + conditionName: string; + conditionValue: string; + compareToValue: string; + }) { + const template = steps.find( + (step): step is V2StepCondition => step.type === args.conditionType + ); + if (!template) { + throw new Error("Condition type is invalid"); + } + + const condition: V2StepCondition = { + ...template, + id: args.conditionId, + name: args.conditionName, + properties: { + ...template.properties, + value: args.conditionValue, + compare_to: args.compareToValue, + }, + }; + return V2StepConditionSchema.parse(condition); + } + useCopilotAction({ + name: "addCondition", + description: "Add a condition to the workflow.", + parameters: [ + { + name: "conditionId", + description: "The id of the condition to add", + type: "string", + required: true, + }, + { + name: "conditionType", + description: + "The type of the condition to add. One of: 'condition-assert', 'condition-threshold'", + type: "string", + required: true, + }, + { + name: "conditionName", + description: "The kebab-case name of the condition to add", + type: "string", + required: true, + }, + { + name: "conditionValue", + description: "The value of the condition to add", + type: "string", + required: true, + }, + { + name: "compareToValue", + description: "The value to compare the condition to", + type: "string", + required: true, + }, + { + name: "addAfterEdgeId", + description: "The id of the edge to add the condition after", + type: "string", + required: true, + }, + ], + renderAndWaitForResponse: ({ status, args, respond, result }) => { + if (status === "inProgress") { + return ; + } + try { + const condition = getConditionStepFromCopilotAction(args); + if (!condition) { + respond?.({ + status: "error", + error: "Condition definition is invalid", + errorDetail: "Condition type is invalid", + }); + return
Condition definition is invalid
; + } + return ( + + ); + } catch (e) { + respond?.({ status: "error", error: e }); + return
Failed to add condition {e?.message}
; + } + }, + }); // const testStep = useTestStep(); // TODO: add this action diff --git a/keep-ui/features/workflows/builder/ui/WorkflowToolbox.tsx b/keep-ui/features/workflows/builder/ui/WorkflowToolbox.tsx index da0d28c5a..b0a5d6416 100644 --- a/keep-ui/features/workflows/builder/ui/WorkflowToolbox.tsx +++ b/keep-ui/features/workflows/builder/ui/WorkflowToolbox.tsx @@ -23,7 +23,11 @@ const GroupedMenu = ({ isDraggable?: boolean; }) => { const [isOpen, setIsOpen] = useState(!!searchTerm || isDraggable); - const { selectedNode, selectedEdge, addNodeBetween } = useWorkflowStore(); + const { + selectedNode, + selectedEdge, + addNodeBetweenSafe: addNodeBetween, + } = useWorkflowStore(); useEffect(() => { setIsOpen(!!searchTerm || !isDraggable); diff --git a/keep-ui/features/workflows/builder/ui/workflow-status.tsx b/keep-ui/features/workflows/builder/ui/workflow-status.tsx index 05069b736..07d5d35b0 100644 --- a/keep-ui/features/workflows/builder/ui/workflow-status.tsx +++ b/keep-ui/features/workflows/builder/ui/workflow-status.tsx @@ -20,7 +20,7 @@ export const WorkflowStatus = ({ className }: { className?: string }) => { color="rose" > {Object.entries(validationErrors).map(([id, error]) => ( -
+ { @@ -31,7 +31,7 @@ export const WorkflowStatus = ({ className }: { className?: string }) => { {id}: {" "} {error} -
+ ))} ) : ( From 8fd64d2853621f8bd06405ba1ce2f8dd8aa51bba Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Fri, 21 Feb 2025 03:06:49 +0400 Subject: [PATCH 02/14] wip: better placement of new steps --- .../workflows/model/workflow-store.ts | 1 + .../features/workflows/builder/lib/utils.tsx | 25 ++++++++++ .../builder/ui/BuilderChat/AddStepUI.tsx | 32 +++++++++++-- .../builder/ui/BuilderChat/builder-chat.tsx | 47 ++++++++++++------- .../workflows/builder/ui/WorkflowEdge.tsx | 11 +---- 5 files changed, 85 insertions(+), 31 deletions(-) diff --git a/keep-ui/entities/workflows/model/workflow-store.ts b/keep-ui/entities/workflows/model/workflow-store.ts index 6e4be6bce..32cf922a2 100644 --- a/keep-ui/entities/workflows/model/workflow-store.ts +++ b/keep-ui/entities/workflows/model/workflow-store.ts @@ -173,6 +173,7 @@ function addNodeBetween( nodes: newNodes, isLayouted: false, changes: get().changes + 1, + lastChangedAt: Date.now(), }); switch (newNodeId) { diff --git a/keep-ui/features/workflows/builder/lib/utils.tsx b/keep-ui/features/workflows/builder/lib/utils.tsx index fb01860fd..5c1b29d84 100644 --- a/keep-ui/features/workflows/builder/lib/utils.tsx +++ b/keep-ui/features/workflows/builder/lib/utils.tsx @@ -183,3 +183,28 @@ export const normalizeStepType = (type: string) => { ?.replace("condition-", "") ?.replace("trigger_", ""); }; + +export function edgeCanHaveAddButton(source: string, target: string) { + let showAddButton = + !source?.includes("empty") && + !target?.includes("trigger_end") && + source !== "start"; + + if (!showAddButton) { + showAddButton = + target?.includes("trigger_end") && source?.includes("trigger_start"); + } + return showAddButton; +} + +export function edgeCanAddTrigger(source: string, target: string) { + return source?.includes("trigger_start") && target?.includes("trigger_end"); +} + +export function edgeCanAddStep(source: string, target: string) { + return ( + !source?.includes("empty") && + !target?.includes("trigger_end") && + source !== "start" + ); +} diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx index 4213f0a63..51d046090 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx @@ -7,7 +7,7 @@ import { useWorkflowStore } from "@/entities/workflows"; type AddStepUIPropsCommon = { step: V2Step; - addAfterEdgeId: string; + addBeforeNodeId: string; }; type AddStepUIPropsComplete = AddStepUIPropsCommon & { @@ -27,15 +27,26 @@ type AddStepUIProps = AddStepUIPropsComplete | AddStepUIPropsExecuting; export const AddStepUI = ({ status, step, - addAfterEdgeId, + addBeforeNodeId, result, respond, }: AddStepUIProps) => { - const { addNodeBetween } = useWorkflowStore(); + const { addNodeBetween, setSelectedNode } = useWorkflowStore(); const onAdd = () => { try { - addNodeBetween(addAfterEdgeId, step, "edge"); + // Hack to get the conditions to work. TODO: make it more straightforward + // FIX: it fails for ordinary steps + // if (addAfterEdgeId.includes("condition")) { + // const edge = getEdgeById(addAfterEdgeId); + // if (!edge) { + // throw new Error("Edge not found"); + // } + // const nodeId = edge!.source; + // addNodeBetween(nodeId, step, "node"); + // } else { + addNodeBetween(addBeforeNodeId, step, "node"); + // } respond?.({ status: "complete", message: "Step added", @@ -75,7 +86,18 @@ export const AddStepUI = ({
{/* TODO: add the place where the action will be added in text */} -
Do you want to add this action?
+
+ Do you want to add this action before node {addBeforeNodeId} ( + + )? +
diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx index 4c1f4182f..db6bd9094 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { Provider } from "@/app/(keep)/providers/providers"; import { V2Step, @@ -10,9 +10,6 @@ import { V2ActionStep, V2ActionSchema, V2StepStepSchema, - V2StepConditionAssert, - V2StepConditionThreshold, - V2StepConditionAssertSchema, V2StepCondition, V2StepConditionSchema, } from "@/entities/workflows/model/types"; @@ -46,6 +43,11 @@ import "@copilotkit/react-ui/styles.css"; import "./chat.css"; import Skeleton from "react-loading-skeleton"; import { AddStepUI } from "./AddStepUI"; +import { + edgeCanAddStep, + edgeCanAddTrigger, + edgeCanHaveAddButton, +} from "../../lib/utils"; const useAlertKeys = () => { const defaultQuery = { @@ -100,7 +102,11 @@ function getWorkflowSummaryForCopilot(nodes: FlowNode[], edges: Edge[]) { prevStepId: n.prevStepId, ...n.data, })), - edges: edges.map((e) => ({ id: e.id, source: e.source, target: e.target })), + edges: edges.map((e) => ({ + id: e.id, + source: e.source, + target: e.target, + })), }; } @@ -609,8 +615,7 @@ export function BuilderChat({ useCopilotAction({ name: "addAction", - description: - "Add an action to the workflow. Actions are sending notifications to a provider.", + description: `Add an action to the workflow. Actions are sending notifications to a provider.`, parameters: [ { name: "withActionParams", @@ -657,9 +662,11 @@ export function BuilderChat({ required: true, }, { - name: "addAfterEdgeId", - description: - "The id of the edge to add the action after. If you're adding action in condition branch, make sure the edge id ends with '-true' or '-false' according to the desired branch.", + name: "addBeforeNodeId", + description: `The id of the node to add the condition before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. For condition branches: +- Must end with '__empty-true' for true branch +- Must end with '__empty-false' for false branch +Example: 'node_123__empty-true'`, type: "string", required: true, }, @@ -681,7 +688,7 @@ export function BuilderChat({ @@ -771,8 +778,11 @@ export function BuilderChat({ required: true, }, { - name: "addAfterEdgeId", - description: "The id of the edge to add the action after", + name: "addBeforeNodeId", + description: `The id of the node to add the condition before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. For condition branches: +- Must end with '__empty-true' for true branch +- Must end with '__empty-false' for false branch +Example: 'node_123__empty-true'`, type: "string", required: true, }, @@ -795,7 +805,7 @@ export function BuilderChat({ status={status} step={step} result={result} - addAfterEdgeId={args.addAfterEdgeId} + addBeforeNodeId={args.addBeforeNodeId} respond={respond} /> ); @@ -865,8 +875,11 @@ export function BuilderChat({ required: true, }, { - name: "addAfterEdgeId", - description: "The id of the edge to add the condition after", + name: "addBeforeNodeId", + description: `The id of the node to add the condition before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. For condition branches: +- Must end with '__empty-true' for true branch +- Must end with '__empty-false' for false branch +Example: 'node_123__empty-true'`, type: "string", required: true, }, @@ -890,7 +903,7 @@ export function BuilderChat({ status={status} step={condition} result={result} - addAfterEdgeId={args.addAfterEdgeId} + addBeforeNodeId={args.addBeforeNodeId} respond={respond} /> ); diff --git a/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx b/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx index e44d89242..0080e9495 100644 --- a/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx +++ b/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx @@ -7,6 +7,7 @@ import "@xyflow/react/dist/style.css"; import { PlusIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; import { WF_DEBUG_INFO } from "./debug-settings"; +import { edgeCanHaveAddButton } from "../lib/utils"; export function DebugEdgeInfo({ id, @@ -68,15 +69,7 @@ export const WorkflowEdge: React.FC = ({ let dynamicLabel = label; const isLayouted = !!data?.isLayouted; - let showAddButton = - !source?.includes("empty") && - !target?.includes("trigger_end") && - source !== "start"; - - if (!showAddButton) { - showAddButton = - target?.includes("trigger_end") && source?.includes("trigger_start"); - } + let showAddButton = edgeCanHaveAddButton(source, target); const color = dynamicLabel === "True" From 038f8f159f735d2e782d6f6fcced8ec6cb71457b Mon Sep 17 00:00:00 2001 From: Kirill Chernakov Date: Sun, 23 Feb 2025 16:29:41 +0400 Subject: [PATCH 03/14] fix: validation of nested steps; insert steps into right place in conditions --- .../app/(keep)/providers/provider-form.tsx | 2 - .../[workflow_id]/workflow-providers.tsx | 4 +- keep-ui/entities/alerts/model/index.ts | 1 + .../alerts/model/useAvailableAlertFields.ts | 48 +++++++ keep-ui/entities/workflows/model/types.ts | 50 ++++--- .../entities/workflows/model/validation.ts | 87 ++++++++---- .../workflows/model/workflow-store.ts | 75 ++++++++-- .../builder/ui/BuilderChat/AddStepUI.tsx | 35 +++-- .../ui/BuilderChat/SuggestionStatus.tsx | 8 +- .../builder/ui/BuilderChat/builder-chat.tsx | 133 ++++-------------- .../builder/ui/Editor/ReactFlowEditor.tsx | 69 +++++---- .../builder/ui/Editor/StepEditor.tsx | 107 +++++--------- .../workflows/builder/ui/Editor/StepTest.tsx | 5 +- .../workflows/builder/ui/workflow-status.tsx | 116 +++++++++++---- keep-ui/widgets/workflow-builder/builder.tsx | 29 +++- 15 files changed, 441 insertions(+), 328 deletions(-) create mode 100644 keep-ui/entities/alerts/model/useAvailableAlertFields.ts diff --git a/keep-ui/app/(keep)/providers/provider-form.tsx b/keep-ui/app/(keep)/providers/provider-form.tsx index b44c739d9..fada62058 100644 --- a/keep-ui/app/(keep)/providers/provider-form.tsx +++ b/keep-ui/app/(keep)/providers/provider-form.tsx @@ -451,8 +451,6 @@ const ProviderForm = ({ ?.filter((scope) => scope.mandatory_for_webhook) .every((scope) => providerValidatedScopes[scope.name] === true); - const [activeTab, setActiveTab] = useState(0); - const renderFormContent = () => ( <>
diff --git a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-providers.tsx b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-providers.tsx index a800889af..42b524b70 100644 --- a/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-providers.tsx +++ b/keep-ui/app/(keep)/workflows/[workflow_id]/workflow-providers.tsx @@ -8,7 +8,7 @@ import { DynamicImageProviderIcon } from "@/components/ui"; import SlidingPanel from "react-sliding-side-panel"; import { useFetchProviders } from "../../providers/page.client"; import { useRevalidateMultiple } from "@/shared/lib/state-utils"; -import { PROVIDERS_WITH_NO_CONFIG } from "@/entities/workflows/model/validation"; +import { checkProviderNeedsInstallation } from "@/entities/workflows/model/validation"; export const ProvidersCarousel = ({ providers, @@ -110,7 +110,7 @@ export function WorkflowProviders({ workflow }: { workflow: Workflow }) { id: fullProvider.type, }; - if (PROVIDERS_WITH_NO_CONFIG.includes(mergedProvider.type)) { + if (!checkProviderNeedsInstallation(mergedProvider)) { mergedProvider.installed = true; } diff --git a/keep-ui/entities/alerts/model/index.ts b/keep-ui/entities/alerts/model/index.ts index 609c2bb2c..c98d7ed49 100644 --- a/keep-ui/entities/alerts/model/index.ts +++ b/keep-ui/entities/alerts/model/index.ts @@ -1 +1,2 @@ export * from "./models"; +export { useAvailableAlertFields } from "./useAvailableAlertFields"; diff --git a/keep-ui/entities/alerts/model/useAvailableAlertFields.ts b/keep-ui/entities/alerts/model/useAvailableAlertFields.ts new file mode 100644 index 000000000..631d10d9c --- /dev/null +++ b/keep-ui/entities/alerts/model/useAvailableAlertFields.ts @@ -0,0 +1,48 @@ +import { useSearchAlerts } from "@/utils/hooks/useSearchAlerts"; +import { useMemo } from "react"; + +const DAY = 3600 * 24; + +export const useAvailableAlertFields = ({ + timeframe = DAY, +}: { + timeframe?: number; +} = {}) => { + const defaultQuery = { + combinator: "or", + rules: [ + { + combinator: "and", + rules: [{ field: "source", operator: "=", value: "" }], + }, + { + combinator: "and", + rules: [{ field: "source", operator: "=", value: "" }], + }, + ], + }; + const { data: alertsFound = [], isLoading } = useSearchAlerts({ + query: defaultQuery, + timeframe, + }); + + const fields = useMemo(() => { + const getNestedKeys = (obj: any, prefix = ""): string[] => { + return Object.entries(obj).reduce((acc, [key, value]) => { + const newKey = prefix ? `${prefix}.${key}` : key; + if (value && typeof value === "object" && !Array.isArray(value)) { + return [...acc, ...getNestedKeys(value, newKey)]; + } + return [...acc, newKey]; + }, []); + }; + return [ + ...alertsFound.reduce>((acc, alert) => { + const alertKeys = getNestedKeys(alert); + return new Set([...acc, ...alertKeys]); + }, new Set()), + ]; + }, [alertsFound]); + + return { fields, isLoading }; +}; diff --git a/keep-ui/entities/workflows/model/types.ts b/keep-ui/entities/workflows/model/types.ts index 750c9e0b2..4e6ecff4d 100644 --- a/keep-ui/entities/workflows/model/types.ts +++ b/keep-ui/entities/workflows/model/types.ts @@ -1,7 +1,7 @@ import { Edge, Node } from "@xyflow/react"; import { Workflow } from "@/shared/api/workflows"; import { z } from "zod"; - +import { Provider } from "@/app/(keep)/providers/providers"; export type WorkflowMetadata = Pick; export type V2Properties = Record; @@ -90,13 +90,11 @@ export const V2ActionSchema = z.object({ z.union([z.string(), z.number(), z.boolean(), z.object({})]) ) .superRefine((withObj, ctx) => { - console.log(withObj, ctx); + // console.log(withObj, { "ctx.path": ctx.path }); // const actionParams = ctx.path[0].properties.actionParams; // const withKeys = Object.keys(withObj); - // // Check if all keys in 'with' are present in actionParams // const validKeys = withKeys.every((key) => actionParams.includes(key)); - // if (!validKeys) { // ctx.addIssue({ // code: z.ZodIssueCode.custom, @@ -311,6 +309,24 @@ export type StoreSet = ( | ((state: FlowState) => FlowState | Partial) ) => void; +export type ToolboxConfiguration = { + groups: ( + | { + name: "Triggers"; + steps: V2StepTrigger[]; + } + | { + name: string; + steps: Omit[]; + } + )[]; +}; + +export type ProvidersConfiguration = { + providers: Provider[]; + installedProviders: Provider[]; +}; + export interface FlowStateValues { workflowId: string | null; definition: DefinitionV2 | null; @@ -320,6 +336,8 @@ export interface FlowStateValues { selectedEdge: string | null; v2Properties: Record; toolboxConfiguration: ToolboxConfiguration | null; + providers: Provider[] | null; + installedProviders: Provider[] | null; isLayouted: boolean; isInitialized: boolean; @@ -359,16 +377,14 @@ export interface FlowState extends FlowStateValues { step: V2StepTemplate | V2StepTrigger, type: "node" | "edge" ) => string | null; - setToolBoxConfig: (config: ToolboxConfiguration) => void; + setProviders: (providers: Provider[]) => void; + setInstalledProviders: (providers: Provider[]) => void; setEditorOpen: (open: boolean) => void; updateSelectedNodeData: (key: string, value: any) => void; updateV2Properties: (properties: Record) => void; setSelectedNode: (id: string | null) => void; onNodesChange: (changes: any) => void; onEdgesChange: (changes: any) => void; - onConnect: (connection: any) => void; - onDragOver: (event: React.DragEvent) => void; - onDrop: (event: DragEvent, screenToFlowPosition: any) => void; setNodes: (nodes: FlowNode[]) => void; setEdges: (edges: Edge[]) => void; getNodeById: (id: string) => FlowNode | undefined; @@ -386,19 +402,11 @@ export interface FlowState extends FlowStateValues { }) => void; initializeWorkflow: ( workflowId: string | null, - toolboxConfiguration: ToolboxConfiguration + { providers, installedProviders }: ProvidersConfiguration ) => void; updateDefinition: () => void; + // Deprecated + onConnect: (connection: any) => void; + onDragOver: (event: React.DragEvent) => void; + onDrop: (event: DragEvent, screenToFlowPosition: any) => void; } -export type ToolboxConfiguration = { - groups: ( - | { - name: "Triggers"; - steps: V2StepTrigger[]; - } - | { - name: string; - steps: Omit[]; - } - )[]; -}; diff --git a/keep-ui/entities/workflows/model/validation.ts b/keep-ui/entities/workflows/model/validation.ts index 97822cf74..42a0e1da4 100644 --- a/keep-ui/entities/workflows/model/validation.ts +++ b/keep-ui/entities/workflows/model/validation.ts @@ -1,8 +1,11 @@ +import { Provider } from "@/app/(keep)/providers/providers"; import { Definition, V2Step } from "./types"; export type ValidationResult = [string, string]; -export const PROVIDERS_WITH_NO_CONFIG = ["console", "bash"]; +export const checkProviderNeedsInstallation = (providerObject: Provider) => { + return providerObject.config && Object.keys(providerObject.config).length > 0; +}; export function validateGlobalPure(definition: Definition): ValidationResult[] { const errors: ValidationResult[] = []; @@ -91,41 +94,75 @@ export function validateGlobalPure(definition: Definition): ValidationResult[] { return errors; } -export function validateStepPure(step: V2Step): string | null { - if (step.type.includes("condition-")) { +function validateProviderConfig( + providerType: string | undefined, + providerConfig: string, + providers: Provider[] | null | undefined, + installedProviders: Provider[] | null | undefined +) { + if (!providerConfig) { + return "No provider selected"; + } + const providerObject = providers?.find((p) => p.type === providerType); + + if (!providerObject) { + return "This type of provider is not found"; + } + // If config is not empty, it means that the provider needs installation + const doesProviderNeedInstallation = + checkProviderNeedsInstallation(providerObject); + + if ( + providerConfig && + doesProviderNeedInstallation && + installedProviders?.find( + (p) => (p.type === providerType && p.details?.name) === providerConfig + ) === undefined + ) { + return "This provider is not installed and you'll need to install it before executing this workflow."; + } + return null; +} + +export function validateStepPure( + step: V2Step, + providers: Provider[], + installedProviders: Provider[] +): string | null { + if (step.componentType === "switch") { if (!step.name) { return "Step/action name cannot be empty."; } - const branches = - step?.componentType === "switch" - ? step.branches - : { - true: [], - false: [], - }; - const onlyActions = branches?.true?.every((step: V2Step) => + const branches = step.branches || { + true: [], + false: [], + }; + const conditionHasActions = branches.true.length > 0; + if (!conditionHasActions) { + return "Conditions true branch must contain at least one action."; + } + const onlyActions = branches.true.every((step: V2Step) => step.type.includes("action-") ); if (!onlyActions) { return "Conditions can only contain actions."; } - const conditionHasActions = branches?.true - ? branches?.true.length > 0 - : false; - if (!conditionHasActions) { - return "Conditions must contain at least one action."; - } - const valid = conditionHasActions && onlyActions; - return valid ? null : "Conditions must contain at least one action."; + return null; } - if (step?.componentType === "task") { - if (!step?.name) { + if (step.componentType === "task") { + if (!step.name) { return "Step name cannot be empty."; } - const providerType = step?.type.split("-")[1]; - const providerConfig = (step?.properties.config as string)?.trim(); - if (!providerConfig && !PROVIDERS_WITH_NO_CONFIG.includes(providerType)) { - return "No provider selected"; + const providerType = step.type.split("-")[1]; + const providerConfig = (step.properties.config || "").trim(); + const providerError = validateProviderConfig( + providerType, + providerConfig, + providers, + installedProviders + ); + if (providerError) { + return providerError; } if ( !Object.values(step?.properties?.with || {}).some( diff --git a/keep-ui/entities/workflows/model/workflow-store.ts b/keep-ui/entities/workflows/model/workflow-store.ts index 32cf922a2..7b8abf04c 100644 --- a/keep-ui/entities/workflows/model/workflow-store.ts +++ b/keep-ui/entities/workflows/model/workflow-store.ts @@ -22,13 +22,13 @@ import { FlowState, FlowNode, Definition, - ToolboxConfiguration, V2StepTemplateSchema, V2EndStep, V2StartStep, V2StepTrigger, V2StepTemplate, V2StepTriggerSchema, + ProvidersConfiguration, } from "@/entities/workflows"; import { validateStepPure, validateGlobalPure } from "./validation"; import { getLayoutedWorkflowElements } from "../lib/getLayoutedWorkflowElements"; @@ -36,6 +36,12 @@ import { wrapDefinitionV2 } from "@/entities/workflows/lib/parser"; import { showErrorToast } from "@/shared/ui/utils/showErrorToast"; import { ZodError } from "zod"; import { fromError } from "zod-validation-error"; +import { + edgeCanAddStep, + edgeCanAddTrigger, + getToolboxConfiguration, +} from "@/features/workflows/builder/lib/utils"; +import { Provider } from "@/app/(keep)/providers/providers"; class WorkflowBuilderError extends Error { constructor(message: string) { super(message); @@ -87,6 +93,14 @@ function addNodeBetween( throw new WorkflowBuilderError("Edge not found"); } + if (isTriggerComponent && !edgeCanAddTrigger(edge.source, edge.target)) { + throw new WorkflowBuilderError("This edge cannot add trigger"); + } + + if (!isTriggerComponent && !edgeCanAddStep(edge.source, edge.target)) { + throw new WorkflowBuilderError("This edge cannot add step"); + } + let { source: sourceId, target: targetId } = edge || {}; if (!sourceId) { throw new WorkflowBuilderError("Source is not defined"); @@ -124,6 +138,7 @@ function addNodeBetween( if (sourceId === "trigger_start") { targetId = "trigger_end"; } + // for triggers, we use the id from the step, for steps we generate a new id const newNodeId = isTriggerComponent ? step.id : uuidv4(); const cloneStep = JSON.parse(JSON.stringify(step)); const newStep = { ...cloneStep, id: newNodeId }; @@ -225,6 +240,8 @@ const defaultState: FlowStateValues = { v2Properties: {}, editorOpen: false, toolboxConfiguration: null, + providers: null, + installedProviders: null, isInitialized: false, isLayouted: false, selectedEdge: null, @@ -262,6 +279,11 @@ export const useWorkflowStore = create()( step: V2StepTemplate | V2StepTrigger, type: "node" | "edge" ) => { + console.log("addNodeBetween", { + nodeOrEdgeId, + step, + type, + }); const newNodeId = addNodeBetween(nodeOrEdgeId, step, type, set, get); set({ selectedNode: newNodeId, selectedEdge: null }); return newNodeId ?? null; @@ -271,6 +293,11 @@ export const useWorkflowStore = create()( step: V2StepTemplate | V2StepTrigger, type: "node" | "edge" ) => { + console.log("addNodeBetweenSafe", { + nodeOrEdgeId, + step, + type, + }); try { const newNodeId = addNodeBetween(nodeOrEdgeId, step, type, set, get); set({ selectedNode: newNodeId, selectedEdge: null }); @@ -288,8 +315,9 @@ export const useWorkflowStore = create()( return null; } }, - setToolBoxConfig: (config: ToolboxConfiguration) => - set({ toolboxConfiguration: config }), + setProviders: (providers: Provider[]) => set({ providers }), + setInstalledProviders: (installedProviders: Provider[]) => + set({ installedProviders }), setEditorOpen: (open) => set({ editorOpen: open }), updateSelectedNodeData: (key, value) => { const currentSelectedNode = get().selectedNode; @@ -339,7 +367,24 @@ export const useWorkflowStore = create()( // Check each step's validity for (const step of sequence) { - const error = validateStepPure(step); + const error = validateStepPure( + step, + get().providers ?? [], + get().installedProviders ?? [] + ); + if (step.componentType === "switch") { + [...step.branches.true, ...step.branches.false].forEach((branch) => { + const error = validateStepPure( + branch, + get().providers ?? [], + get().installedProviders ?? [] + ); + if (error) { + validationErrors[branch.name || branch.id] = error; + isValid = false; + } + }); + } if (error) { validationErrors[step.name || step.id] = error; isValid = false; @@ -577,12 +622,12 @@ export const useWorkflowStore = create()( getNextEdge: (nodeId: string) => { const node = get().getNodeById(nodeId); if (!node) { - throw new WorkflowBuilderError("getNextEdge::Node not found"); + throw new WorkflowBuilderError("Node not found"); } // TODO: handle multiple edges const edges = get().edges.filter((e) => e.source === nodeId); if (!edges.length) { - throw new WorkflowBuilderError("getNextEdge::Edge not found"); + throw new WorkflowBuilderError("Edge not found"); } if (node.data.componentType === "switch") { // If the node is a switch, return the second edge, because "true" is the second edge @@ -600,8 +645,14 @@ export const useWorkflowStore = create()( }) => onLayout(params, set, get), initializeWorkflow: ( workflowId: string | null, - toolboxConfiguration: ToolboxConfiguration - ) => initializeWorkflow(workflowId, toolboxConfiguration, set, get), + { providers, installedProviders }: ProvidersConfiguration + ) => + initializeWorkflow( + workflowId, + { providers, installedProviders }, + set, + get + ), })) ); @@ -646,9 +697,9 @@ function onLayout( }); } -async function initializeWorkflow( +function initializeWorkflow( workflowId: string | null, - toolboxConfiguration: ToolboxConfiguration, + { providers, installedProviders }: ProvidersConfiguration, set: StoreSet, get: StoreGet ) { @@ -660,6 +711,8 @@ async function initializeWorkflow( let parsedWorkflow = definition?.value; const name = parsedWorkflow?.properties?.name; + const toolboxConfiguration = getToolboxConfiguration(providers); + const fullSequence = [ { id: "start", @@ -689,6 +742,8 @@ async function initializeWorkflow( nodes, edges, v2Properties: { ...(parsedWorkflow?.properties ?? {}), name }, + providers, + installedProviders, toolboxConfiguration, isLoading: false, isInitialized: true, diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx index 51d046090..0e6e75155 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/AddStepUI.tsx @@ -31,27 +31,24 @@ export const AddStepUI = ({ result, respond, }: AddStepUIProps) => { - const { addNodeBetween, setSelectedNode } = useWorkflowStore(); + const { addNodeBetween, setSelectedNode, getNodeById } = useWorkflowStore(); + + const selectNode = () => { + const node = getNodeById(addBeforeNodeId); + if (node) { + setSelectedNode(node.id); + } + }; const onAdd = () => { try { - // Hack to get the conditions to work. TODO: make it more straightforward - // FIX: it fails for ordinary steps - // if (addAfterEdgeId.includes("condition")) { - // const edge = getEdgeById(addAfterEdgeId); - // if (!edge) { - // throw new Error("Edge not found"); - // } - // const nodeId = edge!.source; - // addNodeBetween(nodeId, step, "node"); - // } else { addNodeBetween(addBeforeNodeId, step, "node"); - // } respond?.({ status: "complete", message: "Step added", }); } catch (e) { + console.error("Step not added", e); respond?.({ status: "error", error: e, @@ -70,6 +67,13 @@ export const AddStepUI = ({ if (status === "complete") { return (
+
+ Do you want to add this action before node {addBeforeNodeId} ( + + )? +
Do you want to add this action before node {addBeforeNodeId} ( - )? diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/SuggestionStatus.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/SuggestionStatus.tsx index c2e72dbd8..cc361c9e8 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/SuggestionStatus.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/SuggestionStatus.tsx @@ -1,8 +1,8 @@ -import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { - ExclamationCircleIcon, + CheckCircleIcon, + ExclamationTriangleIcon, NoSymbolIcon, -} from "@heroicons/react/24/outline"; +} from "@heroicons/react/20/solid"; export type SuggestionStatus = "complete" | "error" | "declined"; export type SuggestionResult = { @@ -29,7 +29,7 @@ export const SuggestionStatus = ({ if (status === "error") { return (

- + {message}

); diff --git a/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx b/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx index db6bd9094..58852c5aa 100644 --- a/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx +++ b/keep-ui/features/workflows/builder/ui/BuilderChat/builder-chat.tsx @@ -25,7 +25,6 @@ import { useCopilotReadable, } from "@copilotkit/react-core"; import { Button, Link } from "@/components/ui"; -import { generateStepDefinition } from "@/app/(keep)/workflows/builder/_actions/getStepJson"; import { GENERAL_INSTRUCTIONS } from "@/app/(keep)/workflows/builder/_constants"; import { showSuccessToast } from "@/shared/ui/utils/showSuccessToast"; import { WF_DEBUG_INFO } from "../debug-settings"; @@ -38,56 +37,11 @@ import BuilderChatPlaceholder from "./ai-workflow-placeholder.png"; import Image from "next/image"; import { Edge } from "@xyflow/react"; import { SuggestionResult } from "./SuggestionStatus"; -import { useSearchAlerts } from "@/utils/hooks/useSearchAlerts"; -import "@copilotkit/react-ui/styles.css"; -import "./chat.css"; import Skeleton from "react-loading-skeleton"; import { AddStepUI } from "./AddStepUI"; -import { - edgeCanAddStep, - edgeCanAddTrigger, - edgeCanHaveAddButton, -} from "../../lib/utils"; - -const useAlertKeys = () => { - const defaultQuery = { - combinator: "or", - rules: [ - { - combinator: "and", - rules: [{ field: "source", operator: "=", value: "" }], - }, - { - combinator: "and", - rules: [{ field: "source", operator: "=", value: "" }], - }, - ], - }; - const { data: alertsFound = [], isLoading } = useSearchAlerts({ - query: defaultQuery, - timeframe: 3600 * 24, - }); - - const keys = useMemo(() => { - const getNestedKeys = (obj: any, prefix = ""): string[] => { - return Object.entries(obj).reduce((acc, [key, value]) => { - const newKey = prefix ? `${prefix}.${key}` : key; - if (value && typeof value === "object" && !Array.isArray(value)) { - return [...acc, ...getNestedKeys(value, newKey)]; - } - return [...acc, newKey]; - }, []); - }; - return [ - ...alertsFound.reduce>((acc, alert) => { - const alertKeys = getNestedKeys(alert); - return new Set([...acc, ...alertKeys]); - }, new Set()), - ]; - }, [alertsFound]); - - return { keys, isLoading }; -}; +import { useAvailableAlertFields } from "@/entities/alerts/model"; +import "@copilotkit/react-ui/styles.css"; +import "./chat.css"; interface BuilderChatProps { definition: DefinitionV2; @@ -143,7 +97,6 @@ export function BuilderChat({ nodes, edges, toolboxConfiguration, - addNodeBetween, selectedEdge, selectedNode, deleteNodes, @@ -414,13 +367,13 @@ export function BuilderChat({ }, }); - const { keys } = useAlertKeys(); + const { fields } = useAvailableAlertFields(); const possibleAlertProperties = useMemo(() => { - if (!keys || keys.length === 0) { + if (!fields || fields.length === 0) { return ["source", "severity", "status", "message", "timestamp"]; } - return keys?.map((key) => key.split(".").pop()); - }, [keys]); + return fields?.map((field) => field.split(".").pop()); + }, [fields]); useCopilotReadable({ description: "Possible alert properties", @@ -480,6 +433,7 @@ export function BuilderChat({ }); } + // TODO: if the trigger is already in the workflow, ask to replace it return AddTriggerUI({ status: "executing", args: argsToPass, @@ -615,7 +569,8 @@ export function BuilderChat({ useCopilotAction({ name: "addAction", - description: `Add an action to the workflow. Actions are sending notifications to a provider.`, + description: + "Add an action to the workflow. Actions are sending notifications to a provider.", parameters: [ { name: "withActionParams", @@ -663,10 +618,10 @@ export function BuilderChat({ }, { name: "addBeforeNodeId", - description: `The id of the node to add the condition before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. For condition branches: -- Must end with '__empty-true' for true branch -- Must end with '__empty-false' for false branch -Example: 'node_123__empty-true'`, + description: `The id of the node to add the action before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. If adding to a condition branch, search for node id: +- Must end with '__empty_true' for true branch +- Must end with '__empty_false' for false branch +Example: 'node_123__empty_true'`, type: "string", required: true, }, @@ -676,6 +631,7 @@ Example: 'node_123__empty-true'`, return ; } const action = getActionStepFromCopilotAction(args); + console.log("action", action); if (!action) { respond?.({ status: "error", @@ -779,10 +735,10 @@ Example: 'node_123__empty-true'`, }, { name: "addBeforeNodeId", - description: `The id of the node to add the condition before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. For condition branches: -- Must end with '__empty-true' for true branch -- Must end with '__empty-false' for false branch -Example: 'node_123__empty-true'`, + description: `The id of the node to add the step before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. If adding to a condition branch, search for node id: +- Must end with '__empty_true' for true branch +- Must end with '__empty_false' for false branch +Example: 'node_123__empty_true'`, type: "string", required: true, }, @@ -876,10 +832,10 @@ Example: 'node_123__empty-true'`, }, { name: "addBeforeNodeId", - description: `The id of the node to add the condition before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. For condition branches: -- Must end with '__empty-true' for true branch -- Must end with '__empty-false' for false branch -Example: 'node_123__empty-true'`, + description: `The id of the node to add the condition before. For workflows with no steps, should be 'end'. Cannot be a node with componentType: 'trigger'. If adding to a condition branch, search for node id: +- Must end with '__empty_true' for true branch +- Must end with '__empty_false' for false branch +Example: 'node_123__empty_true'`, type: "string", required: true, }, @@ -975,13 +931,10 @@ Example: 'node_123__empty-true'`, // }); const [debugInfoVisible, setDebugInfoVisible] = useState(false); - const chatInstructions = useMemo(() => { - return ( - GENERAL_INSTRUCTIONS + - `Here is the list of providers that are installed: ${installedProviders.map((p) => `type: ${p.type}, id: ${p.id}`).join(", ")}. If you you need to use a provider that is not installed, add step, but mention to user that you need to add the provider first.` + - "Then asked to create a complete workflow, you break down the workflow into steps, outline the steps, show them to user, and then iterate over the steps one by one, generate step definition, show it to user to decide if they want to add them to the workflow." - ); - }, [installedProviders]); + const chatInstructions = + GENERAL_INSTRUCTIONS + + `If you you need to use a provider that is not installed, add step, but mention to user that you need to add the provider first. + Then asked to create a complete workflow, you break down the workflow into steps, outline the steps, show them to user, and then iterate over the steps one by one, generate step definition, show it to user to decide if they want to add them to the workflow.`; return (
-
{debugInfoVisible && ( diff --git a/keep-ui/features/workflows/builder/ui/Editor/ReactFlowEditor.tsx b/keep-ui/features/workflows/builder/ui/Editor/ReactFlowEditor.tsx index f4514de85..efd8aeb60 100644 --- a/keep-ui/features/workflows/builder/ui/Editor/ReactFlowEditor.tsx +++ b/keep-ui/features/workflows/builder/ui/Editor/ReactFlowEditor.tsx @@ -8,45 +8,45 @@ import { WorkflowToolbox } from "../WorkflowToolbox"; import { WorkflowEditorV2 } from "./WorkflowEditor"; import { TriggerEditor } from "./TriggerEditor"; import { WorkflowStatus } from "../workflow-status"; +import { triggerTypes } from "../../lib/utils"; const ReactFlowEditor = () => { const { selectedNode, selectedEdge, setEditorOpen, getNodeById, editorOpen } = useWorkflowStore(); - const stepEditorRef = useRef(null); const containerRef = useRef(null); - const isTrigger = ["interval", "manual", "alert", "incident"].includes( - selectedNode || "" - ); + const dividerRef = useRef(null); - useEffect(() => { - setEditorOpen(true); - if (!selectedNode && !selectedEdge) { - return; - } - // TODO: refactor to use ref callback function, e.g. ref={(el) => { - // if (el) { - // el.scrollIntoView({ behavior: "smooth", block: "start" }); - // } - // }} - // Scroll the StepEditorV2 into view when the editor is opened - const timer = setTimeout(() => { - if (containerRef.current && stepEditorRef.current) { + const isTrigger = triggerTypes.includes(selectedNode || ""); + const isStepEditor = !selectedNode?.includes("empty") && !isTrigger; + + useEffect( + function scrollRelevantEditorIntoView() { + if (!selectedNode && !selectedEdge) { + return; + } + // Scroll the view to the divider into view when the editor is opened, so the relevant editor is visible + const timer = setTimeout(() => { + if (!containerRef.current || !dividerRef.current) { + return; + } const containerRect = containerRef.current.getBoundingClientRect(); - const stepEditorRect = stepEditorRef.current.getBoundingClientRect(); - // Check if StepEditorV2 is already at the top of the container - const isAtTop = stepEditorRect.top <= containerRect.top; + const dividerRect = dividerRef.current.getBoundingClientRect(); + // Check if the divider is already at the top of the container + const isAtTop = dividerRect.top <= containerRect.top; - if (!isAtTop) { - // Scroll the StepEditorV2 into view - stepEditorRef.current.scrollIntoView({ - behavior: "smooth", - block: "start", - }); + if (isAtTop) { + return; } - } - }, 100); - return () => clearTimeout(timer); // Cleanup the timer on unmount - }, [selectedNode, selectedEdge]); + // Scroll the divider into view + dividerRef.current.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }, 100); + return () => clearTimeout(timer); // Cleanup the timer on unmount + }, + [selectedNode, selectedEdge] + ); const initialFormData = useMemo(() => { if (!selectedNode) { @@ -57,8 +57,7 @@ const ReactFlowEditor = () => { return { name, type, properties }; }, [selectedNode]); - const isStepEditor = - !selectedNode?.includes("empty") && !isTrigger && initialFormData; + const showDivider = Boolean(selectedNode || selectedEdge); return (
@@ -94,11 +93,9 @@ const ReactFlowEditor = () => {
- {(isStepEditor || isTrigger) && ( - - )} + {showDivider && } {isTrigger && } - {isStepEditor && ( + {isStepEditor && initialFormData && ( - p.type === providerType && p.config && Object.keys(p.config).length > 0 - ) ?? false; - - if ( - providerConfig && - isThisProviderNeedsInstallation && - installedProviders?.find( - (p) => (p.type === providerType && p.details?.name) === providerConfig - ) === undefined - ) { - return "This provider is not installed and you'll need to install it before executing this workflow."; - } - return ""; -} - function InstallProviderButton({ providerType }: { providerType: string }) { const { data: { providers } = {} } = useProviders(); const revalidateMultiple = useRevalidateMultiple(); @@ -163,21 +122,24 @@ function KeepSetupProviderEditor({ updateProperty, providerType, providerError, - providerNameError, }: KeepEditorProps & { providerError?: string | null; providerNameError?: string | null; }) { const { data: { providers, installed_providers: installedProviders } = {} } = useProviders(); + const providerObject = + providers?.find((p) => p.type === providerType) ?? null; const installedProviderByType = installedProviders?.filter( (p) => p.type === providerType ); - const providerConfig = getProviderConfig(providerType, properties); - - const providerObject = - providers?.find((p) => p.type === providerType) ?? null; + const doesProviderNeedInstallation = providerObject + ? checkProviderNeedsInstallation(providerObject) + : false; + const providerConfig = !doesProviderNeedInstallation + ? "default-" + providerType + : (properties.config ?? "")?.trim(); const isCustomConfig = installedProviderByType?.find((p) => p.details?.name === providerConfig) === @@ -187,6 +149,10 @@ function KeepSetupProviderEditor({ isCustomConfig ? "enter-manually" : (providerConfig ?? "") ); + const isGeneralError = providerError?.includes("No provider selected"); + const inputError = + providerError && !isGeneralError ? providerError : undefined; + const handleSelectChange = (value: string) => { setSelectValue(value); if (value === "enter-manually" || value === "add-new") { @@ -215,12 +181,12 @@ function KeepSetupProviderEditor({ ); }; - if (PROVIDERS_WITH_NO_CONFIG.includes(providerType ?? "")) { + if (!doesProviderNeedInstallation) { return (
{providerType} provider does not - require configuration + require installation
); @@ -230,7 +196,9 @@ function KeepSetupProviderEditor({
Select provider - {providerError && {providerError}} + {isGeneralError && ( + {providerError} + )}