diff --git a/src/backend/base/langflow/__main__.py b/src/backend/base/langflow/__main__.py index cbfe65d96f35..8ef351b88ab6 100644 --- a/src/backend/base/langflow/__main__.py +++ b/src/backend/base/langflow/__main__.py @@ -156,6 +156,11 @@ def run( help="Defines the maximum file size for the upload in MB.", show_default=False, ), + webhook_polling_interval: int | None = typer.Option( # noqa: ARG001 + None, + help="Defines the polling interval for the webhook.", + show_default=False, + ), ) -> None: """Run Langflow.""" # Register SIGTERM handler diff --git a/src/backend/base/langflow/api/v1/schemas.py b/src/backend/base/langflow/api/v1/schemas.py index 0d90d12df930..3c9e72903021 100644 --- a/src/backend/base/langflow/api/v1/schemas.py +++ b/src/backend/base/langflow/api/v1/schemas.py @@ -376,3 +376,4 @@ class ConfigResponse(BaseModel): auto_saving_interval: int health_check_max_retries: int max_file_size_upload: int + webhook_polling_interval: int diff --git a/src/backend/base/langflow/base/constants.py b/src/backend/base/langflow/base/constants.py index 3e2f2f6c75a1..4e9988cd8c5b 100644 --- a/src/backend/base/langflow/base/constants.py +++ b/src/backend/base/langflow/base/constants.py @@ -38,6 +38,7 @@ "refresh_button_text", "options", "advanced", + "copy_field", ] ORJSON_OPTIONS = orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS | orjson.OPT_OMIT_MICROSECONDS diff --git a/src/backend/base/langflow/components/data/webhook.py b/src/backend/base/langflow/components/data/webhook.py index 537836c66ace..e4860fd1c251 100644 --- a/src/backend/base/langflow/components/data/webhook.py +++ b/src/backend/base/langflow/components/data/webhook.py @@ -7,7 +7,6 @@ class WebhookComponent(Component): display_name = "Webhook" - description = "Defines a webhook input for the flow." name = "Webhook" icon = "webhook" @@ -16,7 +15,23 @@ class WebhookComponent(Component): name="data", display_name="Payload", info="Receives a payload from external systems via HTTP POST.", - ) + advanced=True, + ), + MultilineInput( + name="curl", + display_name="cURL", + value="CURL_WEBHOOK", + advanced=True, + input_types=[], + ), + MultilineInput( + name="endpoint", + display_name="Endpoint", + value="BACKEND_URL", + advanced=False, + copy_field=True, + input_types=[], + ), ] outputs = [ Output(display_name="Data", name="output_data", method="build_data"), diff --git a/src/backend/base/langflow/graph/graph/base.py b/src/backend/base/langflow/graph/graph/base.py index f7b3eec89414..6b65fe49ccd7 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -31,6 +31,7 @@ should_continue, ) from langflow.graph.schema import InterfaceComponentTypes, RunOutputs +from langflow.graph.utils import log_vertex_build from langflow.graph.vertex.base import Vertex, VertexStates from langflow.graph.vertex.schema import NodeData, NodeTypeEnum from langflow.graph.vertex.vertex_types import ComponentVertex, InterfaceVertex, StateVertex @@ -1596,6 +1597,15 @@ async def _execute_tasks(self, tasks: list[asyncio.Task], lock: asyncio.Lock) -> t.cancel() raise result if isinstance(result, VertexBuildResult): + await log_vertex_build( + flow_id=self.flow_id, + vertex_id=result.vertex.id, + valid=result.valid, + params=result.params, + data=result.result_dict, + artifacts=result.artifacts, + ) + vertices.append(result.vertex) else: msg = f"Invalid result from task {task_name}: {result}" diff --git a/src/backend/base/langflow/inputs/inputs.py b/src/backend/base/langflow/inputs/inputs.py index 48e0c0649693..6c77f89fe894 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -226,7 +226,7 @@ class MultilineInput(MessageTextInput, MultilineMixin, InputTraceMixin, ToolMode field_type: SerializableFieldTypes = FieldTypes.TEXT multiline: CoalesceBool = True - + copy_field: CoalesceBool = False class MultilineSecretInput(MessageTextInput, MultilineMixin, InputTraceMixin): """Represents a multiline input field. diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py index 687e4f6f1c8a..7500ba0614f1 100644 --- a/src/backend/base/langflow/services/settings/base.py +++ b/src/backend/base/langflow/services/settings/base.py @@ -198,6 +198,8 @@ class Settings(BaseSettings): """The maximum number of vertex builds to keep in the database.""" max_vertex_builds_per_vertex: int = 2 """The maximum number of builds to keep per vertex. Older builds will be deleted.""" + webhook_polling_interval: int = 5000 + """The polling interval for the webhook in ms.""" # MCP Server mcp_server_enabled: bool = True diff --git a/src/backend/base/langflow/template/field/base.py b/src/backend/base/langflow/template/field/base.py index e16a9fe7b07f..559709edbdf5 100644 --- a/src/backend/base/langflow/template/field/base.py +++ b/src/backend/base/langflow/template/field/base.py @@ -89,6 +89,7 @@ class Input(BaseModel): refresh_button: bool | None = None """Specifies if the field should have a refresh button. Defaults to False.""" + refresh_button_text: str | None = None """Specifies the text for the refresh button. Defaults to None.""" @@ -97,6 +98,7 @@ class Input(BaseModel): load_from_db: bool = False """Specifies if the field should be loaded from the database. Defaults to False.""" + title_case: bool = False """Specifies if the field should be displayed in title case. Defaults to True.""" diff --git a/src/backend/base/langflow/utils/util.py b/src/backend/base/langflow/utils/util.py index b0cb9845607c..5b85910ba583 100644 --- a/src/backend/base/langflow/utils/util.py +++ b/src/backend/base/langflow/utils/util.py @@ -415,6 +415,7 @@ async def update_settings( auto_saving_interval: int = 1000, health_check_max_retries: int = 5, max_file_size_upload: int = 100, + webhook_polling_interval: int = 5000, ) -> None: """Update the settings from a config file.""" # Check for database_url in the environment variables @@ -448,6 +449,9 @@ async def update_settings( if max_file_size_upload is not None: logger.debug(f"Setting max_file_size_upload to {max_file_size_upload}") settings_service.settings.update_settings(max_file_size_upload=max_file_size_upload) + if webhook_polling_interval is not None: + logger.debug(f"Setting webhook_polling_interval to {webhook_polling_interval}") + settings_service.settings.update_settings(webhook_polling_interval=webhook_polling_interval) def is_class_method(func, cls): diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx index f43d4b9bca5e..c81dca65126c 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeDescription/index.tsx @@ -10,7 +10,7 @@ export default function NodeDescription({ description, selected, nodeId, - emptyPlaceholder = "Double Click to Edit Description", + emptyPlaceholder = "", placeholderClassName, charLimit, inputClassName, @@ -69,23 +69,23 @@ export default function NodeDescription({ }, [description]); const MemoizedMarkdown = memo(Markdown); - const renderedDescription = useMemo( - () => - description === "" || !description ? ( - emptyPlaceholder - ) : ( - - {String(description)} - - ), - [description, emptyPlaceholder, mdClassName], - ); + + const renderedDescription = useMemo(() => { + if (description === "" || !description) { + return emptyPlaceholder; + } + return ( + + {String(description)} + + ); + }, [description, emptyPlaceholder, mdClassName]); const handleBlurFn = () => { setNodeDescription(nodeDescription); diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx index 8f03374ad121..e0223f6ed60c 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeInputField/index.tsx @@ -1,12 +1,14 @@ import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class"; +import { NodeInfoType } from "@/components/core/parameterRenderComponent/types"; import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value"; import { CustomParameterComponent, CustomParameterLabel, getCustomParameterTitle, } from "@/customization/components/custom-parameter"; +import useAuthStore from "@/stores/authStore"; import { cn } from "@/utils/utils"; -import { useEffect, useRef } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { default as IconComponent } from "../../../../components/common/genericIconComponent"; import ShadTooltip from "../../../../components/common/shadTooltipComponent"; import { @@ -44,6 +46,8 @@ export default function NodeInputField({ const ref = useRef(null); const nodes = useFlowStore((state) => state.nodes); const edges = useFlowStore((state) => state.edges); + const isAuth = useAuthStore((state) => state.isAuthenticated); + const currentFlow = useFlowStore((state) => state.currentFlow); const myData = useTypesStore((state) => state.data); const postTemplateValue = usePostTemplateValue({ node: data.node!, @@ -64,6 +68,17 @@ export default function NodeInputField({ name, }); + const nodeInformationMetadata: NodeInfoType = useMemo(() => { + return { + flowId: currentFlow?.id ?? "", + nodeType: data?.type?.toLowerCase() ?? "", + flowName: currentFlow?.name ?? "", + endpointName: currentFlow?.endpoint_name ?? "", + isAuth, + variableName: name, + }; + }, [data?.node?.id, isAuth, name]); + useFetchDataOnMount(data.node!, handleNodeClass, name, postTemplateValue); useEffect(() => { @@ -193,6 +208,7 @@ export default function NodeInputField({ : data.node?.template[name].placeholder } isToolMode={isToolMode} + nodeInformationMetadata={nodeInformationMetadata} /> )} diff --git a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx index ae4d86c883c1..7622917f506b 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx @@ -42,14 +42,6 @@ const SnowflakeIcon = memo(() => ( )); -const ScanEyeIcon = memo(({ className }: { className: string }) => ( - -)); - // Memoize Button components const HideShowButton = memo( ({ diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index bb86f9b2574a..e9b5c797b79d 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -252,6 +252,13 @@ function GenericNode({ const [hasChangedNodeDescription, setHasChangedNodeDescription] = useState(false); + const editedNameDescription = + editNameDescription && hasChangedNodeDescription; + + const hasDescription = useMemo(() => { + return data.node?.description && data.node?.description !== ""; + }, [data.node?.description]); + const memoizedNodeToolbarComponent = useMemo(() => { return selected ? ( <> @@ -294,25 +301,21 @@ function GenericNode({ showNode ? "top-2 translate-x-[10.4rem]" : "top-0 translate-x-[6.4rem]", - editNameDescription && hasChangedNodeDescription + editedNameDescription ? "bg-accent-emerald" : "bg-zinc-foreground", )} data-testid={ - editNameDescription && hasChangedNodeDescription + editedNameDescription ? "save-name-description-button" : "edit-name-description-button" } >
+ `w-full ${isFocused ? "" : "pr-3"}`, + editNode: "input-edit-node", + normal: ({ isFocused }: { isFocused: boolean }) => + `primary-input ${isFocused ? "text-primary" : "text-muted-foreground"}`, + disabled: "disabled-state", +}; + +const externalLinkIconClasses = { + gradient: ({ + editNode, + disabled, + }: { + editNode: boolean; + disabled: boolean; + }) => + disabled + ? "gradient-fade-input-edit-node" + : editNode + ? "gradient-fade-input-edit-node" + : "gradient-fade-input", + background: ({ + editNode, + disabled, + }: { + editNode: boolean; + disabled: boolean; + }) => + disabled + ? "" + : editNode + ? "background-fade-input-edit-node" + : "background-fade-input", + icon: "icons-parameters-comp absolute right-3 h-4 w-4 shrink-0", + editNodeTop: "top-[-1.4rem] h-5", + normalTop: "top-[-2.1rem] h-7", + iconTop: "top-[-1.7rem]", +}; + +export default function CopyFieldAreaComponent({ + value, + handleOnNewValue, + editNode = false, + id = "", + nodeInformationMetadata, +}: InputProps): JSX.Element { + const inputRef = useRef(null); + const [isFocused, setIsFocused] = useState(false); + const [isCopied, setIsCopied] = useState(false); + + const isValueToReplace = value === BACKEND_URL; + const setSuccessData = useAlertStore((state) => state.setSuccessData); + + const valueToRender = useMemo(() => { + if (isValueToReplace) { + return isValueToReplace ? URL_WEBHOOK : value; + } + return value; + }, [value]); + + const getInputClassName = () => { + return cn( + inputClasses.base({ isFocused }), + editNode ? inputClasses.editNode : inputClasses.normal({ isFocused }), + isFocused && "pr-10", + ); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + handleOnNewValue({ value: e.target.value }); + }; + + const handleCopy = (event?: React.MouseEvent) => { + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + navigator.clipboard.writeText(valueToRender); + + setSuccessData({ + title: "Endpoint URL copied", + }); + + event?.stopPropagation(); + }; + + const renderIcon = () => ( + <> + {!isFocused && ( +