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
This commit is contained in:
Gabriel Almeida 2023-05-02 16:54:40 -03:00
commit ed5fd7f862
2 changed files with 208 additions and 185 deletions

View file

@ -1,9 +1,9 @@
import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/24/outline"; import { Cog6ToothIcon, TrashIcon } from "@heroicons/react/24/outline";
import { import {
classNames, classNames,
nodeColors, nodeColors,
nodeIcons, nodeIcons,
snakeToNormalCase, snakeToNormalCase,
} from "../../utils"; } from "../../utils";
import ParameterComponent from "./components/parameterComponent"; import ParameterComponent from "./components/parameterComponent";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
@ -14,144 +14,154 @@ import { PopUpContext } from "../../contexts/popUpContext";
import NodeModal from "../../modals/NodeModal"; import NodeModal from "../../modals/NodeModal";
import { useCallback } from "react"; import { useCallback } from "react";
import { TabsContext } from "../../contexts/tabsContext"; import { TabsContext } from "../../contexts/tabsContext";
import { debounce } from "../../utils";
export default function GenericNode({ export default function GenericNode({
data, data,
selected, selected,
}: { }: {
data: NodeDataType; data: NodeDataType;
selected: boolean; selected: boolean;
}) { }) {
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const showError = useRef(true); const showError = useRef(true);
const { types, deleteNode } = useContext(typesContext); const { types, deleteNode } = useContext(typesContext);
const { openPopUp } = useContext(PopUpContext); const { openPopUp } = useContext(PopUpContext);
const Icon = nodeIcons[types[data.type]]; const Icon = nodeIcons[types[data.type]];
const [validationStatus, setValidationStatus] = useState("idle"); const [validationStatus, setValidationStatus] = useState("idle");
// State for outline color // State for outline color
const [isValid, setIsValid] = useState(false); const [isValid, setIsValid] = useState(false);
const {save} = useContext(TabsContext) const { save } = useContext(TabsContext);
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
const [params, setParams] = useState([]); const [params, setParams] = useState([]);
console.log(); console.log();
useEffect(() => { useEffect(() => {
if (reactFlowInstance) { if (reactFlowInstance) {
setParams(Object.values(reactFlowInstance.toObject())); setParams(Object.values(reactFlowInstance.toObject()));
} }
}, [save]); }, [save]);
useEffect(() => { const validateNode = useCallback(
try { debounce(async () => {
fetch(`/validate/node/${data.id}`, { try {
method: "POST", const response = await fetch(`/validate/node/${data.id}`, {
headers: { method: "POST",
"Content-Type": "application/json", headers: {
}, "Content-Type": "application/json",
body: JSON.stringify(reactFlowInstance.toObject()), },
}).then((response) => { body: JSON.stringify(reactFlowInstance.toObject()),
console.log(response.status, response.body); });
if (response.status === 200) { console.log(response.status, response.body);
setValidationStatus("success");
} else if (response.status === 500) {
setValidationStatus("error");
}
});
} catch (error) {
console.error("Error validating node:", error);
setValidationStatus("error");
}
}, [params]);
useEffect(() => { if (response.status === 200) {
if (validationStatus === "success") { setValidationStatus("success");
setIsValid(true); } else if (response.status === 500) {
} else { setValidationStatus("error");
setIsValid(false); }
} } catch (error) {
}, [validationStatus]); 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) { useEffect(() => {
if (showError.current) { if (validationStatus === "success") {
setErrorData({ setIsValid(true);
title: data.type } else {
? `The ${data.type} node could not be rendered, please review your json file` setIsValid(false);
: "There was a node that can't be rendered, please review your json file", }
}); }, [validationStatus]);
showError.current = false;
}
deleteNode(data.id);
return;
}
return ( if (!Icon) {
<div if (showError.current) {
className={classNames( setErrorData({
isValid ? "animate-pulse-green" : "border-red-outline", title: data.type
selected ? "border border-blue-500" : "border dark:border-gray-700", ? `The ${data.type} node could not be rendered, please review your json file`
"prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center" : "There was a node that can't be rendered, please review your json file",
)} });
> showError.current = false;
<div className="w-full dark:text-white flex items-center justify-between p-4 gap-8 bg-gray-50 rounded-t-lg dark:bg-gray-800 border-b dark:border-b-gray-700 "> }
<div className="w-full flex items-center truncate gap-4 text-lg"> deleteNode(data.id);
<Icon return;
className="w-10 h-10 p-1 rounded" }
style={{
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
}}
/>
<div className="truncate">{data.type}</div>
</div>
<div className="flex gap-3">
<button
className="relative"
onClick={(event) => {
event.preventDefault();
openPopUp(<NodeModal data={data} />);
}}
>
<div className=" absolute text-red-600 -top-2 -right-1">
{Object.keys(data.node.template).some(
(t) =>
data.node.template[t].advanced &&
data.node.template[t].required
)
? " *"
: ""}
</div>
<Cog6ToothIcon
className={classNames(
Object.keys(data.node.template).some(
(t) => data.node.template[t].advanced && data.node.template[t].show
)
? ""
: "hidden",
"w-6 h-6 dark:text-gray-500 hover:animate-spin"
)}
></Cog6ToothIcon>
</button>
<button
onClick={() => {
deleteNode(data.id);
}}
>
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon>
</button>
</div>
</div>
<div className="w-full h-full py-5"> return (
<div className="w-full text-gray-500 px-5 pb-3 text-sm"> <div
{data.node.description} className={classNames(
</div> isValid ? "animate-pulse-green" : "border-red-outline",
selected ? "border border-blue-500" : "border dark:border-gray-700",
"prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center"
)}
>
<div className="w-full dark:text-white flex items-center justify-between p-4 gap-8 bg-gray-50 rounded-t-lg dark:bg-gray-800 border-b dark:border-b-gray-700 ">
<div className="w-full flex items-center truncate gap-4 text-lg">
<Icon
className="w-10 h-10 p-1 rounded"
style={{
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
}}
/>
<div className="truncate">{data.type}</div>
</div>
<div className="flex gap-3">
<button
className="relative"
onClick={(event) => {
event.preventDefault();
openPopUp(<NodeModal data={data} />);
}}
>
<div className=" absolute text-red-600 -top-2 -right-1">
{Object.keys(data.node.template).some(
(t) =>
data.node.template[t].advanced &&
data.node.template[t].required
)
? " *"
: ""}
</div>
<Cog6ToothIcon
className={classNames(
Object.keys(data.node.template).some(
(t) =>
data.node.template[t].advanced && data.node.template[t].show
)
? ""
: "hidden",
"w-6 h-6 dark:text-gray-500 hover:animate-spin"
)}
></Cog6ToothIcon>
</button>
<button
onClick={() => {
deleteNode(data.id);
}}
>
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon>
</button>
</div>
</div>
<> <div className="w-full h-full py-5">
{Object.keys(data.node.template) <div className="w-full text-gray-500 px-5 pb-3 text-sm">
.filter((t) => t.charAt(0) !== "_") {data.node.description}
.map((t: string, idx) => ( </div>
<div key={idx}>
{/* {idx === 0 ? ( <>
{Object.keys(data.node.template)
.filter((t) => t.charAt(0) !== "_")
.map((t: string, idx) => (
<div key={idx}>
{/* {idx === 0 ? (
<div <div
className={classNames( className={classNames(
"px-5 py-2 mt-2 dark:text-white text-center", "px-5 py-2 mt-2 dark:text-white text-center",
@ -170,58 +180,59 @@ export default function GenericNode({
) : ( ) : (
<></> <></>
)} */} )} */}
{data.node.template[t].show && !data.node.template[t].advanced ? ( {data.node.template[t].show &&
<ParameterComponent !data.node.template[t].advanced ? (
data={data} <ParameterComponent
color={ data={data}
nodeColors[types[data.node.template[t].type]] ?? color={
nodeColors.unknown nodeColors[types[data.node.template[t].type]] ??
} nodeColors.unknown
title={ }
data.node.template[t].display_name title={
? data.node.template[t].display_name data.node.template[t].display_name
: data.node.template[t].name ? data.node.template[t].display_name
? snakeToNormalCase(data.node.template[t].name) : data.node.template[t].name
: snakeToNormalCase(t) ? snakeToNormalCase(data.node.template[t].name)
} : snakeToNormalCase(t)
name={t} }
tooltipTitle={ name={t}
"Type: " + tooltipTitle={
data.node.template[t].type + "Type: " +
(data.node.template[t].list ? " list" : "") data.node.template[t].type +
} (data.node.template[t].list ? " list" : "")
required={data.node.template[t].required} }
id={data.node.template[t].type + "|" + t + "|" + data.id} required={data.node.template[t].required}
left={true} id={data.node.template[t].type + "|" + t + "|" + data.id}
type={data.node.template[t].type} left={true}
/> type={data.node.template[t].type}
) : ( />
<></> ) : (
)} <></>
</div> )}
))} </div>
<div ))}
className={classNames( <div
Object.keys(data.node.template).length < 1 ? "hidden" : "", className={classNames(
"w-full flex justify-center" Object.keys(data.node.template).length < 1 ? "hidden" : "",
)} "w-full flex justify-center"
> )}
{" "} >
</div> {" "}
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center"> </div>
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
Output Output
</div> */} </div> */}
<ParameterComponent <ParameterComponent
data={data} data={data}
color={nodeColors[types[data.type]] ?? nodeColors.unknown} color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={data.type} title={data.type}
tooltipTitle={`Type: ${data.node.base_classes.join(" | ")}`} tooltipTitle={`Type: ${data.node.base_classes.join(" | ")}`}
id={[data.type, data.id, ...data.node.base_classes].join("|")} id={[data.type, data.id, ...data.node.base_classes].join("|")}
type={data.node.base_classes.join("|")} type={data.node.base_classes.join("|")}
left={false} left={false}
/> />
</> </>
</div> </div>
</div> </div>
); );
} }

View file

@ -399,8 +399,11 @@ export function removeApiKeys(flow: FlowType): FlowType {
return cleanFLow; return cleanFLow;
} }
export function updateObject<T extends Record<string, any>>(reference: T, objectToUpdate: T): T { export function updateObject<T extends Record<string, any>>(
let clonedObject = _.cloneDeep(objectToUpdate) reference: T,
objectToUpdate: T
): T {
let clonedObject = _.cloneDeep(objectToUpdate);
// Loop through each key in the object to update // Loop through each key in the object to update
for (const key in clonedObject) { for (const key in clonedObject) {
// If the key is not in the reference object, delete it // If the key is not in the reference object, delete it
@ -416,4 +419,13 @@ export function updateObject<T extends Record<string, any>>(reference: T, object
} }
} }
return clonedObject; return clonedObject;
} }
export function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}