diff --git a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx index 6848dd13c..19e192798 100644 --- a/keep-ui/app/workflows/builder/ReactFlowEditor.tsx +++ b/keep-ui/app/workflows/builder/ReactFlowEditor.tsx @@ -21,15 +21,24 @@ const ReactFlowEditor = ({ }; onDefinitionChange: (def: Definition) => void }) => { - const { selectedNode, changes, v2Properties, nodes, edges, setOpneGlobalEditor, synced, setSynced } = useStore(); + const { selectedNode, changes, v2Properties, nodes, edges, setOpneGlobalEditor, synced, setSynced, setCanDeploy } = useStore(); const [isOpen, setIsOpen] = useState(false); const stepEditorRef = useRef(null); const containerRef = useRef(null); const isTrigger = ['interval', 'manual', 'alert'].includes(selectedNode || '') + const saveRef = useRef(false); + useEffect(()=>{ + if(saveRef.current && synced){ + setCanDeploy(true); + saveRef.current = false; + } + }, [saveRef?.current, synced]) + useEffect(() => { setIsOpen(true); if (selectedNode) { + saveRef.current = false; const timer = setTimeout(() => { if (isTrigger) { setOpneGlobalEditor(true); @@ -114,9 +123,16 @@ const ReactFlowEditor = ({
- + {!selectedNode?.includes('empty') && !isTrigger && } - {!selectedNode?.includes('empty') && !isTrigger && } + {!selectedNode?.includes('empty') && !isTrigger && }
diff --git a/keep-ui/app/workflows/builder/builder-store.tsx b/keep-ui/app/workflows/builder/builder-store.tsx index e19b7d6ea..7adae1287 100644 --- a/keep-ui/app/workflows/builder/builder-store.tsx +++ b/keep-ui/app/workflows/builder/builder-store.tsx @@ -149,6 +149,8 @@ export type FlowState = { errorNode: string | null; synced: boolean; setSynced: (synced: boolean) => void; + canDeploy: boolean; + setCanDeploy: (deploy: boolean) => void; }; @@ -260,6 +262,8 @@ const useStore = create((set, get) => ({ firstInitilisationDone: false, errorNode: null, synced: true, + canDeploy: false, + setCanDeploy: (deploy)=>set({canDeploy: deploy}), setSynced: (sync) => set({ synced: sync }), setErrorNode: (id) => set({ errorNode: id }), setFirstInitilisationDone: (firstInitilisationDone) => set({ firstInitilisationDone }), @@ -291,7 +295,7 @@ const useStore = create((set, get) => ({ }); set({ nodes: updatedNodes, - changes: get().changes + 1 + changes: get().changes + 1, }); } }, diff --git a/keep-ui/app/workflows/builder/builder.tsx b/keep-ui/app/workflows/builder/builder.tsx index 020d13bf4..08d60a4c5 100644 --- a/keep-ui/app/workflows/builder/builder.tsx +++ b/keep-ui/app/workflows/builder/builder.tsx @@ -27,6 +27,7 @@ import { WorkflowExecution, WorkflowExecutionFailure } from "./types"; import ReactFlowBuilder from "./ReactFlowBuilder"; import { ReactFlowProvider } from "@xyflow/react"; import useStore, { ReactFlowDefinition, V2Step, Definition as FlowDefinition } from "./builder-store"; +import { toast } from "react-toastify"; interface Props { loadedAlertFile: string | null; @@ -76,7 +77,7 @@ function Builder({ const [compiledAlert, setCompiledAlert] = useState(null); const searchParams = useSearchParams(); - const { setErrorNode } = useStore(); + const { errorNode, setErrorNode, canDeploy, synced } = useStore(); const setStepValidationErrorV2 = (step: V2Step, error: string | null) => { setStepValidationError(error); @@ -210,7 +211,12 @@ function Builder({ }, [triggerRun]); useEffect(() => { + if (triggerSave) { + if(!synced) { + toast('Please save the previous step or wait while properties sync with the workflow.'); + return; + } if (workflowId) { updateWorkflow(); } else { @@ -220,6 +226,20 @@ function Builder({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [triggerSave]); + useEffect(()=>{ + if (canDeploy && !errorNode && definition.isValid) { + if(!synced) { + toast('Please save the previous step or wait while properties sync with the workflow.'); + return; + } + if (workflowId) { + updateWorkflow(); + } else { + addWorkflow(); + } + } + }, [canDeploy, errorNode, definition?.isValid]) + useEffect(() => { enableGenerate( (definition.isValid && diff --git a/keep-ui/app/workflows/builder/editors.tsx b/keep-ui/app/workflows/builder/editors.tsx index 53283f26b..2670b6904 100644 --- a/keep-ui/app/workflows/builder/editors.tsx +++ b/keep-ui/app/workflows/builder/editors.tsx @@ -13,20 +13,17 @@ import { KeyIcon } from "@heroicons/react/20/solid"; import { Provider } from "app/providers/providers"; import { BackspaceIcon, - BellSnoozeIcon, - ClockIcon, FunnelIcon, - HandRaisedIcon, } from "@heroicons/react/24/outline"; import useStore, { V2Properties } from "./builder-store"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; function EditorLayout({ children }: { children: React.ReactNode }) { return
{children}
; } -export function GlobalEditorV2({synced}: {synced: boolean}) { +export function GlobalEditorV2({ synced, saveRef }: { synced: boolean, saveRef: React.MutableRefObject; }) { const { v2Properties: properties, updateV2Properties: setProperty, selectedNode } = useStore(); return ( @@ -45,6 +42,7 @@ export function GlobalEditorV2({synced}: {synced: boolean}) { properties={properties} setProperties={setProperty} selectedNode={selectedNode} + saveRef={saveRef} /> ); @@ -58,7 +56,7 @@ interface keepEditorProps { installedProviders?: Provider[] | null | undefined; providerType?: string; type?: string; - isV2?:boolean + isV2?: boolean } function KeepStepEditor({ @@ -142,7 +140,7 @@ function KeepStepEditor({ placeholder="Enter provider name manually" onChange={(e: any) => updateProperty("config", e.target.value)} className="my-2.5" - value={providerConfig} + value={providerConfig || ""} error={ providerConfig !== "" && providerConfig !== undefined && @@ -151,14 +149,13 @@ function KeepStepEditor({ (p) => p.details?.name === providerConfig ) === undefined } - errorMessage={`${ - providerConfig && 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
@@ -168,7 +165,7 @@ function KeepStepEditor({ placeholder="If Condition" onValueChange={(value) => updateProperty("if", value)} className="mb-2.5" - value={properties?.if as string} + value={properties?.if || "" as string} />
{uniqueParams @@ -186,7 +183,7 @@ function KeepStepEditor({ placeholder={key} onChange={propertyChanged} className="mb-2.5" - value={currentPropertyValue} + value={currentPropertyValue || ""} /> ); @@ -258,11 +255,13 @@ function KeepForeachEditor({ properties, updateProperty }: keepEditorProps) { function WorkflowEditorV2({ properties, setProperties, - selectedNode + selectedNode, + saveRef }: { properties: V2Properties; setProperties: (updatedProperties: V2Properties) => void; selectedNode: string | null; + saveRef: React.MutableRefObject; }) { const isTrigger = ['interval', 'manual', 'alert'].includes(selectedNode || '') @@ -271,6 +270,9 @@ function WorkflowEditorV2({ const currentFilters = properties.alert || {}; const updatedFilters = { ...currentFilters, [filter]: value }; setProperties({ ...properties, alert: updatedFilters }); + if (saveRef.current) { + saveRef.current = false + } }; const addFilter = () => { @@ -285,8 +287,21 @@ function WorkflowEditorV2({ const currentFilters = { ...properties.alert }; delete currentFilters[filter]; setProperties({ ...properties, alert: currentFilters }); + if (saveRef.current) { + saveRef.current = false + } }; + const handleChange = (key: string, value: string) => { + setProperties({ + ...properties, + [key]: value, + }); + if (saveRef.current) { + saveRef.current = false + } + } + const propertyKeys = Object.keys(properties).filter( (k) => k !== "isLocked" && k !== "id" ); @@ -298,118 +313,112 @@ function WorkflowEditorV2({ const isTrigger = ["manual", "alert", 'interval'].includes(key) ; renderDivider = isTrigger && key === selectedNode ? !renderDivider : false; return ( -
- {renderDivider && } - {((key === selectedNode) || (!isTrigger)) && {key}} - - {(() => { - switch (key) { - case "manual": - return ( - selectedNode === "manual" && ( -
- - setProperties({ - ...properties, - [key]: e.target.checked ? "true" : "false", - }) - } - disabled={true} - /> -
- ) - ); - - case "alert": - return ( - selectedNode === "alert" && ( - <> -
- -
- {properties.alert && - Object.keys(properties.alert as {}).map((filter) => { - return ( - <> - {filter} -
- - updateAlertFilter(filter, e.target.value) - } - value={(properties.alert as any)[filter] as string} - /> - deleteFilter(filter)} - /> -
- - ); - })} - - ) - ); - - case "interval": - return ( - selectedNode === "interval" && ( - - setProperties({ ...properties, [key]: e.target.value }) - } - value={properties[key] as string} - /> - ) - ); - case "disabled": - return ( -
- - setProperties({ - ...properties, - [key]: e.target.checked ? "true" : "false", - }) - } - /> -
- ); - default: - return ( +
+ {renderDivider && } + {((key === selectedNode) || (!isTrigger)) && {key}} + + {(() => { + switch (key) { + case "manual": + return ( + selectedNode === "manual" && ( +
+ + handleChange(key, e.target.checked ? "true" : "false") + } + disabled={true} + /> +
+ ) + ); + + case "alert": + return ( + selectedNode === "alert" && ( + <> +
+ +
+ {properties.alert && + Object.keys(properties.alert as {}).map((filter) => { + return ( + <> + {filter} +
- setProperties({ ...properties, [key]: e.target.value }) - } - value={properties[key] as string} + key={filter} + placeholder={`Set alert ${filter}`} + onChange={(e: any) => + updateAlertFilter(filter, e.target.value) + } + value={(properties.alert as any)[filter] || "" as string} + /> + deleteFilter(filter)} /> - ); +
+ + ); + })} + + ) + ); + + case "interval": + return ( + selectedNode === "interval" && ( + + handleChange(key, e.target.value) + } + value={properties[key] || "" as string} + /> + ) + ); + case "disabled": + return ( +
+ + handleChange(key, e.target.checked ? "true" : "false") + } + /> +
+ ); + default: + return ( + + handleChange(key, e.target.value) } - })()} -
- ); - + value={properties[key] || "" as string} + /> + ); + } + })()} +
+ ); })} ); @@ -420,25 +429,28 @@ function WorkflowEditorV2({ export function StepEditorV2({ providers, installedProviders, - setSynced + setSynced, + saveRef }: { providers: Provider[] | undefined | null; installedProviders?: Provider[] | undefined | null; - setSynced: (sync:boolean) => void; + setSynced: (sync: boolean) => void; + saveRef: React.MutableRefObject; }) { - const [formData, setFormData] = useState<{ name?: string; properties?: V2Properties, type?:string }>({}); + const [formData, setFormData] = useState<{ name?: string; properties?: V2Properties, type?: string }>({}); const { selectedNode, updateSelectedNodeData, - setOpneGlobalEditor, - getNodeById + getNodeById, } = useStore(); + const deployRef = useRef(null); + useEffect(() => { if (selectedNode) { const { data } = getNodeById(selectedNode) || {}; const { name, type, properties } = data || {}; - setFormData({ name, type , properties }); + setFormData({ name, type, properties }); } }, [selectedNode, getNodeById]); @@ -457,6 +469,9 @@ export function StepEditorV2({ properties: { ...formData.properties, [key]: value }, }); setSynced(false); + if (saveRef.current) { + saveRef.current = false; + } }; @@ -464,6 +479,10 @@ export function StepEditorV2({ // Finalize the changes before saving updateSelectedNodeData('name', formData.name); updateSelectedNodeData('properties', formData.properties); + setSynced(false); + if (saveRef && deployRef?.current?.checked) { + saveRef.current = true; + } }; const type = formData ? formData.type?.includes("step-") || formData.type?.includes("action-") : ""; @@ -479,7 +498,7 @@ export function StepEditorV2({ value={formData.name || ''} onChange={handleInputChange} /> - {type && formData.properties ? ( + {type && formData.properties ? ( ) : null} +
+ Deploy + +
);