From 94140ccf2e4f0482827f608faa95e00565022598 Mon Sep 17 00:00:00 2001 From: Cristhian Zanforlin Lousa Date: Mon, 10 Mar 2025 10:10:55 -0300 Subject: [PATCH] feat: Enhance Webhook component (#6313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝 (constants.py): Add "copy_field" attribute to FIELD_FORMAT_ATTRIBUTES list 📝 (webhook.py): Add "copy_field" attribute to MultilineInput component 📝 (input_mixin.py): Add "copy_field" attribute to BaseInputMixin class 📝 (inputs.py): Add "copy_field" attribute to StrInput class 📝 (template/field/base.py): Add "copy_field" attribute to Input class 🚀 (NodeDescription/index.tsx): Remove default placeholder text for emptyPlaceholder prop ✨ (copyFieldAreaComponent/index.tsx): Add new component for handling copy field functionality ♻️ (strRenderComponent/index.tsx): Refactor component to include CopyFieldAreaComponent when copy_field attribute is present in template data * ✨ (NodeDescription/index.tsx): refactor renderedDescription useMemo to improve readability and maintainability ♻️ (GenericNode/index.tsx): refactor code to improve readability and maintainability, and optimize rendering logic * 📝 (webhook.py): Add cURL field to WebhookComponent for better integration with external systems 📝 (graph/base.py): Add logging of vertex build information in Graph class for debugging purposes 📝 (NodeInputField/index.tsx): Add nodeInformationMetadata to NodeInputField for better tracking of node information 📝 (copyFieldAreaComponent/index.tsx): Refactor CopyFieldAreaComponent to handle different types of values, including webhooks 📝 (strRenderComponent/index.tsx): Add WebhookFieldComponent to handle webhook type in StrRenderComponent 📝 (tableNodeCellRender/index.tsx): Add nodeInformationMetadata to TableNodeCellRender for better tracking of node information 📝 (textAreaComponent/index.tsx): Add support for webhook format in TextAreaComponent for better integration with webhooks 📝 (webhookFieldComponent/index.tsx): Add WebhookFieldComponent to handle webhook type in ParameterRenderComponent 📝 (custom-parameter.tsx): Add nodeInformationMetadata to CustomParameterComponent for better tracking of node information 📝 (get-curl-code.tsx): Add support for different formats in getCurlWebhookCode for generating cURL commands 📝 (textAreaModal/index.tsx): Add onCloseModal callback to ComponentTextModal for better handling of modal closing 📝 (index.ts): Add type field to APIClassType for better typing of API classes * ✨ (index.tsx): Add a button to generate a token in the WebhookFieldComponent for improved user experience and functionality. Update the structure of the component to include the new button and styling adjustments. * [autofix.ci] apply automated fixes * ✨ (generate-token-dialog.tsx): add GenerateTokenDialog component to handle token generation in webhookFieldComponent 📝 (index.tsx): import and use GenerateTokenDialog component in WebhookFieldComponent for token generation functionality * ✨ (frontend): introduce new feature to create API keys with customizable modal properties 🔧 (frontend): add modalProps object to customize modal title, description, input label, input placeholder, button text, generated key message, and show icon flag * add pool interval variable and tests * 📝 (NodeOutputfield): Remove unused ScanEyeIcon component ✨ (validate-webhook.ts): Add function to validate webhook data before processing ♻️ (use-get-builds-pooling-mutation): Refactor to set flow pool based on current flow 🔧 (content-render.tsx): Add data-testid attribute to api key input element 🔧 (webhookComponent.spec.ts): Refactor test to use waitForRequest for monitoring build requests * [autofix.ci] apply automated fixes * 🔧 (backend): rename webhook_pooling_interval to webhook_polling_interval for consistency 🔧 (frontend): update references to webhook_pooling_interval to webhook_polling_interval for consistency * 📝 (frontend): Update import paths and remove unused imports for better code organization and maintainability 🔧 (frontend): Refactor background styles in components to use constants for consistency and easier theming 🚀 (frontend): Add custom SecretKeyModalButton component for better modularity and reusability * 📝 (use-get-api-keys.ts): add a TODO comment to request API key from DSLF endpoint for future implementation. * 📝 (input_mixin.py): Remove copy_field attribute from BaseInputMixin as it is no longer needed ♻️ (inputs.py): Remove copy_field attribute from StrInput class as it is no longer needed ♻️ (inputs.py): Set copy_field attribute to False in MultilineInput class to ensure consistency ♻️ (template/field/base.py): Remove copy_field attribute from Input class as it is no longer needed 📝 (textAreaComponent/index.tsx): Replace hardcoded value "CURL_WEBHOOK" with constant WEBHOOK_VALUE for better readability and maintainability * 🐛 (base.py): fix issue where flow_id could be None by defaulting to an empty string if flow_id is None * 🔧 (secret-key-modal.tsx): Remove unused SecretKeyModalButton component 🔧 (get-modal-props.tsx): Remove unused getModalPropsApiKey function and related imports and constants * 📝 (langflow): add noqa comments to suppress linting rule A005 for specific files in the io, logging, and socket modules * [autofix.ci] apply automated fixes * 📝 (frontend): Remove unused endpointName property from NodeInputField component 🔧 (frontend): Add useFlowStore import and use it to get currentFlow and endpointName in CopyFieldAreaComponent, TableNodeCellRender, TextAreaComponent, and WebhookFieldComponent components 🔧 (frontend): Refactor useGetBuildsMutation to handle multiple concurrent requests and prevent duplicate requests * ✨ (webhookComponent.spec.ts): refactor test to improve readability and maintainability by removing redundant code and focusing on essential test steps ♻️ (userSettings.spec.ts): refactor test to improve readability and maintainability by removing redundant code and focusing on essential test steps --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/backend/base/langflow/__main__.py | 5 + src/backend/base/langflow/api/v1/schemas.py | 1 + src/backend/base/langflow/base/constants.py | 1 + .../base/langflow/components/data/webhook.py | 19 +- src/backend/base/langflow/graph/graph/base.py | 10 + src/backend/base/langflow/inputs/inputs.py | 1 + .../base/langflow/services/settings/base.py | 2 + .../base/langflow/template/field/base.py | 2 + src/backend/base/langflow/utils/util.py | 4 + .../components/NodeDescription/index.tsx | 36 ++-- .../components/NodeInputField/index.tsx | 17 +- .../components/NodeOutputfield/index.tsx | 8 - .../src/CustomNodes/GenericNode/index.tsx | 22 +- .../copyFieldAreaComponent/index.tsx | 155 ++++++++++++++ .../components/strRenderComponent/index.tsx | 58 ++++-- .../components/tableNodeCellRender/index.tsx | 16 ++ .../components/textAreaComponent/index.tsx | 47 ++++- .../webhookFieldComponent/index.tsx | 77 +++++++ .../core/parameterRenderComponent/index.tsx | 5 +- .../core/parameterRenderComponent/types.ts | 9 + src/frontend/src/constants/constants.ts | 3 + .../API/helpers/validate-webhook.ts | 11 + .../use-get-builds-polling-mutation.ts | 189 ++++++++++++++++++ .../API/queries/api-keys/use-get-api-keys.ts | 1 + .../API/queries/config/use-get-config.ts | 5 + .../components/custom-parameter.tsx | 4 + .../custom-secret-key-modal-button.tsx | 13 ++ .../customization/utils/get-modal-props.tsx | 1 + .../modals/apiModal/utils/get-curl-code.tsx | 16 +- src/frontend/src/modals/baseModal/index.tsx | 11 +- .../components/content-render.tsx | 55 +++++ .../components/form-key-render.tsx | 41 ++++ .../components/header-render.tsx | 16 ++ .../src/modals/secretKeyModal/index.tsx | 91 ++++----- .../src/modals/textAreaModal/index.tsx | 7 + .../components/bundleItems/index.tsx | 17 +- .../components/categoryDisclouse/index.tsx | 9 +- .../components/categoryGroup/index.tsx | 4 +- .../components/sidebarBundles/index.tsx | 20 +- .../sidebarDraggableComponent/index.tsx | 1 + .../components/sidebarItemsList/index.tsx | 11 +- .../helpers/disable-item.ts | 14 ++ .../helpers/get-disabled-tooltip.ts | 14 ++ .../components/flowSidebarComponent/index.tsx | 17 +- .../flowSidebarComponent/types/index.ts | 59 +++++- .../components/ApiKeyHeader/index.tsx | 8 +- .../ApiKeysPage/helpers/get-modal-props.tsx | 26 +++ .../SettingsPage/pages/ApiKeysPage/index.tsx | 1 - src/frontend/src/stores/utilityStore.ts | 3 + src/frontend/src/style/applies.css | 2 +- src/frontend/src/types/api/index.ts | 1 + src/frontend/src/types/components/index.ts | 26 ++- src/frontend/src/types/tweaks/index.ts | 4 +- .../src/types/zustand/utility/index.ts | 2 + src/frontend/src/utils/reactflowUtils.ts | 4 + .../tests/core/unit/webhookComponent.spec.ts | 127 ++++++++++++ .../extended/features/userSettings.spec.ts | 8 +- 57 files changed, 1155 insertions(+), 182 deletions(-) create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/copyFieldAreaComponent/index.tsx create mode 100644 src/frontend/src/components/core/parameterRenderComponent/components/webhookFieldComponent/index.tsx create mode 100644 src/frontend/src/controllers/API/helpers/validate-webhook.ts create mode 100644 src/frontend/src/controllers/API/queries/_builds/use-get-builds-polling-mutation.ts create mode 100644 src/frontend/src/customization/components/custom-secret-key-modal-button.tsx create mode 100644 src/frontend/src/customization/utils/get-modal-props.tsx create mode 100644 src/frontend/src/modals/secretKeyModal/components/content-render.tsx create mode 100644 src/frontend/src/modals/secretKeyModal/components/form-key-render.tsx create mode 100644 src/frontend/src/modals/secretKeyModal/components/header-render.tsx create mode 100644 src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/disable-item.ts create mode 100644 src/frontend/src/pages/FlowPage/components/flowSidebarComponent/helpers/get-disabled-tooltip.ts create mode 100644 src/frontend/src/pages/SettingsPage/pages/ApiKeysPage/helpers/get-modal-props.tsx create mode 100644 src/frontend/tests/core/unit/webhookComponent.spec.ts diff --git a/src/backend/base/langflow/__main__.py b/src/backend/base/langflow/__main__.py index cbfe65d96..8ef351b88 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 8ff419d39..777390a12 100644 --- a/src/backend/base/langflow/api/v1/schemas.py +++ b/src/backend/base/langflow/api/v1/schemas.py @@ -376,6 +376,7 @@ class ConfigResponse(BaseModel): auto_saving_interval: int health_check_max_retries: int max_file_size_upload: int + webhook_polling_interval: int event_delivery: Literal["polling", "streaming"] diff --git a/src/backend/base/langflow/base/constants.py b/src/backend/base/langflow/base/constants.py index 3e2f2f6c7..4e9988cd8 100644 --- a/src/backend/base/langflow/base/constants.py +++ b/src/backend/base/langflow/base/constants.py @@ -38,6 +38,7 @@ FIELD_FORMAT_ATTRIBUTES = [ "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 537836c66..e4860fd1c 100644 --- a/src/backend/base/langflow/components/data/webhook.py +++ b/src/backend/base/langflow/components/data/webhook.py @@ -7,7 +7,6 @@ from langflow.schema import Data 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 6a6a4b422..45a3eeddc 100644 --- a/src/backend/base/langflow/graph/graph/base.py +++ b/src/backend/base/langflow/graph/graph/base.py @@ -31,6 +31,7 @@ from langflow.graph.graph.utils import ( 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 @@ -1607,6 +1608,15 @@ class Graph: t.cancel() raise result if isinstance(result, VertexBuildResult): + await log_vertex_build( + flow_id=self.flow_id or "", + 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 e72366756..13e4f6879 100644 --- a/src/backend/base/langflow/inputs/inputs.py +++ b/src/backend/base/langflow/inputs/inputs.py @@ -240,6 +240,7 @@ class MultilineInput(MessageTextInput, MultilineMixin, InputTraceMixin, ToolMode field_type: SerializableFieldTypes = FieldTypes.TEXT multiline: CoalesceBool = True + copy_field: CoalesceBool = False class MultilineSecretInput(MessageTextInput, MultilineMixin, InputTraceMixin): diff --git a/src/backend/base/langflow/services/settings/base.py b/src/backend/base/langflow/services/settings/base.py index 5f2bdc356..c97f37fab 100644 --- a/src/backend/base/langflow/services/settings/base.py +++ b/src/backend/base/langflow/services/settings/base.py @@ -215,6 +215,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 e16a9fe7b..559709edb 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 b0cb98456..5b85910ba 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 f43d4b9bc..c81dca651 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 8f03374ad..8d6407a0d 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,16 @@ export default function NodeInputField({ name, }); + const nodeInformationMetadata: NodeInfoType = useMemo(() => { + return { + flowId: currentFlow?.id ?? "", + nodeType: data?.type?.toLowerCase() ?? "", + flowName: currentFlow?.name ?? "", + isAuth, + variableName: name, + }; + }, [data?.node?.id, isAuth, name]); + useFetchDataOnMount(data.node!, handleNodeClass, name, postTemplateValue); useEffect(() => { @@ -193,6 +207,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 d7945ad45..c6df43a4b 100644 --- a/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/components/NodeOutputfield/index.tsx @@ -49,14 +49,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 30577c4d7..05e6cd215 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 = "", +}: 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 currentFlow = useFlowStore((state) => state.currentFlow); + const endpointName = currentFlow?.endpoint_name ?? ""; + + const valueToRender = useMemo(() => { + if (isValueToReplace) { + const urlWebhook = `${URL_WEBHOOK}${endpointName}`; + return isValueToReplace ? urlWebhook : value; + } + return value; + }, [value, endpointName]); + + 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 && ( +