From ed5fd7f86247585856394ef70475fc9c54c9bfca Mon Sep 17 00:00:00 2001 From: Gabriel Almeida Date: Tue, 2 May 2023 16:54:40 -0300 Subject: [PATCH] feat(GenericNode): add debounced validation of node on change to improve performance and user experience feat(utils.ts): add debounce function to debounce function calls --- .../src/CustomNodes/GenericNode/index.tsx | 375 +++++++++--------- src/frontend/src/utils.ts | 18 +- 2 files changed, 208 insertions(+), 185 deletions(-) diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index fb9771be8..877958f54 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -1,9 +1,9 @@ import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/24/outline"; import { - classNames, - nodeColors, - nodeIcons, - snakeToNormalCase, + classNames, + nodeColors, + nodeIcons, + snakeToNormalCase, } from "../../utils"; import ParameterComponent from "./components/parameterComponent"; import { typesContext } from "../../contexts/typesContext"; @@ -14,144 +14,154 @@ import { PopUpContext } from "../../contexts/popUpContext"; import NodeModal from "../../modals/NodeModal"; import { useCallback } from "react"; import { TabsContext } from "../../contexts/tabsContext"; +import { debounce } from "../../utils"; export default function GenericNode({ - data, - selected, + data, + selected, }: { - data: NodeDataType; - selected: boolean; + data: NodeDataType; + selected: boolean; }) { - const { setErrorData } = useContext(alertContext); - const showError = useRef(true); - const { types, deleteNode } = useContext(typesContext); - const { openPopUp } = useContext(PopUpContext); - const Icon = nodeIcons[types[data.type]]; - const [validationStatus, setValidationStatus] = useState("idle"); - // State for outline color - const [isValid, setIsValid] = useState(false); - const {save} = useContext(TabsContext) - const { reactFlowInstance } = useContext(typesContext); - const [params, setParams] = useState([]); + const { setErrorData } = useContext(alertContext); + const showError = useRef(true); + const { types, deleteNode } = useContext(typesContext); + const { openPopUp } = useContext(PopUpContext); + const Icon = nodeIcons[types[data.type]]; + const [validationStatus, setValidationStatus] = useState("idle"); + // State for outline color + const [isValid, setIsValid] = useState(false); + const { save } = useContext(TabsContext); + const { reactFlowInstance } = useContext(typesContext); + const [params, setParams] = useState([]); - console.log(); + console.log(); - useEffect(() => { - if (reactFlowInstance) { - setParams(Object.values(reactFlowInstance.toObject())); - } - }, [save]); + useEffect(() => { + if (reactFlowInstance) { + setParams(Object.values(reactFlowInstance.toObject())); + } + }, [save]); - useEffect(() => { - try { - fetch(`/validate/node/${data.id}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(reactFlowInstance.toObject()), - }).then((response) => { - console.log(response.status, response.body); + const validateNode = useCallback( + debounce(async () => { + try { + const response = await fetch(`/validate/node/${data.id}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(reactFlowInstance.toObject()), + }); - if (response.status === 200) { - setValidationStatus("success"); - } else if (response.status === 500) { - setValidationStatus("error"); - } - }); - } catch (error) { - console.error("Error validating node:", error); - setValidationStatus("error"); - } - }, [params]); + console.log(response.status, response.body); - useEffect(() => { - if (validationStatus === "success") { - setIsValid(true); - } else { - setIsValid(false); - } - }, [validationStatus]); + if (response.status === 200) { + setValidationStatus("success"); + } else if (response.status === 500) { + setValidationStatus("error"); + } + } catch (error) { + console.error("Error validating node:", error); + setValidationStatus("error"); + } + }, 1000), // Adjust the debounce delay (500ms) as needed + [reactFlowInstance, data.id] + ); + useEffect(() => { + if (params.length > 0) { + validateNode(); + } + }, [params, validateNode]); - if (!Icon) { - if (showError.current) { - setErrorData({ - title: data.type - ? `The ${data.type} node could not be rendered, please review your json file` - : "There was a node that can't be rendered, please review your json file", - }); - showError.current = false; - } - deleteNode(data.id); - return; - } + useEffect(() => { + if (validationStatus === "success") { + setIsValid(true); + } else { + setIsValid(false); + } + }, [validationStatus]); - return ( -
-
-
- -
{data.type}
-
-
- - -
-
+ if (!Icon) { + if (showError.current) { + setErrorData({ + title: data.type + ? `The ${data.type} node could not be rendered, please review your json file` + : "There was a node that can't be rendered, please review your json file", + }); + showError.current = false; + } + deleteNode(data.id); + return; + } -
-
- {data.node.description} -
+ return ( +
+
+
+ +
{data.type}
+
+
+ + +
+
- <> - {Object.keys(data.node.template) - .filter((t) => t.charAt(0) !== "_") - .map((t: string, idx) => ( -
- {/* {idx === 0 ? ( +
+
+ {data.node.description} +
+ + <> + {Object.keys(data.node.template) + .filter((t) => t.charAt(0) !== "_") + .map((t: string, idx) => ( +
+ {/* {idx === 0 ? (
)} */} - {data.node.template[t].show && !data.node.template[t].advanced ? ( - - ) : ( - <> - )} -
- ))} -
- {" "} -
- {/*
+ {data.node.template[t].show && + !data.node.template[t].advanced ? ( + + ) : ( + <> + )} +
+ ))} +
+ {" "} +
+ {/*
Output
*/} - - -
-
- ); + + +
+
+ ); } diff --git a/src/frontend/src/utils.ts b/src/frontend/src/utils.ts index b45f69d11..9e5bceab6 100644 --- a/src/frontend/src/utils.ts +++ b/src/frontend/src/utils.ts @@ -399,8 +399,11 @@ export function removeApiKeys(flow: FlowType): FlowType { return cleanFLow; } -export function updateObject>(reference: T, objectToUpdate: T): T { - let clonedObject = _.cloneDeep(objectToUpdate) +export function updateObject>( + reference: T, + objectToUpdate: T +): T { + let clonedObject = _.cloneDeep(objectToUpdate); // Loop through each key in the object to update for (const key in clonedObject) { // If the key is not in the reference object, delete it @@ -416,4 +419,13 @@ export function updateObject>(reference: T, object } } return clonedObject; -} \ No newline at end of file +} + +export function debounce(func, wait) { + let timeout; + return function (...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +}