feat(validate): add debounced validation for nodes in GenericNode component

fix(validate): set validation status to "success" or "error" based on response status
feat(validate): add state for validation status and outline color in GenericNode component
refactor(validate): use useDebouncedCallback hook for debouncing validation function
refactor(validate): simplify useEffect dependencies in GenericNode component
This commit is contained in:
Gabriel Almeida 2023-04-25 14:31:57 -03:00
commit 7af5f68861
2 changed files with 180 additions and 163 deletions

View file

@ -46,7 +46,7 @@ def post_validate_node(node_id: str, data: dict):
node = graph.get_node(node_id) node = graph.get_node(node_id)
if node is not None: if node is not None:
_ = node.build() _ = node.build()
return node.params return str(node.params)
raise Exception(f"Node {node_id} not found") raise Exception(f"Node {node_id} not found")
except Exception as e: except Exception as e:
logger.exception(e) logger.exception(e)

View file

@ -1,186 +1,203 @@
import { TrashIcon } from "@heroicons/react/24/outline"; import { TrashIcon } from "@heroicons/react/24/outline";
import { useDebouncedCallback } from "use-debounce";
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";
import { useContext, useState, useEffect, useRef } from "react"; import { useContext, useState, useEffect, useRef } from "react";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { useCallback } from 'react'; import { useCallback } from "react";
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 Icon = nodeIcons[types[data.type]]; const Icon = nodeIcons[types[data.type]];
const [validationStatus, setValidationStatus] = useState("idle");
// State for outline color
const [isGreenOutline, setIsGreenOutline] = useState(false);
const [isRedOutline, setIsRedOutline] = useState(false);
const { reactFlowInstance } = useContext(typesContext);
// State for outline color const debouncedValidateNode = useDebouncedCallback(async () => {
const [isGreenOutline, setIsGreenOutline] = useState(false); // Check if the validationStatus is "success"
const [isRedOutline, setIsRedOutline] = useState(false); if (validationStatus === "success") return;
const { reactFlowInstance } = useContext(typesContext);
const validateNode = useCallback(async () => { try {
try { const response = await fetch(`/validate/node/${data.id}`, {
const response = await fetch(`/validate/node/${data.id}`, { method: "POST",
method: 'POST', headers: {
headers: { "Content-Type": "application/json",
'Content-Type': 'application/json', },
}, body: JSON.stringify(reactFlowInstance.toObject()),
body: JSON.stringify(reactFlowInstance.toObject()), });
});
if (response.status === 200) { if (response.status === 200) {
setIsGreenOutline(true); setValidationStatus("success");
setIsRedOutline(false); } else if (response.status === 500) {
} else if (response.status === 500) { setValidationStatus("error");
setIsRedOutline(true); }
setIsGreenOutline(false); } catch (error) {
} console.error("Error validating node:", error);
} catch (error) { setValidationStatus("error");
console.error('Error validating node:', error); }
setIsRedOutline(true); }, 1000);
}
}, [data.id, reactFlowInstance]);
useEffect(() => { const validateNode = useCallback(() => {
validateNode(); debouncedValidateNode();
}, [ }, [debouncedValidateNode]);
validateNode,
...Object.values(data.node.template).flatMap((t) => Object.values(t)),
]); useEffect(() => {
validateNode();
}, [
validateNode,
...Object.values(data.node.template).flatMap((t) => Object.values(t)),
]);
useEffect(() => { useEffect(() => {
if (isGreenOutline) { if (validationStatus === "success") {
setTimeout(() => { setIsGreenOutline(true);
setIsGreenOutline(false); setIsRedOutline(false);
}, 1000); setTimeout(() => {
} setIsGreenOutline(false);
}, [isGreenOutline]); }, 1000);
} else if (validationStatus === "error") {
setIsRedOutline(true);
setIsGreenOutline(false);
} else {
setIsGreenOutline(false);
setIsRedOutline(false);
}
}, [validationStatus]);
const outlineColor = isGreenOutline ? 'animate-pulse-green' : isRedOutline ? 'border-red-outline' : ''; const outlineColor = isGreenOutline
? "animate-pulse-green"
: isRedOutline
? "border-red-outline"
: "";
if (!Icon) { if (!Icon) {
if (showError.current) { if (showError.current) {
setErrorData({ setErrorData({
title: data.type title: data.type
? `The ${data.type} node could not be rendered, please review your json file` ? `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", : "There was a node that can't be rendered, please review your json file",
}); });
showError.current = false; showError.current = false;
} }
deleteNode(data.id); deleteNode(data.id);
return; return;
} }
return ( return (
<div <div
className={classNames( className={classNames(
outlineColor, outlineColor,
selected ? "border border-blue-500" : "border dark:border-gray-700", 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" "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 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"> <div className="w-full flex items-center truncate gap-4 text-lg">
<Icon <Icon
className="w-10 h-10 p-1 rounded" className="w-10 h-10 p-1 rounded"
style={{ style={{
color: nodeColors[types[data.type]] ?? nodeColors.unknown, color: nodeColors[types[data.type]] ?? nodeColors.unknown,
}} }}
/> />
<div className="truncate">{data.type}</div> <div className="truncate">{data.type}</div>
</div> </div>
<button <button
onClick={() => { onClick={() => {
deleteNode(data.id); deleteNode(data.id);
}} }}
> >
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon> <TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon>
</button> </button>
</div> </div>
<div className="w-full h-full py-5"> <div className="w-full h-full py-5">
<div className="w-full text-gray-500 px-5 text-sm"> <div className="w-full text-gray-500 px-5 text-sm">
{data.node.description} {data.node.description}
</div> </div>
<> <>
{Object.keys(data.node.template) {Object.keys(data.node.template)
.filter((t) => t.charAt(0) !== "_") .filter((t) => t.charAt(0) !== "_")
.map((t: string, idx) => ( .map((t: string, idx) => (
<div key={idx}> <div key={idx}>
{idx === 0 ? ( {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",
Object.keys(data.node.template).filter( Object.keys(data.node.template).filter(
(key) => (key) =>
!key.startsWith("_") && data.node.template[key].show !key.startsWith("_") && data.node.template[key].show
).length === 0 ).length === 0
? "hidden" ? "hidden"
: "" : ""
)} )}
> >
Inputs Inputs
</div> </div>
) : ( ) : (
<></> <></>
)} )}
{data.node.template[t].show ? ( {data.node.template[t].show ? (
<ParameterComponent <ParameterComponent
data={data} data={data}
color={ color={
nodeColors[types[data.node.template[t].type]] ?? nodeColors[types[data.node.template[t].type]] ??
nodeColors.unknown nodeColors.unknown
} }
title={ title={
data.node.template[t].display_name data.node.template[t].display_name
? data.node.template[t].display_name ? data.node.template[t].display_name
: data.node.template[t].name : data.node.template[t].name
? snakeToNormalCase(data.node.template[t].name) ? snakeToNormalCase(data.node.template[t].name)
: snakeToNormalCase(t) : snakeToNormalCase(t)
} }
name={t} name={t}
tooltipTitle={ tooltipTitle={
"Type: " + "Type: " +
data.node.template[t].type + data.node.template[t].type +
(data.node.template[t].list ? " list" : "") (data.node.template[t].list ? " list" : "")
} }
required={data.node.template[t].required} required={data.node.template[t].required}
id={data.node.template[t].type + "|" + t + "|" + data.id} id={data.node.template[t].type + "|" + t + "|" + data.id}
left={true} left={true}
type={data.node.template[t].type} type={data.node.template[t].type}
/> />
) : ( ) : (
<></> <></>
)} )}
</div> </div>
))} ))}
<div className="px-5 py-2 mt-2 dark:text-white text-center"> <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>
); );
} }