move folder
This commit is contained in:
parent
4c87f7662c
commit
472c8737dd
12 changed files with 1882 additions and 0 deletions
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
|
||||||
|
import { groupByFamily } from "../../../../utils/utils";
|
||||||
|
import TooltipRenderComponent from "../tooltipRenderComponent";
|
||||||
|
import { useTypesStore } from "../../../../stores/typesStore";
|
||||||
|
import { NodeType } from "../../../../types/flow";
|
||||||
|
import useFlowStore from "../../../../stores/flowStore";
|
||||||
|
|
||||||
|
export default function HandleTooltips({
|
||||||
|
left,
|
||||||
|
tooltipTitle,
|
||||||
|
}: {
|
||||||
|
left: boolean;
|
||||||
|
nodes: NodeType[];
|
||||||
|
tooltipTitle: string;
|
||||||
|
}) {
|
||||||
|
const myData = useTypesStore((state) => state.data);
|
||||||
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
|
|
||||||
|
let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, nodes!);
|
||||||
|
|
||||||
|
if (groupedObj && groupedObj.length > 0) {
|
||||||
|
//@ts-ignore
|
||||||
|
return groupedObj.map((item, index) => {
|
||||||
|
return <TooltipRenderComponent index={index} item={item} left={left} />;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
//@ts-ignore
|
||||||
|
return <span data-testid={`empty-tooltip-filter`}>{TOOLTIP_EMPTY}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { useUpdateNodeInternals } from "reactflow";
|
||||||
|
import ForwardedIconComponent from "../../../../components/genericIconComponent";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../../../../components/ui/dropdown-menu";
|
||||||
|
import useFlowStore from "../../../../stores/flowStore";
|
||||||
|
import { outputComponentType } from "../../../../types/components";
|
||||||
|
import { NodeDataType } from "../../../../types/flow";
|
||||||
|
import { cn } from "../../../../utils/utils";
|
||||||
|
import { Button } from "../../../../components/ui/button";
|
||||||
|
|
||||||
|
export default function OutputComponent({
|
||||||
|
selected,
|
||||||
|
types,
|
||||||
|
frozen = false,
|
||||||
|
nodeId,
|
||||||
|
idx,
|
||||||
|
name,
|
||||||
|
}: outputComponentType) {
|
||||||
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
|
||||||
|
if (types.length < 2) {
|
||||||
|
return <span className={cn(frozen ? " text-ice" : "")}>{selected}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="nocopy nopan nodelete nodrag noundo flex items-center gap-2 ">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
disabled={frozen}
|
||||||
|
variant="primary"
|
||||||
|
size="xs"
|
||||||
|
className={cn(
|
||||||
|
frozen ? "text-ice" : "",
|
||||||
|
"items-center gap-1 pl-2 pr-1.5 align-middle text-xs font-normal",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="pb-px">{selected}</span>
|
||||||
|
<ForwardedIconComponent name="ChevronDown" className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{types.map((type) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
// TODO: UDPDATE SET NODE TO NEW NODE FORM
|
||||||
|
setNode(nodeId, (node) => {
|
||||||
|
const newNode = cloneDeep(node);
|
||||||
|
(newNode.data as NodeDataType).node!.outputs![idx].selected =
|
||||||
|
type;
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
updateNodeInternals(nodeId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<span>{name}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export const TEXT_FIELD_TYPES: string[] = ["str", "SecretStr"];
|
||||||
|
|
@ -0,0 +1,579 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||||
|
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||||
|
import CodeAreaComponent from "../../../../components/codeAreaComponent";
|
||||||
|
import DictComponent from "../../../../components/dictComponent";
|
||||||
|
import Dropdown from "../../../../components/dropdownComponent";
|
||||||
|
import FloatComponent from "../../../../components/floatComponent";
|
||||||
|
import { default as IconComponent } from "../../../../components/genericIconComponent";
|
||||||
|
import InputFileComponent from "../../../../components/inputFileComponent";
|
||||||
|
import InputGlobalComponent from "../../../../components/inputGlobalComponent";
|
||||||
|
import InputListComponent from "../../../../components/inputListComponent";
|
||||||
|
import IntComponent from "../../../../components/intComponent";
|
||||||
|
import KeypairListComponent from "../../../../components/keypairListComponent";
|
||||||
|
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||||
|
import ShadTooltip from "../../../../components/shadTooltipComponent";
|
||||||
|
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||||
|
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||||
|
import { Button } from "../../../../components/ui/button";
|
||||||
|
import { RefreshButton } from "../../../../components/ui/refreshButton";
|
||||||
|
import { LANGFLOW_SUPPORTED_TYPES } from "../../../../constants/constants";
|
||||||
|
import { Case } from "../../../../shared/components/caseComponent";
|
||||||
|
import useFlowStore from "../../../../stores/flowStore";
|
||||||
|
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
|
||||||
|
import { useTypesStore } from "../../../../stores/typesStore";
|
||||||
|
import { APIClassType } from "../../../../types/api";
|
||||||
|
import { ParameterComponentType } from "../../../../types/components";
|
||||||
|
import {
|
||||||
|
debouncedHandleUpdateValues,
|
||||||
|
handleUpdateValues,
|
||||||
|
} from "../../../../utils/parameterUtils";
|
||||||
|
import {
|
||||||
|
convertObjToArray,
|
||||||
|
convertValuesToNumbers,
|
||||||
|
hasDuplicateKeys,
|
||||||
|
isValidConnection,
|
||||||
|
scapedJSONStringfy,
|
||||||
|
} from "../../../../utils/reactflowUtils";
|
||||||
|
import { nodeColors } from "../../../../utils/styleUtils";
|
||||||
|
import { classNames, groupByFamily } from "../../../../utils/utils";
|
||||||
|
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
|
||||||
|
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
|
||||||
|
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
|
||||||
|
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
|
||||||
|
import HandleTooltips from "../HandleTooltipComponent";
|
||||||
|
import OutputComponent from "../OutputComponent";
|
||||||
|
import { TEXT_FIELD_TYPES } from "./constants";
|
||||||
|
|
||||||
|
export default function ParameterComponent({
|
||||||
|
left,
|
||||||
|
id,
|
||||||
|
data,
|
||||||
|
tooltipTitle,
|
||||||
|
title,
|
||||||
|
color,
|
||||||
|
type,
|
||||||
|
name = "",
|
||||||
|
required = false,
|
||||||
|
optionalHandle = null,
|
||||||
|
info = "",
|
||||||
|
proxy,
|
||||||
|
showNode,
|
||||||
|
index,
|
||||||
|
outputName,
|
||||||
|
}: ParameterComponentType): JSX.Element {
|
||||||
|
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||||
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
|
const edges = useFlowStore((state) => state.edges);
|
||||||
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
const myData = useTypesStore((state) => state.data);
|
||||||
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||||
|
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
|
||||||
|
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
takeSnapshot,
|
||||||
|
handleUpdateValues,
|
||||||
|
debouncedHandleUpdateValues,
|
||||||
|
setNode,
|
||||||
|
isLoading,
|
||||||
|
setIsLoading
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
takeSnapshot,
|
||||||
|
setNode,
|
||||||
|
updateNodeInternals
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
|
||||||
|
useHandleRefreshButtonPress(setIsLoading, setNode);
|
||||||
|
|
||||||
|
let disabled =
|
||||||
|
edges.some(
|
||||||
|
(edge) =>
|
||||||
|
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id)
|
||||||
|
) ?? false;
|
||||||
|
|
||||||
|
const handleRefreshButtonPress = async (name, data) => {
|
||||||
|
handleRefreshButtonPressHook(name, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
useFetchDataOnMount(data, name, handleUpdateValues, setNode, setIsLoading);
|
||||||
|
|
||||||
|
const handleOnNewValue = async (
|
||||||
|
newValue: string | string[] | boolean | Object[],
|
||||||
|
skipSnapshot: boolean | undefined = false
|
||||||
|
): Promise<void> => {
|
||||||
|
handleOnNewValueHook(newValue, skipSnapshot);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
|
||||||
|
handleNodeClassHook(newNodeClass, code);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
infoHtml.current = (
|
||||||
|
<div className="h-full w-full break-words">
|
||||||
|
{info.split("\n").map((line, index) => (
|
||||||
|
<p key={index} className="block">
|
||||||
|
{line}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [info]);
|
||||||
|
|
||||||
|
function renderTitle() {
|
||||||
|
return !left ? (
|
||||||
|
<OutputComponent
|
||||||
|
idx={index}
|
||||||
|
types={type?.split("|") ?? []}
|
||||||
|
selected={
|
||||||
|
data.node?.outputs![index].selected ??
|
||||||
|
data.node?.outputs![index].types[0] ??
|
||||||
|
title
|
||||||
|
}
|
||||||
|
nodeId={data.id}
|
||||||
|
frozen={data.node?.frozen}
|
||||||
|
name={outputName ?? type ?? title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{title}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If optionalHandle is an empty list, then it is not an optional handle
|
||||||
|
if (optionalHandle && optionalHandle.length === 0) {
|
||||||
|
optionalHandle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !showNode ? (
|
||||||
|
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
|
||||||
|
<div className="flex">
|
||||||
|
<ShadTooltip
|
||||||
|
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||||
|
delayDuration={1000}
|
||||||
|
content={
|
||||||
|
<HandleTooltips
|
||||||
|
left={left}
|
||||||
|
nodes={nodes}
|
||||||
|
tooltipTitle={tooltipTitle!}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
side={left ? "left" : "right"}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
data-test-id={`handle-${title.toLowerCase()}-${
|
||||||
|
left ? "target" : "source"
|
||||||
|
}`}
|
||||||
|
type={left ? "target" : "source"}
|
||||||
|
position={left ? Position.Left : Position.Right}
|
||||||
|
key={
|
||||||
|
proxy
|
||||||
|
? scapedJSONStringfy({ ...id, proxy })
|
||||||
|
: scapedJSONStringfy(id)
|
||||||
|
}
|
||||||
|
id={
|
||||||
|
proxy
|
||||||
|
? scapedJSONStringfy({ ...id, proxy })
|
||||||
|
: scapedJSONStringfy(id)
|
||||||
|
}
|
||||||
|
isValidConnection={(connection) =>
|
||||||
|
isValidConnection(connection, nodes, edges)
|
||||||
|
}
|
||||||
|
className={classNames(
|
||||||
|
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||||
|
"h-3 w-3 rounded-full border-2 bg-background",
|
||||||
|
!showNode ? "mt-0" : ""
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
borderColor: color ?? nodeColors.unknown,
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setFilterEdge(
|
||||||
|
groupByFamily(myData, tooltipTitle!, left, nodes!)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></Handle>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
|
||||||
|
((name === "code" && type === "code") ||
|
||||||
|
(name.includes("code") && proxy)
|
||||||
|
? " hidden "
|
||||||
|
: "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"flex w-full items-center truncate text-sm" +
|
||||||
|
(left ? "" : " justify-end")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Case condition={left && data.node?.frozen}>
|
||||||
|
<div className="pr-1">
|
||||||
|
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
{proxy ? (
|
||||||
|
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||||
|
{renderTitle()}
|
||||||
|
</ShadTooltip>
|
||||||
|
) : (
|
||||||
|
renderTitle()
|
||||||
|
)}
|
||||||
|
<span className={(required ? "ml-2 " : "") + "text-status-red"}>
|
||||||
|
{required ? "*" : ""}
|
||||||
|
</span>
|
||||||
|
<div className="">
|
||||||
|
{info !== "" && (
|
||||||
|
<ShadTooltip content={infoHtml.current}>
|
||||||
|
{/* put div to avoid bug that does not display tooltip */}
|
||||||
|
<div>
|
||||||
|
<IconComponent
|
||||||
|
name="Info"
|
||||||
|
className="relative bottom-px ml-1.5 h-3 w-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
|
||||||
|
<div className="flex">
|
||||||
|
<ShadTooltip
|
||||||
|
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||||
|
delayDuration={1000}
|
||||||
|
content={
|
||||||
|
<HandleTooltips
|
||||||
|
left={left}
|
||||||
|
nodes={nodes}
|
||||||
|
tooltipTitle={tooltipTitle!}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
side={left ? "left" : "right"}
|
||||||
|
>
|
||||||
|
<Handle
|
||||||
|
data-test-id={`handle-${title.toLowerCase()}-${
|
||||||
|
left ? "left" : "right"
|
||||||
|
}`}
|
||||||
|
type={left ? "target" : "source"}
|
||||||
|
position={left ? Position.Left : Position.Right}
|
||||||
|
key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
|
||||||
|
id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
|
||||||
|
isValidConnection={(connection) =>
|
||||||
|
isValidConnection(connection, nodes, edges)
|
||||||
|
}
|
||||||
|
className={classNames(
|
||||||
|
left ? "-ml-0.5" : "-mr-0.5",
|
||||||
|
"h-3 w-3 rounded-full border-2 bg-background"
|
||||||
|
)}
|
||||||
|
style={{ borderColor: color ?? nodeColors.unknown }}
|
||||||
|
onClick={() => {
|
||||||
|
setFilterEdge(
|
||||||
|
groupByFamily(myData, tooltipTitle!, left, nodes!)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Case
|
||||||
|
condition={
|
||||||
|
left === true &&
|
||||||
|
TEXT_FIELD_TYPES.includes(type ?? "") &&
|
||||||
|
!data.node?.template[name]?.options
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<Case condition={data.node?.template[name]?.list}>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
// Commenting this out until we have a better
|
||||||
|
// way to display
|
||||||
|
// (data.node?.template[name]?.refresh ? "w-5/6 " : "") +
|
||||||
|
"flex-grow"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InputListComponent
|
||||||
|
componentName={name}
|
||||||
|
disabled={disabled}
|
||||||
|
value={
|
||||||
|
!data.node!.template[name]?.value ||
|
||||||
|
data.node!.template[name]?.value === ""
|
||||||
|
? [""]
|
||||||
|
: data.node!.template[name]?.value
|
||||||
|
}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
<Case condition={data.node?.template[name]?.multiline}>
|
||||||
|
<div className="mt-2 flex w-full flex-col ">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<TextAreaComponent
|
||||||
|
disabled={disabled}
|
||||||
|
value={data.node!.template[name]?.value ?? ""}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
id={"textarea-" + data.node!.template[name]?.name}
|
||||||
|
data-testid={"textarea-" + data.node!.template[name]?.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.node?.template[name]?.refresh_button && (
|
||||||
|
<div className="flex-grow">
|
||||||
|
<RefreshButton
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
data={data}
|
||||||
|
button_text={
|
||||||
|
data.node?.template[name].refresh_button_text
|
||||||
|
}
|
||||||
|
className="extra-side-bar-buttons mt-1"
|
||||||
|
handleUpdateValues={handleRefreshButtonPress}
|
||||||
|
id={"refresh-button-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
<Case
|
||||||
|
condition={
|
||||||
|
!data.node?.template[name]?.multiline &&
|
||||||
|
!data.node?.template[name]?.list
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mt-2 flex w-full items-center">
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"flex-grow " +
|
||||||
|
(data.node?.template[name]?.refresh_button ? "w-5/6" : "")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InputGlobalComponent
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
setDb={(value) => {
|
||||||
|
setNode(data.id, (oldNode) => {
|
||||||
|
let newNode = cloneDeep(oldNode);
|
||||||
|
newNode.data = {
|
||||||
|
...newNode.data,
|
||||||
|
};
|
||||||
|
newNode.data.node.template[name].load_from_db = value;
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
name={name}
|
||||||
|
data={data.node?.template[name]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.node?.template[name]?.refresh_button && (
|
||||||
|
<div className="w-1/6">
|
||||||
|
<RefreshButton
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
data={data}
|
||||||
|
button_text={
|
||||||
|
data.node?.template[name].refresh_button_text
|
||||||
|
}
|
||||||
|
className="extra-side-bar-buttons ml-2 mt-1"
|
||||||
|
handleUpdateValues={handleRefreshButtonPress}
|
||||||
|
id={"refresh-button-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "bool"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<ToggleShadComponent
|
||||||
|
id={"toggle-" + name}
|
||||||
|
disabled={disabled}
|
||||||
|
enabled={data.node?.template[name]?.value ?? false}
|
||||||
|
setEnabled={handleOnNewValue}
|
||||||
|
size="large"
|
||||||
|
editNode={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "float"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<FloatComponent
|
||||||
|
disabled={disabled}
|
||||||
|
value={data.node?.template[name]?.value ?? ""}
|
||||||
|
rangeSpec={data.node?.template[name]?.rangeSpec}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case
|
||||||
|
condition={
|
||||||
|
left === true &&
|
||||||
|
type === "str" &&
|
||||||
|
(data.node?.template[name]?.options ||
|
||||||
|
data.node?.template[name]?.real_time_refresh)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="mt-2 flex w-full items-center gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Dropdown
|
||||||
|
disabled={disabled}
|
||||||
|
isLoading={isLoading}
|
||||||
|
options={data.node!.template[name]?.options}
|
||||||
|
onSelect={handleOnNewValue}
|
||||||
|
value={data.node!.template[name]?.value}
|
||||||
|
id={"dropdown-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{data.node?.template[name]?.refresh_button && (
|
||||||
|
<div className="w-1/6">
|
||||||
|
<RefreshButton
|
||||||
|
isLoading={isLoading}
|
||||||
|
disabled={disabled}
|
||||||
|
name={name}
|
||||||
|
data={data}
|
||||||
|
button_text={data.node?.template[name]?.refresh_button_text}
|
||||||
|
handleUpdateValues={handleRefreshButtonPress}
|
||||||
|
id={"refresh-button-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "code"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<CodeAreaComponent
|
||||||
|
readonly={
|
||||||
|
data.node?.flow && data.node.template[name]?.dynamic
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
dynamic={data.node?.template[name]?.dynamic ?? false}
|
||||||
|
setNodeClass={handleNodeClass}
|
||||||
|
nodeClass={data.node}
|
||||||
|
disabled={disabled}
|
||||||
|
value={data.node?.template[name]?.value ?? ""}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
id={"code-input-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "file"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<InputFileComponent
|
||||||
|
disabled={disabled}
|
||||||
|
value={data.node?.template[name]?.value ?? ""}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
fileTypes={data.node?.template[name]?.fileTypes}
|
||||||
|
onFileChange={(filePath: string) => {
|
||||||
|
data.node!.template[name].file_path = filePath;
|
||||||
|
}}
|
||||||
|
></InputFileComponent>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "int"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<IntComponent
|
||||||
|
rangeSpec={data.node?.template[name]?.rangeSpec}
|
||||||
|
disabled={disabled}
|
||||||
|
value={data.node?.template[name]?.value ?? ""}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
id={"int-input-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "prompt"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<PromptAreaComponent
|
||||||
|
readonly={data.node?.flow ? true : false}
|
||||||
|
field_name={name}
|
||||||
|
setNodeClass={handleNodeClass}
|
||||||
|
nodeClass={data.node}
|
||||||
|
disabled={disabled}
|
||||||
|
value={data.node?.template[name]?.value ?? ""}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
id={"prompt-input-" + name}
|
||||||
|
data-testid={"prompt-input-" + name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "NestedDict"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<DictComponent
|
||||||
|
disabled={disabled}
|
||||||
|
editNode={false}
|
||||||
|
value={
|
||||||
|
!data.node!.template[name]?.value ||
|
||||||
|
data.node!.template[name]?.value?.toString() === "{}"
|
||||||
|
? {}
|
||||||
|
: data.node!.template[name]?.value
|
||||||
|
}
|
||||||
|
onChange={handleOnNewValue}
|
||||||
|
id="div-dict-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
|
||||||
|
<Case condition={left === true && type === "dict"}>
|
||||||
|
<div className="mt-2 w-full">
|
||||||
|
<KeypairListComponent
|
||||||
|
disabled={disabled}
|
||||||
|
editNode={false}
|
||||||
|
value={
|
||||||
|
data.node!.template[name]?.value?.length === 0 ||
|
||||||
|
!data.node!.template[name]?.value
|
||||||
|
? [{ "": "" }]
|
||||||
|
: convertObjToArray(data.node!.template[name]?.value, type!)
|
||||||
|
}
|
||||||
|
duplicateKey={errorDuplicateKey}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
const valueToNumbers = convertValuesToNumbers(newValue);
|
||||||
|
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
|
||||||
|
// if data.node?.template[name]?.list is true, then the value is an array of objects
|
||||||
|
// else we need to get the first object of the array
|
||||||
|
|
||||||
|
if (data.node?.template[name]?.list) {
|
||||||
|
handleOnNewValue(valueToNumbers);
|
||||||
|
} else handleOnNewValue(valueToNumbers[0]);
|
||||||
|
}}
|
||||||
|
isList={data.node?.template[name]?.list ?? false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Case>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
INPUT_HANDLER_HOVER,
|
||||||
|
OUTPUT_HANDLER_HOVER,
|
||||||
|
} from "../../../../constants/constants";
|
||||||
|
import {
|
||||||
|
nodeColors,
|
||||||
|
nodeIconsLucide,
|
||||||
|
nodeNames,
|
||||||
|
} from "../../../../utils/styleUtils";
|
||||||
|
import { classNames } from "../../../../utils/utils";
|
||||||
|
|
||||||
|
const TooltipRenderComponent = ({ item, index, left }) => {
|
||||||
|
const Icon = nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
data-testid={`available-${left ? "input" : "output"}-${item.family}`}
|
||||||
|
>
|
||||||
|
{index === 0 && (
|
||||||
|
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className={classNames(
|
||||||
|
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-5 w-5"
|
||||||
|
style={{
|
||||||
|
color: nodeColors[item.family],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className="h-5 w-5"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
style={{
|
||||||
|
color: nodeColors[item.family] ?? nodeColors.unknown,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="ps-2 text-xs text-foreground"
|
||||||
|
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
|
||||||
|
>
|
||||||
|
{nodeNames[item.family] ?? "Other"}{" "}
|
||||||
|
{item?.display_name && item?.display_name?.length > 0 ? (
|
||||||
|
<span
|
||||||
|
className="text-xs"
|
||||||
|
data-testid={`tooltip-${item?.display_name}`}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
{item.display_name === "" ? "" : " - "}
|
||||||
|
{item.display_name.split(", ").length > 2
|
||||||
|
? item.display_name.split(", ").map((el, index) => (
|
||||||
|
<React.Fragment key={el + name}>
|
||||||
|
<span>
|
||||||
|
{index === item.display_name.split(", ").length - 1
|
||||||
|
? el
|
||||||
|
: (el += `, `)}
|
||||||
|
</span>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
: item.display_name}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs" data-testid={`tooltip-${item?.type}`}>
|
||||||
|
{" "}
|
||||||
|
{item.type === "" ? "" : " - "}
|
||||||
|
{item.type.split(", ").length > 2
|
||||||
|
? item.type.split(", ").map((el, index) => (
|
||||||
|
<React.Fragment key={el + name}>
|
||||||
|
<span>
|
||||||
|
{index === item.type.split(", ").length - 1
|
||||||
|
? el
|
||||||
|
: (el += `, `)}
|
||||||
|
</span>
|
||||||
|
</React.Fragment>
|
||||||
|
))
|
||||||
|
: item.type}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TooltipRenderComponent;
|
||||||
860
src/frontend/src/CustomNodes/genericNode/index.tsx
Normal file
860
src/frontend/src/CustomNodes/genericNode/index.tsx
Normal file
|
|
@ -0,0 +1,860 @@
|
||||||
|
import emojiRegex from "emoji-regex";
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||||
|
import IconComponent from "../../components/genericIconComponent";
|
||||||
|
import InputComponent from "../../components/inputComponent";
|
||||||
|
import ShadTooltip from "../../components/shadTooltipComponent";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import Checkmark from "../../components/ui/checkmark";
|
||||||
|
import Loading from "../../components/ui/loading";
|
||||||
|
import { Textarea } from "../../components/ui/textarea";
|
||||||
|
import Xmark from "../../components/ui/xmark";
|
||||||
|
import {
|
||||||
|
RUN_TIMESTAMP_PREFIX,
|
||||||
|
STATUS_BUILD,
|
||||||
|
STATUS_BUILDING,
|
||||||
|
} from "../../constants/constants";
|
||||||
|
import { BuildStatus } from "../../constants/enums";
|
||||||
|
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||||
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
import { useDarkStore } from "../../stores/darkStore";
|
||||||
|
import useFlowStore from "../../stores/flowStore";
|
||||||
|
import useFlowsManagerStore from "../../stores/flowsManagerStore";
|
||||||
|
import { useTypesStore } from "../../stores/typesStore";
|
||||||
|
import { APIClassType } from "../../types/api";
|
||||||
|
import { validationStatusType } from "../../types/components";
|
||||||
|
import { NodeDataType } from "../../types/flow";
|
||||||
|
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||||
|
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||||
|
import { classNames, cn } from "../../utils/utils";
|
||||||
|
import getFieldTitle from "../utils/get-field-title";
|
||||||
|
import sortFields from "../utils/sort-fields";
|
||||||
|
import ParameterComponent from "./components/parameterComponent";
|
||||||
|
|
||||||
|
export default function GenericNode({
|
||||||
|
data,
|
||||||
|
xPos,
|
||||||
|
yPos,
|
||||||
|
selected,
|
||||||
|
}: {
|
||||||
|
data: NodeDataType;
|
||||||
|
selected: boolean;
|
||||||
|
xPos: number;
|
||||||
|
yPos: number;
|
||||||
|
}): JSX.Element {
|
||||||
|
const types = useTypesStore((state) => state.types);
|
||||||
|
const templates = useTypesStore((state) => state.templates);
|
||||||
|
const deleteNode = useFlowStore((state) => state.deleteNode);
|
||||||
|
const flowPool = useFlowStore((state) => state.flowPool);
|
||||||
|
const buildFlow = useFlowStore((state) => state.buildFlow);
|
||||||
|
const setNode = useFlowStore((state) => state.setNode);
|
||||||
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
|
||||||
|
const [inputName, setInputName] = useState(false);
|
||||||
|
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||||
|
const [inputDescription, setInputDescription] = useState(false);
|
||||||
|
const [nodeDescription, setNodeDescription] = useState(
|
||||||
|
data.node?.description!,
|
||||||
|
);
|
||||||
|
const [isOutdated, setIsOutdated] = useState(false);
|
||||||
|
const buildStatus = useFlowStore(
|
||||||
|
(state) => state.flowBuildStatus[data.id]?.status,
|
||||||
|
);
|
||||||
|
const lastRunTime = useFlowStore(
|
||||||
|
(state) => state.flowBuildStatus[data.id]?.timestamp,
|
||||||
|
);
|
||||||
|
const [validationStatus, setValidationStatus] =
|
||||||
|
useState<validationStatusType | null>(null);
|
||||||
|
const [handles, setHandles] = useState<number>(0);
|
||||||
|
|
||||||
|
const [validationString, setValidationString] = useState<string>("");
|
||||||
|
|
||||||
|
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This one should run only once
|
||||||
|
// first check if data.type in NATIVE_CATEGORIES
|
||||||
|
// if not return
|
||||||
|
if (!data.node?.template?.code?.value) return;
|
||||||
|
const thisNodeTemplate = templates[data.type]?.template;
|
||||||
|
// if the template does not have a code key
|
||||||
|
// return
|
||||||
|
if (!thisNodeTemplate?.code) return;
|
||||||
|
const currentCode = thisNodeTemplate.code?.value;
|
||||||
|
const thisNodesCode = data.node!.template?.code?.value;
|
||||||
|
const componentsToIgnore = ["Custom Component"];
|
||||||
|
if (
|
||||||
|
currentCode !== thisNodesCode &&
|
||||||
|
!componentsToIgnore.includes(data.node!.display_name)
|
||||||
|
) {
|
||||||
|
setIsOutdated(true);
|
||||||
|
} else {
|
||||||
|
setIsOutdated(false);
|
||||||
|
}
|
||||||
|
// template.code can be undefined
|
||||||
|
}, [data.node?.template?.code?.value]);
|
||||||
|
|
||||||
|
const updateNodeCode = useCallback(
|
||||||
|
(newNodeClass: APIClassType, code: string, name: string) => {
|
||||||
|
setNode(data.id, (oldNode) => {
|
||||||
|
let newNode = cloneDeep(oldNode);
|
||||||
|
|
||||||
|
newNode.data = {
|
||||||
|
...newNode.data,
|
||||||
|
node: newNodeClass,
|
||||||
|
description: newNodeClass.description ?? data.node!.description,
|
||||||
|
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
newNode.data.node.template[name].value = code;
|
||||||
|
setIsOutdated(false);
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateNodeInternals(data.id);
|
||||||
|
},
|
||||||
|
[data.id, data.node, setNode, setIsOutdated],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data.node!.template) {
|
||||||
|
setErrorData({
|
||||||
|
title: `Error in component ${data.node!.display_name}`,
|
||||||
|
list: [
|
||||||
|
`The component ${data.node!.display_name} has no template.`,
|
||||||
|
`Please contact the developer of the component to fix this issue.`,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
takeSnapshot();
|
||||||
|
deleteNode(data.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function countHandles(): void {
|
||||||
|
let count = Object.keys(data.node!.template)
|
||||||
|
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||||
|
.map((templateCamp) => {
|
||||||
|
const { template } = data.node!;
|
||||||
|
if (template[templateCamp].input_types) return true;
|
||||||
|
if (!template[templateCamp].show) return false;
|
||||||
|
switch (template[templateCamp].type) {
|
||||||
|
case "str":
|
||||||
|
case "bool":
|
||||||
|
case "float":
|
||||||
|
case "code":
|
||||||
|
case "prompt":
|
||||||
|
case "file":
|
||||||
|
case "int":
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((total, value) => total + (value ? 1 : 0), 0);
|
||||||
|
|
||||||
|
setHandles(count);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
countHandles();
|
||||||
|
}, [data, data.node]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selected) {
|
||||||
|
setInputName(false);
|
||||||
|
setInputDescription(false);
|
||||||
|
}
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
|
// State for outline color
|
||||||
|
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||||
|
|
||||||
|
// should be empty string if no duration
|
||||||
|
// else should be `Duration: ${duration}`
|
||||||
|
const getDurationString = (duration: number | undefined): string => {
|
||||||
|
if (duration === undefined) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return `${duration}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const durationString = getDurationString(validationStatus?.data.duration);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNodeDescription(data.node!.description);
|
||||||
|
}, [data.node!.description]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNodeName(data.node!.display_name);
|
||||||
|
}, [data.node!.display_name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const relevantData =
|
||||||
|
flowPool[data.id] && flowPool[data.id]?.length > 0
|
||||||
|
? flowPool[data.id][flowPool[data.id].length - 1]
|
||||||
|
: null;
|
||||||
|
if (relevantData) {
|
||||||
|
// Extract validation information from relevantData and update the validationStatus state
|
||||||
|
setValidationStatus(relevantData);
|
||||||
|
} else {
|
||||||
|
setValidationStatus(null);
|
||||||
|
}
|
||||||
|
}, [flowPool[data.id], data.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (validationStatus?.params) {
|
||||||
|
// if it is not a string turn it into a string
|
||||||
|
let newValidationString = validationStatus.params;
|
||||||
|
if (typeof newValidationString !== "string") {
|
||||||
|
newValidationString = JSON.stringify(validationStatus.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
setValidationString(newValidationString);
|
||||||
|
}
|
||||||
|
}, [validationStatus, validationStatus?.params]);
|
||||||
|
|
||||||
|
const [showNode, setShowNode] = useState(data.showNode ?? true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowNode(data.showNode ?? true);
|
||||||
|
}, [data.showNode]);
|
||||||
|
|
||||||
|
const nameEditable = true;
|
||||||
|
|
||||||
|
const isEmoji = emojiRegex().test(data?.node?.icon!);
|
||||||
|
|
||||||
|
const iconNodeRender = useCallback(() => {
|
||||||
|
const iconElement = data?.node?.icon;
|
||||||
|
const iconColor = nodeColors[types[data.type]];
|
||||||
|
const iconName =
|
||||||
|
iconElement || (data.node?.flow ? "group_components" : name);
|
||||||
|
const iconClassName = `generic-node-icon ${
|
||||||
|
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
|
||||||
|
}`;
|
||||||
|
if (iconElement && isEmoji) {
|
||||||
|
return nodeIconFragment(iconElement);
|
||||||
|
} else {
|
||||||
|
return checkNodeIconFragment(iconColor, iconName, iconClassName);
|
||||||
|
}
|
||||||
|
}, [data, isEmoji, name, showNode]);
|
||||||
|
|
||||||
|
const nodeIconFragment = (icon) => {
|
||||||
|
return <span className="text-lg">{icon}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkNodeIconFragment = (iconColor, iconName, iconClassName) => {
|
||||||
|
return (
|
||||||
|
<IconComponent
|
||||||
|
name={iconName}
|
||||||
|
className={iconClassName}
|
||||||
|
iconColor={iconColor}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDark = useDarkStore((state) => state.dark);
|
||||||
|
const renderIconStatus = (
|
||||||
|
buildStatus: BuildStatus | undefined,
|
||||||
|
validationStatus: validationStatusType | null,
|
||||||
|
) => {
|
||||||
|
const conditionSuccess = validationStatus && validationStatus.valid;
|
||||||
|
const conditionInactive =
|
||||||
|
validationStatus &&
|
||||||
|
!validationStatus.valid &&
|
||||||
|
buildStatus === BuildStatus.INACTIVE;
|
||||||
|
const conditionError =
|
||||||
|
buildStatus === BuildStatus.ERROR ||
|
||||||
|
(validationStatus && !validationStatus.valid);
|
||||||
|
|
||||||
|
if (buildStatus === BuildStatus.BUILDING) {
|
||||||
|
return <Loading className="text-medium-indigo" />;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<IconComponent
|
||||||
|
name="Play"
|
||||||
|
className={cn(
|
||||||
|
!conditionSuccess && !conditionInactive && !conditionError
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
"absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground transition-all group-hover:text-medium-indigo group-hover/node:opacity-100",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{conditionSuccess ? (
|
||||||
|
<Checkmark
|
||||||
|
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover/node:opacity-0"
|
||||||
|
isVisible={true}
|
||||||
|
/>
|
||||||
|
) : conditionInactive ? (
|
||||||
|
<IconComponent
|
||||||
|
name="Play"
|
||||||
|
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-gray opacity-30 transition-all group-hover/node:opacity-0"
|
||||||
|
/>
|
||||||
|
) : conditionError ? (
|
||||||
|
<Xmark
|
||||||
|
isVisible={true}
|
||||||
|
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover/node:opacity-0"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getSpecificClassFromBuildStatus = (
|
||||||
|
buildStatus: BuildStatus | undefined,
|
||||||
|
validationStatus: validationStatusType | null,
|
||||||
|
) => {
|
||||||
|
let isInvalid = validationStatus && !validationStatus.valid;
|
||||||
|
|
||||||
|
if (buildStatus === BuildStatus.INACTIVE) {
|
||||||
|
// INACTIVE should have its own class
|
||||||
|
return "inactive-status";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(buildStatus === BuildStatus.BUILT && isInvalid) ||
|
||||||
|
buildStatus === BuildStatus.ERROR
|
||||||
|
) {
|
||||||
|
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
|
||||||
|
} else if (buildStatus === BuildStatus.BUILDING) {
|
||||||
|
return "building-status";
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeBorderClassName = (
|
||||||
|
selected: boolean,
|
||||||
|
showNode: boolean,
|
||||||
|
buildStatus: BuildStatus | undefined,
|
||||||
|
validationStatus: validationStatusType | null,
|
||||||
|
) => {
|
||||||
|
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
|
||||||
|
buildStatus,
|
||||||
|
validationStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseBorderClass = getBaseBorderClass(selected);
|
||||||
|
const nodeSizeClass = getNodeSizeClass(showNode);
|
||||||
|
const names = classNames(
|
||||||
|
baseBorderClass,
|
||||||
|
nodeSizeClass,
|
||||||
|
"generic-node-div group/node",
|
||||||
|
specificClassFromBuildStatus,
|
||||||
|
);
|
||||||
|
return names;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBaseBorderClass = (selected) => {
|
||||||
|
let className = selected ? "border border-ring" : "border";
|
||||||
|
let frozenClass = selected ? "border-ring-frozen" : "border-frozen";
|
||||||
|
return data.node?.frozen ? frozenClass : className;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNodeSizeClass = (showNode) =>
|
||||||
|
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
|
||||||
|
|
||||||
|
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<NodeToolbar>
|
||||||
|
<NodeToolbarComponent
|
||||||
|
data={data}
|
||||||
|
deleteNode={(id) => {
|
||||||
|
takeSnapshot();
|
||||||
|
deleteNode(id);
|
||||||
|
}}
|
||||||
|
setShowNode={(show) => {
|
||||||
|
setNode(data.id, (old) => ({
|
||||||
|
...old,
|
||||||
|
data: { ...old.data, showNode: show },
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
setShowState={setShowNode}
|
||||||
|
numberOfHandles={handles}
|
||||||
|
showNode={showNode}
|
||||||
|
openAdvancedModal={false}
|
||||||
|
onCloseAdvancedModal={() => {}}
|
||||||
|
updateNodeCode={updateNodeCode}
|
||||||
|
isOutdated={isOutdated}
|
||||||
|
selected={selected}
|
||||||
|
/>
|
||||||
|
</NodeToolbar>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
data,
|
||||||
|
deleteNode,
|
||||||
|
takeSnapshot,
|
||||||
|
setNode,
|
||||||
|
setShowNode,
|
||||||
|
handles,
|
||||||
|
showNode,
|
||||||
|
updateNodeCode,
|
||||||
|
isOutdated,
|
||||||
|
selected,
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{memoizedNodeToolbarComponent}
|
||||||
|
<div
|
||||||
|
className={getNodeBorderClassName(
|
||||||
|
selected,
|
||||||
|
showNode,
|
||||||
|
buildStatus,
|
||||||
|
validationStatus,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{data.node?.beta && showNode && (
|
||||||
|
<div className="beta-badge-wrapper">
|
||||||
|
<div className="beta-badge-content">BETA</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
data-testid={"div-generic-node"}
|
||||||
|
className={
|
||||||
|
"generic-node-div-title " +
|
||||||
|
(!showNode
|
||||||
|
? " relative h-24 w-24 rounded-full "
|
||||||
|
: " justify-between rounded-t-lg ")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"generic-node-title-arrangement rounded-full" +
|
||||||
|
(!showNode && " justify-center ")
|
||||||
|
}
|
||||||
|
data-testid="generic-node-title-arrangement"
|
||||||
|
>
|
||||||
|
{iconNodeRender()}
|
||||||
|
{showNode && (
|
||||||
|
<div className="generic-node-tooltip-div">
|
||||||
|
{nameEditable && inputName ? (
|
||||||
|
<div>
|
||||||
|
<InputComponent
|
||||||
|
onBlur={() => {
|
||||||
|
setInputName(false);
|
||||||
|
if (nodeName.trim() !== "") {
|
||||||
|
setNodeName(nodeName);
|
||||||
|
setNode(data.id, (old) => ({
|
||||||
|
...old,
|
||||||
|
data: {
|
||||||
|
...old.data,
|
||||||
|
node: {
|
||||||
|
...old.data.node,
|
||||||
|
display_name: nodeName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setNodeName(data.node!.display_name);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
value={nodeName}
|
||||||
|
onChange={setNodeName}
|
||||||
|
password={false}
|
||||||
|
blurOnEnter={true}
|
||||||
|
id={`input-title-${data.node?.display_name}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="group flex items-start gap-1.5">
|
||||||
|
<ShadTooltip content={data.node?.display_name}>
|
||||||
|
<div
|
||||||
|
onClick={(event) => {
|
||||||
|
if (nameEditable) {
|
||||||
|
setInputName(true);
|
||||||
|
}
|
||||||
|
takeSnapshot();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
data-testid={"title-" + data.node?.display_name}
|
||||||
|
className="generic-node-tooltip-div cursor-text text-primary"
|
||||||
|
>
|
||||||
|
{data.node?.display_name}
|
||||||
|
</div>
|
||||||
|
</ShadTooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{!showNode && (
|
||||||
|
<>
|
||||||
|
{Object.keys(data.node!.template)
|
||||||
|
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||||
|
.map(
|
||||||
|
(templateField: string, idx) =>
|
||||||
|
data.node!.template[templateField].show &&
|
||||||
|
!data.node!.template[templateField].advanced && (
|
||||||
|
<ParameterComponent
|
||||||
|
index={idx}
|
||||||
|
key={scapedJSONStringfy({
|
||||||
|
inputTypes:
|
||||||
|
data.node!.template[templateField].input_types,
|
||||||
|
type: data.node!.template[templateField].type,
|
||||||
|
id: data.id,
|
||||||
|
fieldName: templateField,
|
||||||
|
proxy: data.node!.template[templateField].proxy,
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
color={
|
||||||
|
data.node?.template[templateField].input_types &&
|
||||||
|
data.node?.template[templateField].input_types!
|
||||||
|
.length > 0
|
||||||
|
? nodeColors[
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types![
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
|
] ??
|
||||||
|
nodeColors[
|
||||||
|
types[
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types![
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
: nodeColors[
|
||||||
|
data.node?.template[templateField].type!
|
||||||
|
] ??
|
||||||
|
nodeColors[
|
||||||
|
types[
|
||||||
|
data.node?.template[templateField].type!
|
||||||
|
]
|
||||||
|
] ??
|
||||||
|
nodeColors.unknown
|
||||||
|
}
|
||||||
|
title={getFieldTitle(
|
||||||
|
data.node?.template!,
|
||||||
|
templateField,
|
||||||
|
)}
|
||||||
|
info={data.node?.template[templateField].info}
|
||||||
|
name={templateField}
|
||||||
|
tooltipTitle={
|
||||||
|
data.node?.template[
|
||||||
|
templateField
|
||||||
|
].input_types?.join("\n") ??
|
||||||
|
data.node?.template[templateField].type
|
||||||
|
}
|
||||||
|
required={
|
||||||
|
data.node!.template[templateField].required
|
||||||
|
}
|
||||||
|
id={{
|
||||||
|
inputTypes:
|
||||||
|
data.node!.template[templateField].input_types,
|
||||||
|
type: data.node!.template[templateField].type,
|
||||||
|
id: data.id,
|
||||||
|
fieldName: templateField,
|
||||||
|
}}
|
||||||
|
left={true}
|
||||||
|
type={data.node?.template[templateField].type}
|
||||||
|
optionalHandle={
|
||||||
|
data.node?.template[templateField].input_types
|
||||||
|
}
|
||||||
|
proxy={data.node?.template[templateField].proxy}
|
||||||
|
showNode={showNode}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{/* <ParameterComponent
|
||||||
|
index={0}
|
||||||
|
key={scapedJSONStringfy({
|
||||||
|
baseClasses: data.node!.base_classes,
|
||||||
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||||
|
title={
|
||||||
|
data.node?.output_types &&
|
||||||
|
data.node.output_types.length > 0
|
||||||
|
? data.node.output_types.join("|")
|
||||||
|
: data.type
|
||||||
|
}
|
||||||
|
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||||
|
id={{
|
||||||
|
baseClasses: data.node!.base_classes,
|
||||||
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
idx: 0,
|
||||||
|
}}
|
||||||
|
type={data.node?.base_classes.join("|")}
|
||||||
|
left={false}
|
||||||
|
showNode={showNode}
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{showNode && (
|
||||||
|
<ShadTooltip
|
||||||
|
content={
|
||||||
|
buildStatus === BuildStatus.BUILDING ? (
|
||||||
|
<span> {STATUS_BUILDING} </span>
|
||||||
|
) : !validationStatus ? (
|
||||||
|
<span className="flex">{STATUS_BUILD}</span>
|
||||||
|
) : (
|
||||||
|
<div className="max-h-100 p-2">
|
||||||
|
<div>
|
||||||
|
{lastRunTime && (
|
||||||
|
<div className="justify-left flex font-normal text-muted-foreground">
|
||||||
|
<div>{RUN_TIMESTAMP_PREFIX}</div>
|
||||||
|
<div className="ml-1 text-status-blue">
|
||||||
|
{lastRunTime}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="justify-left flex font-normal text-muted-foreground">
|
||||||
|
<div>Duration:</div>
|
||||||
|
<div className="mb-3 ml-1 text-status-blue">
|
||||||
|
{validationStatus?.data.duration}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<span className="mb-2 mt-2 flex justify-center font-semibold text-muted-foreground">
|
||||||
|
Output
|
||||||
|
</span>
|
||||||
|
<div className="max-h-96 overflow-auto font-normal custom-scroll">
|
||||||
|
{validationString.split("\n").map((line, index) => (
|
||||||
|
<div className="font-normal" key={index}>
|
||||||
|
{line}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
side="bottom"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (buildStatus === BuildStatus.BUILDING || isBuilding)
|
||||||
|
return;
|
||||||
|
setValidationStatus(null);
|
||||||
|
buildFlow({ stopNodeId: data.id });
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
className={"group h-9 px-1.5"}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-testid={
|
||||||
|
`button_run_` + data?.node?.display_name.toLowerCase()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="generic-node-status-position flex items-center justify-center">
|
||||||
|
{renderIconStatus(buildStatus, validationStatus)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</ShadTooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showNode && (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
showNode
|
||||||
|
? data.node?.description === "" && !nameEditable
|
||||||
|
? "pb-5"
|
||||||
|
: "py-5"
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="generic-node-desc">
|
||||||
|
{showNode && nameEditable && inputDescription ? (
|
||||||
|
<Textarea
|
||||||
|
autoFocus
|
||||||
|
onBlur={() => {
|
||||||
|
setInputDescription(false);
|
||||||
|
setInputName(false);
|
||||||
|
setNodeDescription(nodeDescription);
|
||||||
|
setNode(data.id, (old) => ({
|
||||||
|
...old,
|
||||||
|
data: {
|
||||||
|
...old.data,
|
||||||
|
node: {
|
||||||
|
...old.data.node,
|
||||||
|
description: nodeDescription,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
value={nodeDescription}
|
||||||
|
onChange={(e) => setNodeDescription(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
handleKeyDown(e, nodeDescription, "");
|
||||||
|
if (
|
||||||
|
e.key === "Enter" &&
|
||||||
|
e.shiftKey === false &&
|
||||||
|
e.ctrlKey === false &&
|
||||||
|
e.altKey === false
|
||||||
|
) {
|
||||||
|
setInputDescription(false);
|
||||||
|
setNodeDescription(nodeDescription);
|
||||||
|
setNode(data.id, (old) => ({
|
||||||
|
...old,
|
||||||
|
data: {
|
||||||
|
...old.data,
|
||||||
|
node: {
|
||||||
|
...old.data.node,
|
||||||
|
description: nodeDescription,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"generic-node-desc-text cursor-text truncate-multiline word-break-break-word",
|
||||||
|
(data.node?.description === "" ||
|
||||||
|
!data.node?.description) &&
|
||||||
|
nameEditable
|
||||||
|
? "font-light italic"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
setInputDescription(true);
|
||||||
|
takeSnapshot();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(data.node?.description === "" || !data.node?.description) &&
|
||||||
|
nameEditable
|
||||||
|
? "Double Click to Edit Description"
|
||||||
|
: data.node?.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<>
|
||||||
|
{Object.keys(data.node!.template)
|
||||||
|
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||||
|
.sort((a, b) => sortFields(a, b, data.node?.field_order ?? []))
|
||||||
|
.map((templateField: string, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
|
{data.node!.template[templateField].show &&
|
||||||
|
!data.node!.template[templateField].advanced ? (
|
||||||
|
<ParameterComponent
|
||||||
|
index={idx}
|
||||||
|
key={scapedJSONStringfy({
|
||||||
|
inputTypes:
|
||||||
|
data.node!.template[templateField].input_types,
|
||||||
|
type: data.node!.template[templateField].type,
|
||||||
|
id: data.id,
|
||||||
|
fieldName: templateField,
|
||||||
|
proxy: data.node!.template[templateField].proxy,
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
color={
|
||||||
|
data.node?.template[templateField].input_types &&
|
||||||
|
data.node?.template[templateField].input_types!
|
||||||
|
.length > 0
|
||||||
|
? nodeColors[
|
||||||
|
data.node?.template[templateField].input_types![
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
|
] ??
|
||||||
|
nodeColors[
|
||||||
|
types[
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types![
|
||||||
|
data.node?.template[templateField]
|
||||||
|
.input_types!.length - 1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
: nodeColors[
|
||||||
|
data.node?.template[templateField].type!
|
||||||
|
] ??
|
||||||
|
nodeColors[
|
||||||
|
types[data.node?.template[templateField].type!]
|
||||||
|
] ??
|
||||||
|
nodeColors.unknown
|
||||||
|
}
|
||||||
|
title={getFieldTitle(
|
||||||
|
data.node?.template!,
|
||||||
|
templateField,
|
||||||
|
)}
|
||||||
|
info={data.node?.template[templateField].info}
|
||||||
|
name={templateField}
|
||||||
|
tooltipTitle={
|
||||||
|
data.node?.template[templateField].input_types?.join(
|
||||||
|
"\n",
|
||||||
|
) ?? data.node?.template[templateField].type
|
||||||
|
}
|
||||||
|
required={data.node!.template[templateField].required}
|
||||||
|
id={{
|
||||||
|
inputTypes:
|
||||||
|
data.node!.template[templateField].input_types,
|
||||||
|
type: data.node!.template[templateField].type,
|
||||||
|
id: data.id,
|
||||||
|
fieldName: templateField,
|
||||||
|
}}
|
||||||
|
left={true}
|
||||||
|
type={data.node?.template[templateField].type}
|
||||||
|
optionalHandle={
|
||||||
|
data.node?.template[templateField].input_types
|
||||||
|
}
|
||||||
|
proxy={data.node?.template[templateField].proxy}
|
||||||
|
showNode={showNode}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
Object.keys(data.node!.template).length < 1 ? "hidden" : "",
|
||||||
|
"flex-max-width justify-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
</div>
|
||||||
|
{data.node!.outputs &&
|
||||||
|
data.node!.outputs.length > 0 &&
|
||||||
|
data.node!.outputs.map((output, idx) => (
|
||||||
|
<ParameterComponent
|
||||||
|
index={idx}
|
||||||
|
key={scapedJSONStringfy({
|
||||||
|
output_types: output.types,
|
||||||
|
name: output.name,
|
||||||
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
})}
|
||||||
|
data={data}
|
||||||
|
color={
|
||||||
|
nodeColors[output.selected ?? output.types[0]] ??
|
||||||
|
nodeColors[types[output.selected ?? output.types[0]]] ??
|
||||||
|
nodeColors[types[data.type]] ??
|
||||||
|
nodeColors.unknown
|
||||||
|
}
|
||||||
|
title={output.name}
|
||||||
|
tooltipTitle={output.selected ?? output.types[0]}
|
||||||
|
id={{
|
||||||
|
output_types: [output.selected ?? output.types[0]],
|
||||||
|
id: data.id,
|
||||||
|
dataType: data.type,
|
||||||
|
name: output.name,
|
||||||
|
}}
|
||||||
|
type={output.types.join("|")}
|
||||||
|
left={false}
|
||||||
|
showNode={showNode}
|
||||||
|
outputName={output.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||||
|
|
||||||
|
const useFetchDataOnMount = (
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
handleUpdateValues,
|
||||||
|
setNode,
|
||||||
|
setIsLoading
|
||||||
|
) => {
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
if (
|
||||||
|
(data.node?.template[name]?.real_time_refresh ||
|
||||||
|
data.node?.template[name]?.refresh_button) &&
|
||||||
|
// options can be undefined but not an empty array
|
||||||
|
(data.node?.template[name]?.options?.length ?? 0) === 0
|
||||||
|
) {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
let newTemplate = await handleUpdateValues(name, data);
|
||||||
|
|
||||||
|
if (newTemplate) {
|
||||||
|
setNode(data.id, (oldNode) => {
|
||||||
|
let newNode = cloneDeep(oldNode);
|
||||||
|
newNode.data = {
|
||||||
|
...newNode.data,
|
||||||
|
};
|
||||||
|
newNode.data.node.template = newTemplate;
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
let responseError = error as ResponseErrorDetailAPI;
|
||||||
|
|
||||||
|
setErrorData({
|
||||||
|
title: "Error while updating the Component",
|
||||||
|
list: [responseError?.response?.data?.detail ?? "Unknown error"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData();
|
||||||
|
}, []); // Empty dependency array ensures that this effect runs only once, on mount
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFetchDataOnMount;
|
||||||
73
src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx
Normal file
73
src/frontend/src/CustomNodes/hooks/use-handle-new-value.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
import { ResponseErrorTypeAPI } from "../../types/api";
|
||||||
|
|
||||||
|
const useHandleOnNewValue = (
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
takeSnapshot,
|
||||||
|
handleUpdateValues,
|
||||||
|
debouncedHandleUpdateValues,
|
||||||
|
setNode,
|
||||||
|
setIsLoading
|
||||||
|
) => {
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
||||||
|
const handleOnNewValue = async (newValue, skipSnapshot = false) => {
|
||||||
|
const nodeTemplate = data.node!.template[name];
|
||||||
|
const currentValue = nodeTemplate.value;
|
||||||
|
|
||||||
|
if (currentValue !== newValue && !skipSnapshot) {
|
||||||
|
takeSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldUpdate =
|
||||||
|
data.node?.template[name].real_time_refresh &&
|
||||||
|
!data.node?.template[name].refresh_button &&
|
||||||
|
currentValue !== newValue;
|
||||||
|
|
||||||
|
const typeToDebounce = nodeTemplate.type;
|
||||||
|
|
||||||
|
nodeTemplate.value = newValue;
|
||||||
|
|
||||||
|
let newTemplate;
|
||||||
|
if (shouldUpdate) {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
if (["int"].includes(typeToDebounce)) {
|
||||||
|
newTemplate = await handleUpdateValues(name, data);
|
||||||
|
} else {
|
||||||
|
newTemplate = await debouncedHandleUpdateValues(name, data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
let responseError = error as ResponseErrorTypeAPI;
|
||||||
|
setErrorData({
|
||||||
|
title: "Error while updating the Component",
|
||||||
|
list: [
|
||||||
|
responseError?.response?.data?.detail.error ?? "Unknown error",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
setNode(data.id, (oldNode) => {
|
||||||
|
const newNode = cloneDeep(oldNode);
|
||||||
|
newNode.data = {
|
||||||
|
...newNode.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.node?.template[name].real_time_refresh && newTemplate) {
|
||||||
|
newNode.data.node.template = newTemplate;
|
||||||
|
} else {
|
||||||
|
newNode.data.node.template[name].value = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleOnNewValue };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useHandleOnNewValue;
|
||||||
37
src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx
Normal file
37
src/frontend/src/CustomNodes/hooks/use-handle-node-class.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
|
||||||
|
const useHandleNodeClass = (
|
||||||
|
data,
|
||||||
|
name,
|
||||||
|
takeSnapshot,
|
||||||
|
setNode,
|
||||||
|
updateNodeInternals
|
||||||
|
) => {
|
||||||
|
const handleNodeClass = (newNodeClass, code) => {
|
||||||
|
if (!data.node) return;
|
||||||
|
if (data.node!.template[name].value !== code) {
|
||||||
|
takeSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
setNode(data.id, (oldNode) => {
|
||||||
|
let newNode = cloneDeep(oldNode);
|
||||||
|
|
||||||
|
newNode.data = {
|
||||||
|
...newNode.data,
|
||||||
|
node: newNodeClass,
|
||||||
|
description: newNodeClass.description ?? data.node!.description,
|
||||||
|
display_name: newNodeClass.display_name ?? data.node!.display_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
newNode.data.node.template[name].value = code;
|
||||||
|
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateNodeInternals(data.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleNodeClass };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useHandleNodeClass;
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { cloneDeep } from "lodash";
|
||||||
|
import useAlertStore from "../../stores/alertStore";
|
||||||
|
import { ResponseErrorDetailAPI } from "../../types/api";
|
||||||
|
import { handleUpdateValues } from "../../utils/parameterUtils";
|
||||||
|
|
||||||
|
const useHandleRefreshButtonPress = (setIsLoading, setNode) => {
|
||||||
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
||||||
|
const handleRefreshButtonPress = async (name, data) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
let newTemplate = await handleUpdateValues(name, data);
|
||||||
|
|
||||||
|
if (newTemplate) {
|
||||||
|
setNode(data.id, (oldNode) => {
|
||||||
|
let newNode = cloneDeep(oldNode);
|
||||||
|
newNode.data = {
|
||||||
|
...newNode.data,
|
||||||
|
};
|
||||||
|
newNode.data.node.template = newTemplate;
|
||||||
|
return newNode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
let responseError = error as ResponseErrorDetailAPI;
|
||||||
|
|
||||||
|
setErrorData({
|
||||||
|
title: "Error while updating the Component",
|
||||||
|
list: [responseError?.response?.data?.detail ?? "Unknown error"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { handleRefreshButtonPress };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useHandleRefreshButtonPress;
|
||||||
10
src/frontend/src/CustomNodes/utils/get-field-title.tsx
Normal file
10
src/frontend/src/CustomNodes/utils/get-field-title.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { APITemplateType } from "../../types/api";
|
||||||
|
|
||||||
|
export default function getFieldTitle(
|
||||||
|
template: APITemplateType,
|
||||||
|
templateField: string
|
||||||
|
): string {
|
||||||
|
return template[templateField].display_name
|
||||||
|
? template[templateField].display_name!
|
||||||
|
: template[templateField].name ?? templateField;
|
||||||
|
}
|
||||||
40
src/frontend/src/CustomNodes/utils/sort-fields.tsx
Normal file
40
src/frontend/src/CustomNodes/utils/sort-fields.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { priorityFields } from "../../constants/constants";
|
||||||
|
|
||||||
|
export default function sortFields(a, b, fieldOrder) {
|
||||||
|
// Early return for empty fields
|
||||||
|
if (!a && !b) return 0;
|
||||||
|
if (!a) return 1;
|
||||||
|
if (!b) return -1;
|
||||||
|
|
||||||
|
// Normalize the case to ensure case-insensitive comparison
|
||||||
|
const normalizedFieldA = a.toLowerCase();
|
||||||
|
const normalizedFieldB = b.toLowerCase();
|
||||||
|
|
||||||
|
const aIsPriority = priorityFields.has(normalizedFieldA);
|
||||||
|
const bIsPriority = priorityFields.has(normalizedFieldB);
|
||||||
|
|
||||||
|
// Sort by priority
|
||||||
|
if (aIsPriority && !bIsPriority) return -1;
|
||||||
|
if (!aIsPriority && bIsPriority) return 1;
|
||||||
|
|
||||||
|
// Check if either field is in the fieldOrder array
|
||||||
|
const indexOfA = fieldOrder.indexOf(normalizedFieldA);
|
||||||
|
const indexOfB = fieldOrder.indexOf(normalizedFieldB);
|
||||||
|
|
||||||
|
// If both fields are in fieldOrder, sort by their order in the array
|
||||||
|
if (indexOfA !== -1 && indexOfB !== -1) {
|
||||||
|
return indexOfA - indexOfB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only one of the fields is in fieldOrder, that field comes first
|
||||||
|
if (indexOfA !== -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (indexOfB !== -1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default case for fields not in priorityFields and not found in fieldOrder
|
||||||
|
// You might want to sort them alphabetically or in another specific manner
|
||||||
|
return a.localeCompare(b);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue