feat: Enhance Webhook component (#6313)
* 📝 (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>
This commit is contained in:
parent
08f886f507
commit
94140ccf2e
57 changed files with 1155 additions and 182 deletions
|
|
@ -156,6 +156,11 @@ def run(
|
||||||
help="Defines the maximum file size for the upload in MB.",
|
help="Defines the maximum file size for the upload in MB.",
|
||||||
show_default=False,
|
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:
|
) -> None:
|
||||||
"""Run Langflow."""
|
"""Run Langflow."""
|
||||||
# Register SIGTERM handler
|
# Register SIGTERM handler
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@ class ConfigResponse(BaseModel):
|
||||||
auto_saving_interval: int
|
auto_saving_interval: int
|
||||||
health_check_max_retries: int
|
health_check_max_retries: int
|
||||||
max_file_size_upload: int
|
max_file_size_upload: int
|
||||||
|
webhook_polling_interval: int
|
||||||
event_delivery: Literal["polling", "streaming"]
|
event_delivery: Literal["polling", "streaming"]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ FIELD_FORMAT_ATTRIBUTES = [
|
||||||
"refresh_button_text",
|
"refresh_button_text",
|
||||||
"options",
|
"options",
|
||||||
"advanced",
|
"advanced",
|
||||||
|
"copy_field",
|
||||||
]
|
]
|
||||||
|
|
||||||
ORJSON_OPTIONS = orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS | orjson.OPT_OMIT_MICROSECONDS
|
ORJSON_OPTIONS = orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS | orjson.OPT_OMIT_MICROSECONDS
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from langflow.schema import Data
|
||||||
|
|
||||||
class WebhookComponent(Component):
|
class WebhookComponent(Component):
|
||||||
display_name = "Webhook"
|
display_name = "Webhook"
|
||||||
description = "Defines a webhook input for the flow."
|
|
||||||
name = "Webhook"
|
name = "Webhook"
|
||||||
icon = "webhook"
|
icon = "webhook"
|
||||||
|
|
||||||
|
|
@ -16,7 +15,23 @@ class WebhookComponent(Component):
|
||||||
name="data",
|
name="data",
|
||||||
display_name="Payload",
|
display_name="Payload",
|
||||||
info="Receives a payload from external systems via HTTP POST.",
|
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 = [
|
outputs = [
|
||||||
Output(display_name="Data", name="output_data", method="build_data"),
|
Output(display_name="Data", name="output_data", method="build_data"),
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ from langflow.graph.graph.utils import (
|
||||||
should_continue,
|
should_continue,
|
||||||
)
|
)
|
||||||
from langflow.graph.schema import InterfaceComponentTypes, RunOutputs
|
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.base import Vertex, VertexStates
|
||||||
from langflow.graph.vertex.schema import NodeData, NodeTypeEnum
|
from langflow.graph.vertex.schema import NodeData, NodeTypeEnum
|
||||||
from langflow.graph.vertex.vertex_types import ComponentVertex, InterfaceVertex, StateVertex
|
from langflow.graph.vertex.vertex_types import ComponentVertex, InterfaceVertex, StateVertex
|
||||||
|
|
@ -1607,6 +1608,15 @@ class Graph:
|
||||||
t.cancel()
|
t.cancel()
|
||||||
raise result
|
raise result
|
||||||
if isinstance(result, VertexBuildResult):
|
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)
|
vertices.append(result.vertex)
|
||||||
else:
|
else:
|
||||||
msg = f"Invalid result from task {task_name}: {result}"
|
msg = f"Invalid result from task {task_name}: {result}"
|
||||||
|
|
|
||||||
|
|
@ -240,6 +240,7 @@ class MultilineInput(MessageTextInput, MultilineMixin, InputTraceMixin, ToolMode
|
||||||
|
|
||||||
field_type: SerializableFieldTypes = FieldTypes.TEXT
|
field_type: SerializableFieldTypes = FieldTypes.TEXT
|
||||||
multiline: CoalesceBool = True
|
multiline: CoalesceBool = True
|
||||||
|
copy_field: CoalesceBool = False
|
||||||
|
|
||||||
|
|
||||||
class MultilineSecretInput(MessageTextInput, MultilineMixin, InputTraceMixin):
|
class MultilineSecretInput(MessageTextInput, MultilineMixin, InputTraceMixin):
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,8 @@ class Settings(BaseSettings):
|
||||||
"""The maximum number of vertex builds to keep in the database."""
|
"""The maximum number of vertex builds to keep in the database."""
|
||||||
max_vertex_builds_per_vertex: int = 2
|
max_vertex_builds_per_vertex: int = 2
|
||||||
"""The maximum number of builds to keep per vertex. Older builds will be deleted."""
|
"""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
|
||||||
mcp_server_enabled: bool = True
|
mcp_server_enabled: bool = True
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ class Input(BaseModel):
|
||||||
|
|
||||||
refresh_button: bool | None = None
|
refresh_button: bool | None = None
|
||||||
"""Specifies if the field should have a refresh button. Defaults to False."""
|
"""Specifies if the field should have a refresh button. Defaults to False."""
|
||||||
|
|
||||||
refresh_button_text: str | None = None
|
refresh_button_text: str | None = None
|
||||||
"""Specifies the text for the refresh button. Defaults to None."""
|
"""Specifies the text for the refresh button. Defaults to None."""
|
||||||
|
|
||||||
|
|
@ -97,6 +98,7 @@ class Input(BaseModel):
|
||||||
|
|
||||||
load_from_db: bool = False
|
load_from_db: bool = False
|
||||||
"""Specifies if the field should be loaded from the database. Defaults to False."""
|
"""Specifies if the field should be loaded from the database. Defaults to False."""
|
||||||
|
|
||||||
title_case: bool = False
|
title_case: bool = False
|
||||||
"""Specifies if the field should be displayed in title case. Defaults to True."""
|
"""Specifies if the field should be displayed in title case. Defaults to True."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,7 @@ async def update_settings(
|
||||||
auto_saving_interval: int = 1000,
|
auto_saving_interval: int = 1000,
|
||||||
health_check_max_retries: int = 5,
|
health_check_max_retries: int = 5,
|
||||||
max_file_size_upload: int = 100,
|
max_file_size_upload: int = 100,
|
||||||
|
webhook_polling_interval: int = 5000,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update the settings from a config file."""
|
"""Update the settings from a config file."""
|
||||||
# Check for database_url in the environment variables
|
# Check for database_url in the environment variables
|
||||||
|
|
@ -448,6 +449,9 @@ async def update_settings(
|
||||||
if max_file_size_upload is not None:
|
if max_file_size_upload is not None:
|
||||||
logger.debug(f"Setting max_file_size_upload to {max_file_size_upload}")
|
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)
|
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):
|
def is_class_method(func, cls):
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export default function NodeDescription({
|
||||||
description,
|
description,
|
||||||
selected,
|
selected,
|
||||||
nodeId,
|
nodeId,
|
||||||
emptyPlaceholder = "Double Click to Edit Description",
|
emptyPlaceholder = "",
|
||||||
placeholderClassName,
|
placeholderClassName,
|
||||||
charLimit,
|
charLimit,
|
||||||
inputClassName,
|
inputClassName,
|
||||||
|
|
@ -69,11 +69,12 @@ export default function NodeDescription({
|
||||||
}, [description]);
|
}, [description]);
|
||||||
|
|
||||||
const MemoizedMarkdown = memo(Markdown);
|
const MemoizedMarkdown = memo(Markdown);
|
||||||
const renderedDescription = useMemo(
|
|
||||||
() =>
|
const renderedDescription = useMemo(() => {
|
||||||
description === "" || !description ? (
|
if (description === "" || !description) {
|
||||||
emptyPlaceholder
|
return emptyPlaceholder;
|
||||||
) : (
|
}
|
||||||
|
return (
|
||||||
<MemoizedMarkdown
|
<MemoizedMarkdown
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -83,9 +84,8 @@ export default function NodeDescription({
|
||||||
>
|
>
|
||||||
{String(description)}
|
{String(description)}
|
||||||
</MemoizedMarkdown>
|
</MemoizedMarkdown>
|
||||||
),
|
|
||||||
[description, emptyPlaceholder, mdClassName],
|
|
||||||
);
|
);
|
||||||
|
}, [description, emptyPlaceholder, mdClassName]);
|
||||||
|
|
||||||
const handleBlurFn = () => {
|
const handleBlurFn = () => {
|
||||||
setNodeDescription(nodeDescription);
|
setNodeDescription(nodeDescription);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
|
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 { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||||
import {
|
import {
|
||||||
CustomParameterComponent,
|
CustomParameterComponent,
|
||||||
CustomParameterLabel,
|
CustomParameterLabel,
|
||||||
getCustomParameterTitle,
|
getCustomParameterTitle,
|
||||||
} from "@/customization/components/custom-parameter";
|
} from "@/customization/components/custom-parameter";
|
||||||
|
import useAuthStore from "@/stores/authStore";
|
||||||
import { cn } from "@/utils/utils";
|
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 { default as IconComponent } from "../../../../components/common/genericIconComponent";
|
||||||
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
||||||
import {
|
import {
|
||||||
|
|
@ -44,6 +46,8 @@ export default function NodeInputField({
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const nodes = useFlowStore((state) => state.nodes);
|
const nodes = useFlowStore((state) => state.nodes);
|
||||||
const edges = useFlowStore((state) => state.edges);
|
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 myData = useTypesStore((state) => state.data);
|
||||||
const postTemplateValue = usePostTemplateValue({
|
const postTemplateValue = usePostTemplateValue({
|
||||||
node: data.node!,
|
node: data.node!,
|
||||||
|
|
@ -64,6 +68,16 @@ export default function NodeInputField({
|
||||||
name,
|
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);
|
useFetchDataOnMount(data.node!, handleNodeClass, name, postTemplateValue);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -193,6 +207,7 @@ export default function NodeInputField({
|
||||||
: data.node?.template[name].placeholder
|
: data.node?.template[name].placeholder
|
||||||
}
|
}
|
||||||
isToolMode={isToolMode}
|
isToolMode={isToolMode}
|
||||||
|
nodeInformationMetadata={nodeInformationMetadata}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,6 @@ const SnowflakeIcon = memo(() => (
|
||||||
<IconComponent className="h-5 w-5 text-ice" name="Snowflake" />
|
<IconComponent className="h-5 w-5 text-ice" name="Snowflake" />
|
||||||
));
|
));
|
||||||
|
|
||||||
const ScanEyeIcon = memo(({ className }: { className: string }) => (
|
|
||||||
<IconComponent
|
|
||||||
className={className}
|
|
||||||
name="ScanEye"
|
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
// Memoize Button components
|
// Memoize Button components
|
||||||
const HideShowButton = memo(
|
const HideShowButton = memo(
|
||||||
({
|
({
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,13 @@ function GenericNode({
|
||||||
const [hasChangedNodeDescription, setHasChangedNodeDescription] =
|
const [hasChangedNodeDescription, setHasChangedNodeDescription] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const editedNameDescription =
|
||||||
|
editNameDescription && hasChangedNodeDescription;
|
||||||
|
|
||||||
|
const hasDescription = useMemo(() => {
|
||||||
|
return data.node?.description && data.node?.description !== "";
|
||||||
|
}, [data.node?.description]);
|
||||||
|
|
||||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||||
return selected ? (
|
return selected ? (
|
||||||
<>
|
<>
|
||||||
|
|
@ -294,25 +301,21 @@ function GenericNode({
|
||||||
showNode
|
showNode
|
||||||
? "top-2 translate-x-[10.4rem]"
|
? "top-2 translate-x-[10.4rem]"
|
||||||
: "top-0 translate-x-[6.4rem]",
|
: "top-0 translate-x-[6.4rem]",
|
||||||
editNameDescription && hasChangedNodeDescription
|
editedNameDescription
|
||||||
? "bg-accent-emerald"
|
? "bg-accent-emerald"
|
||||||
: "bg-zinc-foreground",
|
: "bg-zinc-foreground",
|
||||||
)}
|
)}
|
||||||
data-testid={
|
data-testid={
|
||||||
editNameDescription && hasChangedNodeDescription
|
editedNameDescription
|
||||||
? "save-name-description-button"
|
? "save-name-description-button"
|
||||||
: "edit-name-description-button"
|
: "edit-name-description-button"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<ForwardedIconComponent
|
<ForwardedIconComponent
|
||||||
name={
|
name={editedNameDescription ? "Check" : "PencilLine"}
|
||||||
editNameDescription && hasChangedNodeDescription
|
|
||||||
? "Check"
|
|
||||||
: "PencilLine"
|
|
||||||
}
|
|
||||||
strokeWidth={ICON_STROKE_WIDTH}
|
strokeWidth={ICON_STROKE_WIDTH}
|
||||||
className={cn(
|
className={cn(
|
||||||
editNameDescription && hasChangedNodeDescription
|
editedNameDescription
|
||||||
? "text-accent-emerald-foreground"
|
? "text-accent-emerald-foreground"
|
||||||
: "text-muted-foreground",
|
: "text-muted-foreground",
|
||||||
"icon-size",
|
"icon-size",
|
||||||
|
|
@ -490,8 +493,9 @@ function GenericNode({
|
||||||
<div
|
<div
|
||||||
data-testid={`${data.id}-main-node`}
|
data-testid={`${data.id}-main-node`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"grid gap-3 text-wrap p-4 leading-5",
|
"grid truncate text-wrap p-4 leading-5",
|
||||||
showNode ? "border-b" : "relative",
|
showNode ? "border-b" : "relative",
|
||||||
|
hasDescription && "gap-3",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { GRADIENT_CLASS_DISABLED } from "@/constants/constants";
|
||||||
|
import useAlertStore from "@/stores/alertStore";
|
||||||
|
import useFlowStore from "@/stores/flowStore";
|
||||||
|
import { useMemo, useRef, useState } from "react";
|
||||||
|
import { cn } from "../../../../../utils/utils";
|
||||||
|
import IconComponent from "../../../../common/genericIconComponent";
|
||||||
|
import { Input } from "../../../../ui/input";
|
||||||
|
import { InputProps, TextAreaComponentType } from "../../types";
|
||||||
|
|
||||||
|
const BACKEND_URL = "BACKEND_URL";
|
||||||
|
const URL_WEBHOOK = `${window.location.protocol}//${window.location.host}/api/v1/webhook/`;
|
||||||
|
|
||||||
|
const inputClasses = {
|
||||||
|
base: ({ isFocused }: { isFocused: boolean }) =>
|
||||||
|
`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<string, TextAreaComponentType>): JSX.Element {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
|
||||||
|
handleOnNewValue({ value: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = (event?: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => setIsCopied(false), 2000);
|
||||||
|
navigator.clipboard.writeText(valueToRender);
|
||||||
|
|
||||||
|
setSuccessData({
|
||||||
|
title: "Endpoint URL copied",
|
||||||
|
});
|
||||||
|
|
||||||
|
event?.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderIcon = () => (
|
||||||
|
<>
|
||||||
|
{!isFocused && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
externalLinkIconClasses.gradient({
|
||||||
|
editNode,
|
||||||
|
disabled: false,
|
||||||
|
}),
|
||||||
|
editNode
|
||||||
|
? externalLinkIconClasses.editNodeTop
|
||||||
|
: externalLinkIconClasses.normalTop,
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
pointerEvents: "none",
|
||||||
|
background: isFocused ? undefined : GRADIENT_CLASS_DISABLED,
|
||||||
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div onClick={handleCopy}>
|
||||||
|
<IconComponent
|
||||||
|
dataTestId={`btn_copy_${id?.toLowerCase()}${editNode ? "_advanced" : ""}`}
|
||||||
|
name={isCopied ? "Check" : "Copy"}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer bg-muted",
|
||||||
|
externalLinkIconClasses.icon,
|
||||||
|
editNode
|
||||||
|
? externalLinkIconClasses.editNodeTop
|
||||||
|
: externalLinkIconClasses.iconTop,
|
||||||
|
"bg-muted text-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("w-full")}>
|
||||||
|
<Input
|
||||||
|
onFocus={() => setIsFocused(true)}
|
||||||
|
onBlur={() => setIsFocused(false)}
|
||||||
|
id={id}
|
||||||
|
data-testid={id}
|
||||||
|
value={valueToRender}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className={cn(getInputClassName())}
|
||||||
|
aria-label={valueToRender}
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<div className="relative w-full">{renderIcon()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { InputProps, StrRenderComponentType } from "../../types";
|
import { InputProps, StrRenderComponentType } from "../../types";
|
||||||
|
import CopyFieldAreaComponent from "../copyFieldAreaComponent";
|
||||||
import DropdownComponent from "../dropdownComponent";
|
import DropdownComponent from "../dropdownComponent";
|
||||||
import InputGlobalComponent from "../inputGlobalComponent";
|
import InputGlobalComponent from "../inputGlobalComponent";
|
||||||
import TextAreaComponent from "../textAreaComponent";
|
import TextAreaComponent from "../textAreaComponent";
|
||||||
|
import WebhookFieldComponent from "../webhookFieldComponent";
|
||||||
|
|
||||||
export function StrRenderComponent({
|
export function StrRenderComponent({
|
||||||
templateData,
|
templateData,
|
||||||
|
|
@ -10,13 +12,28 @@ export function StrRenderComponent({
|
||||||
placeholder,
|
placeholder,
|
||||||
...baseInputProps
|
...baseInputProps
|
||||||
}: InputProps<string, StrRenderComponentType>) {
|
}: InputProps<string, StrRenderComponentType>) {
|
||||||
const { handleOnNewValue, id, isToolMode } = baseInputProps;
|
const { handleOnNewValue, id, isToolMode, nodeInformationMetadata } =
|
||||||
|
baseInputProps;
|
||||||
|
|
||||||
if (!templateData.options) {
|
const noOptions = !templateData.options;
|
||||||
return templateData.multiline ? (
|
const isMultiline = templateData.multiline;
|
||||||
|
const copyField = templateData.copy_field;
|
||||||
|
const hasOptions = !!templateData.options;
|
||||||
|
const isWebhook = nodeInformationMetadata?.nodeType === "webhook";
|
||||||
|
|
||||||
|
if (noOptions) {
|
||||||
|
if (isMultiline) {
|
||||||
|
if (isWebhook) {
|
||||||
|
return <WebhookFieldComponent {...baseInputProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyField) {
|
||||||
|
return <CopyFieldAreaComponent {...baseInputProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<TextAreaComponent
|
<TextAreaComponent
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
// password={templateData.password}
|
|
||||||
updateVisibility={() => {
|
updateVisibility={() => {
|
||||||
if (templateData.password !== undefined) {
|
if (templateData.password !== undefined) {
|
||||||
handleOnNewValue(
|
handleOnNewValue(
|
||||||
|
|
@ -28,7 +45,10 @@ export function StrRenderComponent({
|
||||||
id={`textarea_${id}`}
|
id={`textarea_${id}`}
|
||||||
isToolMode={isToolMode}
|
isToolMode={isToolMode}
|
||||||
/>
|
/>
|
||||||
) : (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<InputGlobalComponent
|
<InputGlobalComponent
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
password={templateData.password}
|
password={templateData.password}
|
||||||
|
|
@ -41,7 +61,7 @@ export function StrRenderComponent({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!templateData.options) {
|
if (hasOptions) {
|
||||||
return (
|
return (
|
||||||
<DropdownComponent
|
<DropdownComponent
|
||||||
{...baseInputProps}
|
{...baseInputProps}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
|
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
|
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
|
||||||
import { ParameterRenderComponent } from "@/components/core/parameterRenderComponent";
|
import { ParameterRenderComponent } from "@/components/core/parameterRenderComponent";
|
||||||
|
import { NodeInfoType } from "@/components/core/parameterRenderComponent/types";
|
||||||
|
import useAuthStore from "@/stores/authStore";
|
||||||
import useFlowStore from "@/stores/flowStore";
|
import useFlowStore from "@/stores/flowStore";
|
||||||
import { useTweaksStore } from "@/stores/tweaksStore";
|
import { useTweaksStore } from "@/stores/tweaksStore";
|
||||||
import { APIClassType } from "@/types/api";
|
import { APIClassType } from "@/types/api";
|
||||||
import { isTargetHandleConnected } from "@/utils/reactflowUtils";
|
import { isTargetHandleConnected } from "@/utils/reactflowUtils";
|
||||||
import { CustomCellRendererProps } from "ag-grid-react";
|
import { CustomCellRendererProps } from "ag-grid-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
export default function TableNodeCellRender({
|
export default function TableNodeCellRender({
|
||||||
value: { nodeId, parameterId, isTweaks },
|
value: { nodeId, parameterId, isTweaks },
|
||||||
|
|
@ -15,6 +18,8 @@ export default function TableNodeCellRender({
|
||||||
? useTweaksStore((state) => state.getNode(nodeId))
|
? useTweaksStore((state) => state.getNode(nodeId))
|
||||||
: useFlowStore((state) => state.getNode(nodeId));
|
: useFlowStore((state) => state.getNode(nodeId));
|
||||||
const parameter = node?.data?.node?.template?.[parameterId];
|
const parameter = node?.data?.node?.template?.[parameterId];
|
||||||
|
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||||
|
const isAuth = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
|
||||||
const setNode = useTweaksStore((state) => state.setNode);
|
const setNode = useTweaksStore((state) => state.setNode);
|
||||||
|
|
||||||
|
|
@ -37,6 +42,16 @@ export default function TableNodeCellRender({
|
||||||
isTweaks ? setNode : undefined,
|
isTweaks ? setNode : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nodeInformationMetadata: NodeInfoType = useMemo(() => {
|
||||||
|
return {
|
||||||
|
flowId: currentFlow?.id ?? "",
|
||||||
|
nodeType: node?.data?.type?.toLowerCase() ?? "",
|
||||||
|
flowName: currentFlow?.name ?? "",
|
||||||
|
isAuth,
|
||||||
|
variableName: parameterId,
|
||||||
|
};
|
||||||
|
}, [nodeId, isAuth, parameterId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
parameter && (
|
parameter && (
|
||||||
<div className="group mx-auto flex h-full max-h-48 w-[300px] items-center justify-center overflow-auto px-1 py-2.5 custom-scroll">
|
<div className="group mx-auto flex h-full max-h-48 w-[300px] items-center justify-center overflow-auto px-1 py-2.5 custom-scroll">
|
||||||
|
|
@ -50,6 +65,7 @@ export default function TableNodeCellRender({
|
||||||
handleNodeClass={handleNodeClass}
|
handleNodeClass={handleNodeClass}
|
||||||
nodeClass={node?.data.node}
|
nodeClass={node?.data.node}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
nodeInformationMetadata={nodeInformationMetadata}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { GRADIENT_CLASS } from "@/constants/constants";
|
import { GRADIENT_CLASS } from "@/constants/constants";
|
||||||
|
import { getCurlWebhookCode } from "@/modals/apiModal/utils/get-curl-code";
|
||||||
import ComponentTextModal from "@/modals/textAreaModal";
|
import ComponentTextModal from "@/modals/textAreaModal";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { cn } from "../../../../../utils/utils";
|
import { cn } from "../../../../../utils/utils";
|
||||||
import IconComponent from "../../../../common/genericIconComponent";
|
import IconComponent from "../../../../common/genericIconComponent";
|
||||||
import { Input } from "../../../../ui/input";
|
import { Input } from "../../../../ui/input";
|
||||||
|
|
@ -18,6 +19,8 @@ const inputClasses = {
|
||||||
password: "password",
|
password: "password",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WEBHOOK_VALUE = "CURL_WEBHOOK";
|
||||||
|
|
||||||
const externalLinkIconClasses = {
|
const externalLinkIconClasses = {
|
||||||
gradient: ({
|
gradient: ({
|
||||||
disabled,
|
disabled,
|
||||||
|
|
@ -61,12 +64,29 @@ export default function TextAreaComponent({
|
||||||
password,
|
password,
|
||||||
placeholder,
|
placeholder,
|
||||||
isToolMode = false,
|
isToolMode = false,
|
||||||
|
nodeInformationMetadata,
|
||||||
}: InputProps<string, TextAreaComponentType>): JSX.Element {
|
}: InputProps<string, TextAreaComponentType>): JSX.Element {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
const [passwordVisible, setPasswordVisible] = useState(false);
|
const [passwordVisible, setPasswordVisible] = useState(false);
|
||||||
|
|
||||||
|
const isWebhook = useMemo(
|
||||||
|
() => nodeInformationMetadata?.nodeType === "webhook",
|
||||||
|
[nodeInformationMetadata?.nodeType],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isWebhook && value === WEBHOOK_VALUE) {
|
||||||
|
const curlWebhookCode = getCurlWebhookCode({
|
||||||
|
flowId: nodeInformationMetadata?.flowId!,
|
||||||
|
isAuth: nodeInformationMetadata?.isAuth!,
|
||||||
|
flowName: nodeInformationMetadata?.flowName!,
|
||||||
|
format: "singleline",
|
||||||
|
});
|
||||||
|
handleOnNewValue({ value: curlWebhookCode });
|
||||||
|
}
|
||||||
|
}, [isWebhook]);
|
||||||
|
|
||||||
const getInputClassName = () => {
|
const getInputClassName = () => {
|
||||||
return cn(
|
return cn(
|
||||||
inputClasses.base({ isFocused, password: password! }),
|
inputClasses.base({ isFocused, password: password! }),
|
||||||
|
|
@ -81,9 +101,21 @@ export default function TextAreaComponent({
|
||||||
handleOnNewValue({ value: e.target.value });
|
handleOnNewValue({ value: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changeWebhookFormat = (format: "multiline" | "singleline") => {
|
||||||
|
if (isWebhook) {
|
||||||
|
const curlWebhookCode = getCurlWebhookCode({
|
||||||
|
flowId: nodeInformationMetadata?.flowId!,
|
||||||
|
isAuth: nodeInformationMetadata?.isAuth!,
|
||||||
|
flowName: nodeInformationMetadata?.flowName!,
|
||||||
|
format,
|
||||||
|
});
|
||||||
|
handleOnNewValue({ value: curlWebhookCode });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderIcon = () => (
|
const renderIcon = () => (
|
||||||
<div>
|
<div>
|
||||||
{!disabled && (
|
{!disabled && !isFocused && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
externalLinkIconClasses.gradient({
|
externalLinkIconClasses.gradient({
|
||||||
|
|
@ -139,6 +171,7 @@ export default function TextAreaComponent({
|
||||||
aria-label={disabled ? value : undefined}
|
aria-label={disabled ? value : undefined}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type={password ? (passwordVisible ? "text" : "password") : "text"}
|
type={password ? (passwordVisible ? "text" : "password") : "text"}
|
||||||
|
readOnly={isWebhook}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ComponentTextModal
|
<ComponentTextModal
|
||||||
|
|
@ -146,8 +179,14 @@ export default function TextAreaComponent({
|
||||||
value={value}
|
value={value}
|
||||||
setValue={(newValue) => handleOnNewValue({ value: newValue })}
|
setValue={(newValue) => handleOnNewValue({ value: newValue })}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
onCloseModal={() => changeWebhookFormat("singleline")}
|
||||||
>
|
>
|
||||||
<div className="relative w-full">{renderIcon()}</div>
|
<div
|
||||||
|
onClick={() => changeWebhookFormat("multiline")}
|
||||||
|
className="relative w-full"
|
||||||
|
>
|
||||||
|
{renderIcon()}
|
||||||
|
</div>
|
||||||
</ComponentTextModal>
|
</ComponentTextModal>
|
||||||
{password && !isFocused && (
|
{password && !isFocused && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { AuthContext } from "@/contexts/authContext";
|
||||||
|
import { useGetBuildsMutation } from "@/controllers/API/queries/_builds/use-get-builds-polling-mutation";
|
||||||
|
import SecretKeyModalButton from "@/customization/components/custom-secret-key-modal-button";
|
||||||
|
import useFlowStore from "@/stores/flowStore";
|
||||||
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
|
import { InputProps, TextAreaComponentType } from "../../types";
|
||||||
|
import CopyFieldAreaComponent from "../copyFieldAreaComponent";
|
||||||
|
import TextAreaComponent from "../textAreaComponent";
|
||||||
|
|
||||||
|
export default function WebhookFieldComponent({
|
||||||
|
value,
|
||||||
|
handleOnNewValue,
|
||||||
|
editNode = false,
|
||||||
|
id = "",
|
||||||
|
nodeInformationMetadata,
|
||||||
|
...baseInputProps
|
||||||
|
}: InputProps<string, TextAreaComponentType>): JSX.Element {
|
||||||
|
const { userData } = useContext(AuthContext);
|
||||||
|
const [userId, setUserId] = useState("");
|
||||||
|
const { mutate: getBuildsMutation } = useGetBuildsMutation();
|
||||||
|
const hasInitialized = useRef(false);
|
||||||
|
|
||||||
|
const isBackendUrl = nodeInformationMetadata?.variableName === "endpoint";
|
||||||
|
const isCurlWebhook = nodeInformationMetadata?.variableName === "curl";
|
||||||
|
const isAuth = nodeInformationMetadata?.isAuth;
|
||||||
|
const showGenerateToken = isBackendUrl && !editNode && !isAuth;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editNode && isBackendUrl && !hasInitialized.current) {
|
||||||
|
hasInitialized.current = true;
|
||||||
|
getBuildsMutation({
|
||||||
|
flowId: nodeInformationMetadata?.flowId!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (userData) {
|
||||||
|
setUserId(userData.id);
|
||||||
|
}
|
||||||
|
}, [userData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid w-full gap-2">
|
||||||
|
{isBackendUrl && (
|
||||||
|
<div>
|
||||||
|
<CopyFieldAreaComponent
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
editNode={editNode}
|
||||||
|
handleOnNewValue={handleOnNewValue}
|
||||||
|
{...baseInputProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isCurlWebhook && (
|
||||||
|
<div>
|
||||||
|
<TextAreaComponent
|
||||||
|
id={id}
|
||||||
|
value={value}
|
||||||
|
editNode={editNode}
|
||||||
|
handleOnNewValue={handleOnNewValue}
|
||||||
|
{...baseInputProps}
|
||||||
|
nodeInformationMetadata={nodeInformationMetadata}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showGenerateToken && (
|
||||||
|
<div>
|
||||||
|
<SecretKeyModalButton userId={userId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ import PromptAreaComponent from "./components/promptComponent";
|
||||||
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
import { RefreshParameterComponent } from "./components/refreshParameterComponent";
|
||||||
import { StrRenderComponent } from "./components/strRenderComponent";
|
import { StrRenderComponent } from "./components/strRenderComponent";
|
||||||
import ToggleShadComponent from "./components/toggleShadComponent";
|
import ToggleShadComponent from "./components/toggleShadComponent";
|
||||||
import { InputProps } from "./types";
|
import { InputProps, NodeInfoType } from "./types";
|
||||||
|
|
||||||
export function ParameterRenderComponent({
|
export function ParameterRenderComponent({
|
||||||
handleOnNewValue,
|
handleOnNewValue,
|
||||||
|
|
@ -32,6 +32,7 @@ export function ParameterRenderComponent({
|
||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
isToolMode,
|
isToolMode,
|
||||||
|
nodeInformationMetadata,
|
||||||
}: {
|
}: {
|
||||||
handleOnNewValue:
|
handleOnNewValue:
|
||||||
| handleOnNewValueType
|
| handleOnNewValueType
|
||||||
|
|
@ -46,6 +47,7 @@ export function ParameterRenderComponent({
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isToolMode?: boolean;
|
isToolMode?: boolean;
|
||||||
|
nodeInformationMetadata?: NodeInfoType;
|
||||||
}) {
|
}) {
|
||||||
const id = (
|
const id = (
|
||||||
templateData.type +
|
templateData.type +
|
||||||
|
|
@ -67,6 +69,7 @@ export function ParameterRenderComponent({
|
||||||
placeholder,
|
placeholder,
|
||||||
isToolMode,
|
isToolMode,
|
||||||
nodeId,
|
nodeId,
|
||||||
|
nodeInformationMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (TEXT_FIELD_TYPES.includes(templateData.type ?? "")) {
|
if (TEXT_FIELD_TYPES.includes(templateData.type ?? "")) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export type BaseInputProps<valueType = any> = {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
metadata?: any;
|
metadata?: any;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
|
nodeInformationMetadata?: NodeInfoType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generic type for composing input props
|
// Generic type for composing input props
|
||||||
|
|
@ -101,3 +102,11 @@ export type MultiselectComponentType = {
|
||||||
options: string[];
|
options: string[];
|
||||||
combobox?: boolean;
|
combobox?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NodeInfoType = {
|
||||||
|
flowId: string;
|
||||||
|
nodeType: string;
|
||||||
|
flowName: string;
|
||||||
|
isAuth: boolean;
|
||||||
|
variableName: string;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -998,6 +998,9 @@ export const STORE_PAGINATION_ROWS_COUNT = [12, 24, 48, 96];
|
||||||
export const GRADIENT_CLASS =
|
export const GRADIENT_CLASS =
|
||||||
"linear-gradient(to right, hsl(var(--background) / 0.3), hsl(var(--background)))";
|
"linear-gradient(to right, hsl(var(--background) / 0.3), hsl(var(--background)))";
|
||||||
|
|
||||||
|
export const GRADIENT_CLASS_DISABLED =
|
||||||
|
"linear-gradient(to right, hsl(var(--muted) / 0.3), hsl(var(--muted)))";
|
||||||
|
|
||||||
export const RECEIVING_INPUT_VALUE = "Receiving input";
|
export const RECEIVING_INPUT_VALUE = "Receiving input";
|
||||||
|
|
||||||
export const ICON_STROKE_WIDTH = 1.5;
|
export const ICON_STROKE_WIDTH = 1.5;
|
||||||
|
|
|
||||||
11
src/frontend/src/controllers/API/helpers/validate-webhook.ts
Normal file
11
src/frontend/src/controllers/API/helpers/validate-webhook.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const validateWebhookData = (res) => {
|
||||||
|
if (!res?.data?.vertex_builds) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object.keys(res?.data?.vertex_builds).some(
|
||||||
|
(key) =>
|
||||||
|
key.includes("Webhook") &&
|
||||||
|
Array.isArray(res?.data?.vertex_builds[key]) &&
|
||||||
|
res?.data?.vertex_builds[key]?.length > 0,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,189 @@
|
||||||
|
import useFlowStore from "@/stores/flowStore";
|
||||||
|
import { useUtilityStore } from "@/stores/utilityStore";
|
||||||
|
import { useMutationFunctionType } from "@/types/api";
|
||||||
|
import { FlowPoolType } from "@/types/zustand/flow";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { api } from "../../api";
|
||||||
|
import { getURL } from "../../helpers/constants";
|
||||||
|
import { UseRequestProcessor } from "../../services/request-processor";
|
||||||
|
|
||||||
|
interface PollingItem {
|
||||||
|
interval: NodeJS.Timeout;
|
||||||
|
timestamp: number;
|
||||||
|
flowId: string;
|
||||||
|
callback: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PollingManager = {
|
||||||
|
pollingQueue: new Map<string, PollingItem[]>(),
|
||||||
|
activePolls: new Map<string, PollingItem>(),
|
||||||
|
|
||||||
|
enqueuePolling(flowId: string, pollingItem: PollingItem) {
|
||||||
|
if (!this.pollingQueue.has(flowId)) {
|
||||||
|
this.pollingQueue.set(flowId, []);
|
||||||
|
}
|
||||||
|
this.pollingQueue.set(
|
||||||
|
flowId,
|
||||||
|
(this.pollingQueue.get(flowId) || []).filter(
|
||||||
|
(item) => item.timestamp !== pollingItem.timestamp,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.pollingQueue.get(flowId)?.push(pollingItem);
|
||||||
|
|
||||||
|
if (!this.activePolls.has(flowId)) {
|
||||||
|
this.startNextPolling(flowId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startNextPolling(flowId: string) {
|
||||||
|
const queue = this.pollingQueue.get(flowId) || [];
|
||||||
|
if (queue.length === 0) {
|
||||||
|
this.activePolls.delete(flowId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPoll = queue[0];
|
||||||
|
this.activePolls.set(flowId, nextPoll);
|
||||||
|
nextPoll.callback();
|
||||||
|
},
|
||||||
|
|
||||||
|
stopPoll(flowId: string) {
|
||||||
|
const activePoll = this.activePolls.get(flowId);
|
||||||
|
if (activePoll) {
|
||||||
|
clearInterval(activePoll.interval);
|
||||||
|
this.activePolls.delete(flowId);
|
||||||
|
const queue = this.pollingQueue.get(flowId) || [];
|
||||||
|
this.pollingQueue.set(
|
||||||
|
flowId,
|
||||||
|
queue.filter((item) => item.timestamp !== activePoll.timestamp),
|
||||||
|
);
|
||||||
|
this.startNextPolling(flowId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stopAll() {
|
||||||
|
this.activePolls.forEach((poll) => clearInterval(poll.interval));
|
||||||
|
this.activePolls.clear();
|
||||||
|
this.pollingQueue.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFromQueue(flowId: string, timestamp: number) {
|
||||||
|
const queue = this.pollingQueue.get(flowId) || [];
|
||||||
|
this.pollingQueue.set(
|
||||||
|
flowId,
|
||||||
|
queue.filter((item) => item.timestamp !== timestamp),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IGetBuilds {
|
||||||
|
flowId: string;
|
||||||
|
onSuccess?: (data: { vertex_builds: FlowPoolType }) => void;
|
||||||
|
stopPollingOn?: (data: { vertex_builds: FlowPoolType }) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetBuildsMutation: useMutationFunctionType<
|
||||||
|
undefined,
|
||||||
|
IGetBuilds
|
||||||
|
> = (options?) => {
|
||||||
|
const { mutate } = UseRequestProcessor();
|
||||||
|
const webhookPollingInterval = useUtilityStore(
|
||||||
|
(state) => state.webhookPollingInterval,
|
||||||
|
);
|
||||||
|
|
||||||
|
const setFlowPool = useFlowStore((state) => state.setFlowPool);
|
||||||
|
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||||
|
|
||||||
|
const flowIdRef = useRef<string | null>(null);
|
||||||
|
const requestInProgressRef = useRef<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const getBuildsFn = async (
|
||||||
|
payload: IGetBuilds,
|
||||||
|
): Promise<{ vertex_builds: FlowPoolType }> => {
|
||||||
|
if (requestInProgressRef.current[payload.flowId]) {
|
||||||
|
return Promise.reject("Request already in progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestInProgressRef.current[payload.flowId] = true;
|
||||||
|
const config = {};
|
||||||
|
config["params"] = { flow_id: payload.flowId };
|
||||||
|
const res = await api.get<any>(`${getURL("BUILDS")}`, config);
|
||||||
|
|
||||||
|
if (currentFlow) {
|
||||||
|
const flowPool = res?.data?.vertex_builds;
|
||||||
|
setFlowPool(flowPool);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
} finally {
|
||||||
|
requestInProgressRef.current[payload.flowId] = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPolling = (payload: IGetBuilds) => {
|
||||||
|
if (requestInProgressRef.current[payload.flowId]) {
|
||||||
|
return Promise.reject("Request already in progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!webhookPollingInterval || webhookPollingInterval === 0) {
|
||||||
|
return getBuildsFn(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
flowIdRef.current === payload.flowId &&
|
||||||
|
PollingManager.activePolls.has(payload.flowId)
|
||||||
|
) {
|
||||||
|
return Promise.resolve({ vertex_builds: {} as FlowPoolType });
|
||||||
|
}
|
||||||
|
|
||||||
|
flowIdRef.current = payload.flowId;
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const pollCallback = async () => {
|
||||||
|
const data = await getBuildsFn(payload);
|
||||||
|
payload.onSuccess?.(data);
|
||||||
|
|
||||||
|
if (payload.stopPollingOn?.(data)) {
|
||||||
|
PollingManager.stopPoll(payload.flowId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const intervalId = setInterval(pollCallback, webhookPollingInterval);
|
||||||
|
|
||||||
|
const pollingItem: PollingItem = {
|
||||||
|
interval: intervalId,
|
||||||
|
timestamp,
|
||||||
|
flowId: payload.flowId,
|
||||||
|
callback: pollCallback,
|
||||||
|
};
|
||||||
|
|
||||||
|
PollingManager.enqueuePolling(payload.flowId, pollingItem);
|
||||||
|
|
||||||
|
return getBuildsFn(payload).then((data) => {
|
||||||
|
payload.onSuccess?.(data);
|
||||||
|
if (payload.stopPollingOn?.(data)) {
|
||||||
|
PollingManager.stopPoll(payload.flowId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (flowIdRef.current) {
|
||||||
|
PollingManager.stopPoll(flowIdRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const mutation = mutate(
|
||||||
|
["useGetBuildsMutation"],
|
||||||
|
(payload: IGetBuilds) =>
|
||||||
|
startPolling(payload) ?? Promise.reject("Failed to start polling"),
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return mutation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { PollingManager };
|
||||||
|
|
@ -26,6 +26,7 @@ export const useGetApiKeysQuery: useQueryFunctionType<
|
||||||
> = (options) => {
|
> = (options) => {
|
||||||
const { query } = UseRequestProcessor();
|
const { query } = UseRequestProcessor();
|
||||||
|
|
||||||
|
//@TODO: Request API key from DSLF endpoint
|
||||||
const getApiKeysFn = async () => {
|
const getApiKeysFn = async () => {
|
||||||
return await api.get<IApiQueryResponse>(`${getURL("API_KEY")}/`);
|
return await api.get<IApiQueryResponse>(`${getURL("API_KEY")}/`);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export interface ConfigResponse {
|
||||||
health_check_max_retries: number;
|
health_check_max_retries: number;
|
||||||
max_file_size_upload: number;
|
max_file_size_upload: number;
|
||||||
feature_flags: Record<string, any>;
|
feature_flags: Record<string, any>;
|
||||||
|
webhook_polling_interval: number;
|
||||||
event_delivery: EventDeliveryType;
|
event_delivery: EventDeliveryType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,6 +32,9 @@ export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (
|
||||||
(state) => state.setMaxFileSizeUpload,
|
(state) => state.setMaxFileSizeUpload,
|
||||||
);
|
);
|
||||||
const setFeatureFlags = useUtilityStore((state) => state.setFeatureFlags);
|
const setFeatureFlags = useUtilityStore((state) => state.setFeatureFlags);
|
||||||
|
const setWebhookPollingInterval = useUtilityStore(
|
||||||
|
(state) => state.setWebhookPollingInterval,
|
||||||
|
);
|
||||||
|
|
||||||
const { query } = UseRequestProcessor();
|
const { query } = UseRequestProcessor();
|
||||||
|
|
||||||
|
|
@ -48,6 +52,7 @@ export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (
|
||||||
setHealthCheckMaxRetries(data.health_check_max_retries);
|
setHealthCheckMaxRetries(data.health_check_max_retries);
|
||||||
setMaxFileSizeUpload(data.max_file_size_upload);
|
setMaxFileSizeUpload(data.max_file_size_upload);
|
||||||
setFeatureFlags(data.feature_flags);
|
setFeatureFlags(data.feature_flags);
|
||||||
|
setWebhookPollingInterval(data.webhook_polling_interval);
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { ParameterRenderComponent } from "@/components/core/parameterRenderComponent";
|
import { ParameterRenderComponent } from "@/components/core/parameterRenderComponent";
|
||||||
|
import { NodeInfoType } from "@/components/core/parameterRenderComponent/types";
|
||||||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||||
import { APIClassType, InputFieldType } from "@/types/api";
|
import { APIClassType, InputFieldType } from "@/types/api";
|
||||||
import { cn } from "@/utils/utils";
|
import { cn } from "@/utils/utils";
|
||||||
|
|
@ -15,6 +16,7 @@ export function CustomParameterComponent({
|
||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
isToolMode,
|
isToolMode,
|
||||||
|
nodeInformationMetadata,
|
||||||
}: {
|
}: {
|
||||||
handleOnNewValue: handleOnNewValueType;
|
handleOnNewValue: handleOnNewValueType;
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -27,6 +29,7 @@ export function CustomParameterComponent({
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
isToolMode?: boolean;
|
isToolMode?: boolean;
|
||||||
|
nodeInformationMetadata?: NodeInfoType;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<ParameterRenderComponent
|
<ParameterRenderComponent
|
||||||
|
|
@ -41,6 +44,7 @@ export function CustomParameterComponent({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
isToolMode={isToolMode}
|
isToolMode={isToolMode}
|
||||||
|
nodeInformationMetadata={nodeInformationMetadata}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { getModalPropsApiKey } from "@/pages/SettingsPage/pages/ApiKeysPage/helpers/get-modal-props";
|
||||||
|
|
||||||
|
export const SecretKeyModalButton = ({
|
||||||
|
userId,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
}): JSX.Element => {
|
||||||
|
const modalProps = getModalPropsApiKey();
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretKeyModalButton;
|
||||||
1
src/frontend/src/customization/utils/get-modal-props.tsx
Normal file
1
src/frontend/src/customization/utils/get-modal-props.tsx
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const getModalPropsApiKey = () => {};
|
||||||
|
|
@ -51,14 +51,20 @@ export function getCurlWebhookCode({
|
||||||
flowId,
|
flowId,
|
||||||
isAuth,
|
isAuth,
|
||||||
endpointName,
|
endpointName,
|
||||||
}: GetCodeType) {
|
format = "multiline",
|
||||||
|
}: GetCodeType & { format?: "multiline" | "singleline" }) {
|
||||||
|
const baseUrl = `${window.location.protocol}//${window.location.host}/api/v1/webhook/${endpointName || flowId}`;
|
||||||
|
const authHeader = !isAuth ? `-H 'x-api-key: <your api key>'` : "";
|
||||||
|
|
||||||
|
if (format === "singleline") {
|
||||||
|
return `curl -X POST "${baseUrl}" -H 'Content-Type: application/json' ${authHeader} -d '{"any": "data"}'`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
return `curl -X POST \\
|
return `curl -X POST \\
|
||||||
"${window.location.protocol}//${window.location.host}/api/v1/webhook/${
|
"${baseUrl}" \\
|
||||||
endpointName || flowId
|
|
||||||
}" \\
|
|
||||||
-H 'Content-Type: application/json'\\${
|
-H 'Content-Type: application/json'\\${
|
||||||
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
|
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
|
||||||
}
|
}
|
||||||
-d '{"any": "data"}'
|
-d '{"any": "data"}'
|
||||||
`;
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,20 @@ const Trigger: React.FC<TriggerProps> = ({
|
||||||
const Header: React.FC<{
|
const Header: React.FC<{
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
description: string | JSX.Element | null;
|
description: string | JSX.Element | null;
|
||||||
}> = ({ children, description }: modalHeaderType): JSX.Element => {
|
clampDescription?: number;
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
description,
|
||||||
|
clampDescription,
|
||||||
|
}: modalHeaderType): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="line-clamp-1 flex items-center pb-0.5 text-base">
|
<DialogTitle className="line-clamp-1 flex items-center pb-0.5 text-base">
|
||||||
{children}
|
{children}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription className="line-clamp-2 text-sm">
|
<DialogDescription
|
||||||
|
className={`line-clamp-${clampDescription ?? 2} text-sm`}
|
||||||
|
>
|
||||||
{description}
|
{description}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import IconComponent from "../../../components/common/genericIconComponent";
|
||||||
|
|
||||||
|
export const ContentRenderKey = ({
|
||||||
|
inputLabel,
|
||||||
|
inputRef,
|
||||||
|
apiKeyValue,
|
||||||
|
handleCopyClick,
|
||||||
|
textCopied,
|
||||||
|
renderKey,
|
||||||
|
}: {
|
||||||
|
inputLabel: string;
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
apiKeyValue: string;
|
||||||
|
handleCopyClick: () => void;
|
||||||
|
textCopied: boolean;
|
||||||
|
renderKey: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-full">
|
||||||
|
{inputLabel && !renderKey && (
|
||||||
|
<div className="relative bottom-1">
|
||||||
|
{inputLabel as React.ReactNode}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
data-testid="api-key-input"
|
||||||
|
ref={inputRef}
|
||||||
|
readOnly={true}
|
||||||
|
value={apiKeyValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={(e) => {
|
||||||
|
handleCopyClick();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
data-testid="btn-copy-api-key"
|
||||||
|
unstyled
|
||||||
|
>
|
||||||
|
{textCopied ? (
|
||||||
|
<IconComponent name="Copy" className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<IconComponent name="Check" className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import * as Form from "@radix-ui/react-form";
|
||||||
|
|
||||||
|
export const FormKeyRender = ({
|
||||||
|
modalProps,
|
||||||
|
apiKeyName,
|
||||||
|
inputRef,
|
||||||
|
setApiKeyName,
|
||||||
|
}: {
|
||||||
|
modalProps: any;
|
||||||
|
apiKeyName: string;
|
||||||
|
inputRef: React.RefObject<HTMLInputElement>;
|
||||||
|
setApiKeyName: (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Form.Field name="apikey">
|
||||||
|
{modalProps?.inputLabel && (
|
||||||
|
<Form.Label asChild className="mb-2">
|
||||||
|
<Label className="relative bottom-1">
|
||||||
|
{modalProps?.inputLabel as React.ReactNode}
|
||||||
|
</Label>
|
||||||
|
</Form.Label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Form.Control asChild>
|
||||||
|
<Input
|
||||||
|
id="primary-input"
|
||||||
|
value={apiKeyName}
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={({ target: { value } }) => {
|
||||||
|
setApiKeyName(value);
|
||||||
|
}}
|
||||||
|
placeholder={modalProps?.inputPlaceholder}
|
||||||
|
/>
|
||||||
|
</Form.Control>
|
||||||
|
</div>
|
||||||
|
</Form.Field>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import IconComponent from "../../../components/common/genericIconComponent";
|
||||||
|
|
||||||
|
export const HeaderRender = ({ title, showIcon }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="pr-2">{title}</span>
|
||||||
|
{showIcon && (
|
||||||
|
<IconComponent
|
||||||
|
name="Key"
|
||||||
|
className="h-6 w-6 pl-1 text-foreground"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,19 +1,27 @@
|
||||||
import * as Form from "@radix-ui/react-form";
|
import * as Form from "@radix-ui/react-form";
|
||||||
|
import { Label } from "@radix-ui/react-form";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import IconComponent from "../../components/common/genericIconComponent";
|
|
||||||
import { Button } from "../../components/ui/button";
|
|
||||||
import { Input } from "../../components/ui/input";
|
import { Input } from "../../components/ui/input";
|
||||||
import { COPIED_NOTICE_ALERT } from "../../constants/alerts_constants";
|
import { COPIED_NOTICE_ALERT } from "../../constants/alerts_constants";
|
||||||
import { createApiKey } from "../../controllers/API";
|
import { createApiKey } from "../../controllers/API";
|
||||||
import useAlertStore from "../../stores/alertStore";
|
import useAlertStore from "../../stores/alertStore";
|
||||||
import { ApiKeyType } from "../../types/components";
|
import { ApiKeyType } from "../../types/components";
|
||||||
import BaseModal from "../baseModal";
|
import BaseModal from "../baseModal";
|
||||||
|
import { ContentRenderKey } from "./components/content-render";
|
||||||
|
import { FormKeyRender } from "./components/form-key-render";
|
||||||
|
import { HeaderRender } from "./components/header-render";
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
generatedKeyMessage?: React.ReactNode;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SecretKeyModal({
|
export default function SecretKeyModal({
|
||||||
children,
|
children,
|
||||||
data,
|
data,
|
||||||
onCloseModal,
|
onCloseModal,
|
||||||
}: ApiKeyType) {
|
modalProps,
|
||||||
|
}: ApiKeyType & { modalProps?: ModalProps }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
|
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
|
||||||
const [apiKeyValue, setApiKeyValue] = useState("");
|
const [apiKeyValue, setApiKeyValue] = useState("");
|
||||||
|
|
@ -27,7 +35,7 @@ export default function SecretKeyModal({
|
||||||
setRenderKey(false);
|
setRenderKey(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
} else {
|
} else {
|
||||||
onCloseModal();
|
onCloseModal?.();
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
|
@ -72,78 +80,47 @@ export default function SecretKeyModal({
|
||||||
return (
|
return (
|
||||||
<BaseModal
|
<BaseModal
|
||||||
onSubmit={handleSubmitForm}
|
onSubmit={handleSubmitForm}
|
||||||
size="small-h-full"
|
size={modalProps?.size ?? "small-h-full"}
|
||||||
open={open}
|
open={open}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
>
|
>
|
||||||
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||||
<BaseModal.Header
|
<BaseModal.Header
|
||||||
|
clampDescription={3}
|
||||||
description={
|
description={
|
||||||
renderKey ? (
|
renderKey ? (
|
||||||
<>
|
<>{modalProps?.generatedKeyMessage}</>
|
||||||
{" "}
|
|
||||||
Please save this secret key somewhere safe and accessible. For
|
|
||||||
security reasons,{" "}
|
|
||||||
<strong>you won't be able to view it again</strong> through your
|
|
||||||
account. If you lose this secret key, you'll need to generate a
|
|
||||||
new one.
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<>Create a secret API Key to use Langflow API.</>
|
<>{modalProps?.description}</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span className="pr-2">Create API Key</span>
|
<HeaderRender
|
||||||
<IconComponent
|
title={modalProps?.title}
|
||||||
name="Key"
|
showIcon={modalProps?.showIcon}
|
||||||
className="h-6 w-6 pl-1 text-foreground"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
/>
|
||||||
</BaseModal.Header>
|
</BaseModal.Header>
|
||||||
<BaseModal.Content>
|
<BaseModal.Content>
|
||||||
{renderKey ? (
|
{renderKey ? (
|
||||||
<>
|
<ContentRenderKey
|
||||||
<div className="flex items-center gap-3">
|
inputLabel={String(modalProps?.inputLabel ?? "")}
|
||||||
<div className="w-full">
|
inputRef={inputRef}
|
||||||
<Input ref={inputRef} readOnly={true} value={apiKeyValue} />
|
apiKeyValue={apiKeyValue}
|
||||||
</div>
|
handleCopyClick={handleCopyClick}
|
||||||
|
textCopied={textCopied}
|
||||||
<Button
|
renderKey={renderKey}
|
||||||
onClick={() => {
|
/>
|
||||||
handleCopyClick();
|
) : (
|
||||||
}}
|
<FormKeyRender
|
||||||
data-testid="btn-copy-api-key"
|
modalProps={modalProps}
|
||||||
unstyled
|
apiKeyName={apiKeyName}
|
||||||
>
|
inputRef={inputRef}
|
||||||
{textCopied ? (
|
setApiKeyName={setApiKeyName}
|
||||||
<IconComponent name="Copy" className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<IconComponent name="Check" className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Form.Field name="apikey">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<Form.Control asChild>
|
|
||||||
<Input
|
|
||||||
//fake api key
|
|
||||||
id="primary-input"
|
|
||||||
value={apiKeyName}
|
|
||||||
ref={inputRef}
|
|
||||||
onChange={({ target: { value } }) => {
|
|
||||||
setApiKeyName(value);
|
|
||||||
}}
|
|
||||||
placeholder="Insert a name for your API Key"
|
|
||||||
/>
|
/>
|
||||||
</Form.Control>
|
|
||||||
</div>
|
|
||||||
</Form.Field>
|
|
||||||
)}
|
)}
|
||||||
</BaseModal.Content>
|
</BaseModal.Content>
|
||||||
<BaseModal.Footer
|
<BaseModal.Footer
|
||||||
submit={{ label: renderKey ? "Done" : "Create Secret Key" }}
|
submit={{ label: renderKey ? "Done" : (modalProps?.buttonText ?? "") }}
|
||||||
/>
|
/>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export default function ComponentTextModal({
|
||||||
readonly = false,
|
readonly = false,
|
||||||
password,
|
password,
|
||||||
changeVisibility,
|
changeVisibility,
|
||||||
|
onCloseModal,
|
||||||
}: textModalPropsType): JSX.Element {
|
}: textModalPropsType): JSX.Element {
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [inputValue, setInputValue] = useState(value);
|
const [inputValue, setInputValue] = useState(value);
|
||||||
|
|
@ -28,6 +29,12 @@ export default function ComponentTextModal({
|
||||||
if (typeof value === "string") setInputValue(value);
|
if (typeof value === "string") setInputValue(value);
|
||||||
}, [value, modalOpen]);
|
}, [value, modalOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!modalOpen) {
|
||||||
|
onCloseModal?.();
|
||||||
|
}
|
||||||
|
}, [modalOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseModal
|
<BaseModal
|
||||||
onChangeOpenModal={(open) => {}}
|
onChangeOpenModal={(open) => {}}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from "@/components/ui/disclosure";
|
} from "@/components/ui/disclosure";
|
||||||
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
|
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
|
import { BundleItemProps } from "../../types";
|
||||||
import SidebarItemsList from "../sidebarItemsList";
|
import SidebarItemsList from "../sidebarItemsList";
|
||||||
|
|
||||||
export const BundleItem = memo(
|
export const BundleItem = memo(
|
||||||
|
|
@ -15,21 +16,11 @@ export const BundleItem = memo(
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
dataFilter,
|
dataFilter,
|
||||||
nodeColors,
|
nodeColors,
|
||||||
chatInputAdded,
|
uniqueInputsComponents,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
sensitiveSort,
|
sensitiveSort,
|
||||||
handleKeyDownInput,
|
handleKeyDownInput,
|
||||||
}: {
|
}: BundleItemProps) => {
|
||||||
item: any;
|
|
||||||
isOpen: boolean;
|
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
|
||||||
dataFilter: any;
|
|
||||||
nodeColors: any;
|
|
||||||
chatInputAdded: any;
|
|
||||||
onDragStart: any;
|
|
||||||
sensitiveSort: any;
|
|
||||||
handleKeyDownInput: any;
|
|
||||||
}) => {
|
|
||||||
if (
|
if (
|
||||||
!dataFilter[item.name] ||
|
!dataFilter[item.name] ||
|
||||||
Object.keys(dataFilter[item.name]).length === 0
|
Object.keys(dataFilter[item.name]).length === 0
|
||||||
|
|
@ -68,7 +59,7 @@ export const BundleItem = memo(
|
||||||
item={item}
|
item={item}
|
||||||
dataFilter={dataFilter}
|
dataFilter={dataFilter}
|
||||||
nodeColors={nodeColors}
|
nodeColors={nodeColors}
|
||||||
chatInputAdded={chatInputAdded}
|
uniqueInputsComponents={uniqueInputsComponents}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
sensitiveSort={sensitiveSort}
|
sensitiveSort={sensitiveSort}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export const CategoryDisclosure = memo(function CategoryDisclosure({
|
||||||
setOpenCategories,
|
setOpenCategories,
|
||||||
dataFilter,
|
dataFilter,
|
||||||
nodeColors,
|
nodeColors,
|
||||||
chatInputAdded,
|
uniqueInputsComponents,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
sensitiveSort,
|
sensitiveSort,
|
||||||
}: {
|
}: {
|
||||||
|
|
@ -24,7 +24,10 @@ export const CategoryDisclosure = memo(function CategoryDisclosure({
|
||||||
setOpenCategories;
|
setOpenCategories;
|
||||||
dataFilter: any;
|
dataFilter: any;
|
||||||
nodeColors: any;
|
nodeColors: any;
|
||||||
chatInputAdded: boolean;
|
uniqueInputsComponents: {
|
||||||
|
chatInput: boolean;
|
||||||
|
webhookInput: boolean;
|
||||||
|
};
|
||||||
onDragStart: (
|
onDragStart: (
|
||||||
event: React.DragEvent<any>,
|
event: React.DragEvent<any>,
|
||||||
data: { type: string; node?: APIClassType },
|
data: { type: string; node?: APIClassType },
|
||||||
|
|
@ -84,7 +87,7 @@ export const CategoryDisclosure = memo(function CategoryDisclosure({
|
||||||
item={item}
|
item={item}
|
||||||
dataFilter={dataFilter}
|
dataFilter={dataFilter}
|
||||||
nodeColors={nodeColors}
|
nodeColors={nodeColors}
|
||||||
chatInputAdded={chatInputAdded}
|
uniqueInputsComponents={uniqueInputsComponents}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
sensitiveSort={sensitiveSort}
|
sensitiveSort={sensitiveSort}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export const CategoryGroup = memo(function CategoryGroup({
|
||||||
setOpenCategories,
|
setOpenCategories,
|
||||||
search,
|
search,
|
||||||
nodeColors,
|
nodeColors,
|
||||||
chatInputAdded,
|
uniqueInputsComponents,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
sensitiveSort,
|
sensitiveSort,
|
||||||
}: CategoryGroupProps) {
|
}: CategoryGroupProps) {
|
||||||
|
|
@ -65,9 +65,9 @@ export const CategoryGroup = memo(function CategoryGroup({
|
||||||
setOpenCategories={setOpenCategories}
|
setOpenCategories={setOpenCategories}
|
||||||
dataFilter={dataFilter}
|
dataFilter={dataFilter}
|
||||||
nodeColors={nodeColors}
|
nodeColors={nodeColors}
|
||||||
chatInputAdded={chatInputAdded}
|
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
sensitiveSort={sensitiveSort}
|
sensitiveSort={sensitiveSort}
|
||||||
|
uniqueInputsComponents={uniqueInputsComponents}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { memo, useMemo } from "react";
|
import { memo, useMemo } from "react";
|
||||||
|
import { SidebarGroupProps } from "../../types";
|
||||||
import { BundleItem } from "../bundleItems";
|
import { BundleItem } from "../bundleItems";
|
||||||
|
|
||||||
export const MemoizedSidebarGroup = memo(
|
export const MemoizedSidebarGroup = memo(
|
||||||
|
|
@ -14,26 +15,13 @@ export const MemoizedSidebarGroup = memo(
|
||||||
sortedCategories,
|
sortedCategories,
|
||||||
dataFilter,
|
dataFilter,
|
||||||
nodeColors,
|
nodeColors,
|
||||||
chatInputAdded,
|
|
||||||
onDragStart,
|
onDragStart,
|
||||||
sensitiveSort,
|
sensitiveSort,
|
||||||
openCategories,
|
openCategories,
|
||||||
setOpenCategories,
|
setOpenCategories,
|
||||||
handleKeyDownInput,
|
handleKeyDownInput,
|
||||||
}: {
|
uniqueInputsComponents,
|
||||||
BUNDLES: any;
|
}: SidebarGroupProps) => {
|
||||||
search: any;
|
|
||||||
sortedCategories: any;
|
|
||||||
dataFilter: any;
|
|
||||||
nodeColors: any;
|
|
||||||
chatInputAdded: any;
|
|
||||||
onDragStart: any;
|
|
||||||
sensitiveSort: any;
|
|
||||||
openCategories: any;
|
|
||||||
setOpenCategories: any;
|
|
||||||
handleKeyDownInput: any;
|
|
||||||
}) => {
|
|
||||||
// Memoize the sorted bundles calculation
|
|
||||||
const sortedBundles = useMemo(() => {
|
const sortedBundles = useMemo(() => {
|
||||||
return BUNDLES.toSorted((a, b) => {
|
return BUNDLES.toSorted((a, b) => {
|
||||||
const referenceArray = search !== "" ? sortedCategories : BUNDLES;
|
const referenceArray = search !== "" ? sortedCategories : BUNDLES;
|
||||||
|
|
@ -63,7 +51,7 @@ export const MemoizedSidebarGroup = memo(
|
||||||
}}
|
}}
|
||||||
dataFilter={dataFilter}
|
dataFilter={dataFilter}
|
||||||
nodeColors={nodeColors}
|
nodeColors={nodeColors}
|
||||||
chatInputAdded={chatInputAdded}
|
uniqueInputsComponents={uniqueInputsComponents}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
sensitiveSort={sensitiveSort}
|
sensitiveSort={sensitiveSort}
|
||||||
handleKeyDownInput={handleKeyDownInput}
|
handleKeyDownInput={handleKeyDownInput}
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ export const SidebarDraggableComponent = forwardRef(
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
className="m-[1px] rounded-md outline-none ring-ring focus-visible:ring-1"
|
className="m-[1px] rounded-md outline-none ring-ring focus-visible:ring-1"
|
||||||
|
data-testid={`${sectionName.toLowerCase()}_${display_name.toLowerCase()}_draggable`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid={sectionName + display_name}
|
data-testid={sectionName + display_name}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||||
import { removeCountFromString } from "@/utils/utils";
|
import { removeCountFromString } from "@/utils/utils";
|
||||||
|
import { disableItem } from "../../helpers/disable-item";
|
||||||
|
import { getDisabledTooltip } from "../../helpers/get-disabled-tooltip";
|
||||||
import SidebarDraggableComponent from "../sidebarDraggableComponent";
|
import SidebarDraggableComponent from "../sidebarDraggableComponent";
|
||||||
|
|
||||||
const SidebarItemsList = ({
|
const SidebarItemsList = ({
|
||||||
item,
|
item,
|
||||||
dataFilter,
|
dataFilter,
|
||||||
nodeColors,
|
nodeColors,
|
||||||
chatInputAdded,
|
uniqueInputsComponents,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
sensitiveSort,
|
sensitiveSort,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -46,8 +48,11 @@ const SidebarItemsList = ({
|
||||||
official={currentItem.official === false ? false : true}
|
official={currentItem.official === false ? false : true}
|
||||||
beta={currentItem.beta ?? false}
|
beta={currentItem.beta ?? false}
|
||||||
legacy={currentItem.legacy ?? false}
|
legacy={currentItem.legacy ?? false}
|
||||||
disabled={SBItemName === "ChatInput" && chatInputAdded}
|
disabled={disableItem(SBItemName, uniqueInputsComponents)}
|
||||||
disabledTooltip="Chat input already added"
|
disabledTooltip={getDisabledTooltip(
|
||||||
|
SBItemName,
|
||||||
|
uniqueInputsComponents,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</ShadTooltip>
|
</ShadTooltip>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { UniqueInputsComponents } from "../types";
|
||||||
|
|
||||||
|
export const disableItem = (
|
||||||
|
SBItemName: string,
|
||||||
|
uniqueInputsComponents: UniqueInputsComponents,
|
||||||
|
) => {
|
||||||
|
if (SBItemName === "ChatInput" && uniqueInputsComponents.chatInput) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (SBItemName === "Webhook" && uniqueInputsComponents.webhookInput) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { UniqueInputsComponents } from "../types";
|
||||||
|
|
||||||
|
export const getDisabledTooltip = (
|
||||||
|
SBItemName: string,
|
||||||
|
uniqueInputsComponents: UniqueInputsComponents,
|
||||||
|
) => {
|
||||||
|
if (SBItemName === "ChatInput" && uniqueInputsComponents.chatInput) {
|
||||||
|
return "Chat input already added";
|
||||||
|
}
|
||||||
|
if (SBItemName === "Webhook" && uniqueInputsComponents.webhookInput) {
|
||||||
|
return "Webhook already added";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
@ -8,7 +8,7 @@ import SkeletonGroup from "@/components/ui/skeletonGroup";
|
||||||
import { useAddComponent } from "@/hooks/useAddComponent";
|
import { useAddComponent } from "@/hooks/useAddComponent";
|
||||||
import { useShortcutsStore } from "@/stores/shortcuts";
|
import { useShortcutsStore } from "@/stores/shortcuts";
|
||||||
import { useStoreStore } from "@/stores/storeStore";
|
import { useStoreStore } from "@/stores/storeStore";
|
||||||
import { checkChatInput } from "@/utils/reactflowUtils";
|
import { checkChatInput, checkWebhookInput } from "@/utils/reactflowUtils";
|
||||||
import {
|
import {
|
||||||
nodeColors,
|
nodeColors,
|
||||||
SIDEBAR_BUNDLES,
|
SIDEBAR_BUNDLES,
|
||||||
|
|
@ -36,16 +36,18 @@ import { combinedResultsFn } from "./helpers/combined-results";
|
||||||
import { filteredDataFn } from "./helpers/filtered-data";
|
import { filteredDataFn } from "./helpers/filtered-data";
|
||||||
import { normalizeString } from "./helpers/normalize-string";
|
import { normalizeString } from "./helpers/normalize-string";
|
||||||
import { traditionalSearchMetadata } from "./helpers/traditional-search-metadata";
|
import { traditionalSearchMetadata } from "./helpers/traditional-search-metadata";
|
||||||
|
import { UniqueInputsComponents } from "./types";
|
||||||
|
|
||||||
const CATEGORIES = SIDEBAR_CATEGORIES;
|
const CATEGORIES = SIDEBAR_CATEGORIES;
|
||||||
const BUNDLES = SIDEBAR_BUNDLES;
|
const BUNDLES = SIDEBAR_BUNDLES;
|
||||||
|
|
||||||
interface FlowSidebarComponentProps {
|
interface FlowSidebarComponentProps {
|
||||||
|
isLoading?: boolean;
|
||||||
showLegacy: boolean;
|
showLegacy: boolean;
|
||||||
setShowLegacy: (value: boolean) => void;
|
setShowLegacy: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
|
||||||
const { data, templates } = useTypesStore(
|
const { data, templates } = useTypesStore(
|
||||||
useCallback(
|
useCallback(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
|
|
@ -86,6 +88,13 @@ export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
||||||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
const chatInputAdded = useMemo(() => checkChatInput(nodes), [nodes]);
|
const chatInputAdded = useMemo(() => checkChatInput(nodes), [nodes]);
|
||||||
|
const webhookInputAdded = useMemo(() => checkWebhookInput(nodes), [nodes]);
|
||||||
|
const uniqueInputsComponents: UniqueInputsComponents = useMemo(() => {
|
||||||
|
return {
|
||||||
|
chatInput: chatInputAdded,
|
||||||
|
webhookInput: webhookInputAdded,
|
||||||
|
};
|
||||||
|
}, [chatInputAdded, webhookInputAdded]);
|
||||||
|
|
||||||
const customComponent = useMemo(() => {
|
const customComponent = useMemo(() => {
|
||||||
return data?.["custom_component"]?.["CustomComponent"] ?? null;
|
return data?.["custom_component"]?.["CustomComponent"] ?? null;
|
||||||
|
|
@ -347,9 +356,9 @@ export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
||||||
setOpenCategories={setOpenCategories}
|
setOpenCategories={setOpenCategories}
|
||||||
search={search}
|
search={search}
|
||||||
nodeColors={nodeColors}
|
nodeColors={nodeColors}
|
||||||
chatInputAdded={chatInputAdded}
|
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
sensitiveSort={sensitiveSort}
|
sensitiveSort={sensitiveSort}
|
||||||
|
uniqueInputsComponents={uniqueInputsComponents}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasBundleItems && (
|
{hasBundleItems && (
|
||||||
|
|
@ -359,12 +368,12 @@ export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
||||||
sortedCategories={sortedCategories}
|
sortedCategories={sortedCategories}
|
||||||
dataFilter={dataFilter}
|
dataFilter={dataFilter}
|
||||||
nodeColors={nodeColors}
|
nodeColors={nodeColors}
|
||||||
chatInputAdded={chatInputAdded}
|
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
sensitiveSort={sensitiveSort}
|
sensitiveSort={sensitiveSort}
|
||||||
openCategories={openCategories}
|
openCategories={openCategories}
|
||||||
setOpenCategories={setOpenCategories}
|
setOpenCategories={setOpenCategories}
|
||||||
handleKeyDownInput={handleKeyDownInput}
|
handleKeyDownInput={handleKeyDownInput}
|
||||||
|
uniqueInputsComponents={uniqueInputsComponents}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { APIClassType, APIDataType } from "@/types/api";
|
import { APIClassType, APIDataType } from "@/types/api";
|
||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
export interface NodeColors {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CategoryGroupProps {
|
export interface CategoryGroupProps {
|
||||||
dataFilter: APIDataType;
|
dataFilter: APIDataType;
|
||||||
sortedCategories: string[];
|
sortedCategories: string[];
|
||||||
|
|
@ -12,15 +16,57 @@ export interface CategoryGroupProps {
|
||||||
openCategories: string[];
|
openCategories: string[];
|
||||||
setOpenCategories: (categories: string[]) => void;
|
setOpenCategories: (categories: string[]) => void;
|
||||||
search: string;
|
search: string;
|
||||||
nodeColors: {
|
nodeColors: NodeColors;
|
||||||
[key: string]: string;
|
|
||||||
};
|
|
||||||
chatInputAdded: boolean;
|
|
||||||
onDragStart: (
|
onDragStart: (
|
||||||
event: React.DragEvent<any>,
|
event: React.DragEvent<any>,
|
||||||
data: { type: string; node?: APIClassType },
|
data: { type: string; node?: APIClassType },
|
||||||
) => void;
|
) => void;
|
||||||
sensitiveSort: (a: string, b: string) => number;
|
sensitiveSort: (a: string, b: string) => number;
|
||||||
|
uniqueInputsComponents: {
|
||||||
|
chatInput: boolean;
|
||||||
|
webhookInput: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SidebarGroupProps {
|
||||||
|
BUNDLES: any;
|
||||||
|
search: string;
|
||||||
|
sortedCategories: string[];
|
||||||
|
dataFilter: APIDataType;
|
||||||
|
nodeColors: NodeColors;
|
||||||
|
onDragStart: (
|
||||||
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
|
data: { type: string; node?: APIClassType },
|
||||||
|
) => void;
|
||||||
|
sensitiveSort: (a: string, b: string) => number;
|
||||||
|
openCategories: string[];
|
||||||
|
setOpenCategories: (
|
||||||
|
categories: string[] | ((prev: string[]) => string[]),
|
||||||
|
) => void;
|
||||||
|
handleKeyDownInput: (
|
||||||
|
event: React.KeyboardEvent<HTMLDivElement>,
|
||||||
|
name: string,
|
||||||
|
) => void;
|
||||||
|
uniqueInputsComponents: UniqueInputsComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BundleItemProps {
|
||||||
|
item: {
|
||||||
|
name: string;
|
||||||
|
display_name: string;
|
||||||
|
icon: string;
|
||||||
|
};
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
dataFilter: APIDataType;
|
||||||
|
nodeColors: NodeColors;
|
||||||
|
uniqueInputsComponents: UniqueInputsComponents;
|
||||||
|
onDragStart: (
|
||||||
|
event: React.DragEvent<any>,
|
||||||
|
data: { type: string; node?: APIClassType },
|
||||||
|
) => void;
|
||||||
|
sensitiveSort: (a: string, b: string) => number;
|
||||||
|
handleKeyDownInput: (event: React.KeyboardEvent<any>, name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarHeaderComponentProps {
|
export interface SidebarHeaderComponentProps {
|
||||||
|
|
@ -50,3 +96,8 @@ export interface SidebarHeaderComponentProps {
|
||||||
setFilterData: Dispatch<SetStateAction<APIDataType>>;
|
setFilterData: Dispatch<SetStateAction<APIDataType>>;
|
||||||
data: APIDataType;
|
data: APIDataType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UniqueInputsComponents {
|
||||||
|
chatInput: boolean;
|
||||||
|
webhookInput: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import ForwardedIconComponent from "../../../../../../components/common/genericI
|
||||||
import { Button } from "../../../../../../components/ui/button";
|
import { Button } from "../../../../../../components/ui/button";
|
||||||
import { API_PAGE_PARAGRAPH } from "../../../../../../constants/constants";
|
import { API_PAGE_PARAGRAPH } from "../../../../../../constants/constants";
|
||||||
import SecretKeyModal from "../../../../../../modals/secretKeyModal";
|
import SecretKeyModal from "../../../../../../modals/secretKeyModal";
|
||||||
|
import { getModalPropsApiKey } from "../../helpers/get-modal-props";
|
||||||
|
|
||||||
type ApiKeyHeaderComponentProps = {
|
type ApiKeyHeaderComponentProps = {
|
||||||
selectedRows: string[];
|
selectedRows: string[];
|
||||||
|
|
@ -13,6 +14,7 @@ const ApiKeyHeaderComponent = ({
|
||||||
fetchApiKeys,
|
fetchApiKeys,
|
||||||
userId,
|
userId,
|
||||||
}: ApiKeyHeaderComponentProps) => {
|
}: ApiKeyHeaderComponentProps) => {
|
||||||
|
const modalProps = getModalPropsApiKey();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full items-start justify-between gap-6">
|
<div className="flex w-full items-start justify-between gap-6">
|
||||||
|
|
@ -27,7 +29,11 @@ const ApiKeyHeaderComponent = ({
|
||||||
<p className="text-sm text-muted-foreground">{API_PAGE_PARAGRAPH}</p>
|
<p className="text-sm text-muted-foreground">{API_PAGE_PARAGRAPH}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-shrink-0 items-center gap-2">
|
<div className="flex flex-shrink-0 items-center gap-2">
|
||||||
<SecretKeyModal data={userId} onCloseModal={fetchApiKeys}>
|
<SecretKeyModal
|
||||||
|
modalProps={modalProps}
|
||||||
|
data={userId}
|
||||||
|
onCloseModal={fetchApiKeys}
|
||||||
|
>
|
||||||
<Button data-testid="api-key-button-store" variant="primary">
|
<Button data-testid="api-key-button-store" variant="primary">
|
||||||
<ForwardedIconComponent name="Plus" className="w-4" />
|
<ForwardedIconComponent name="Plus" className="w-4" />
|
||||||
Add New
|
Add New
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
export const getModalPropsApiKey = () => {
|
||||||
|
const modalProps = {
|
||||||
|
title: "Create API Key",
|
||||||
|
description: "Create a secret API Key to use Langflow API.",
|
||||||
|
inputPlaceholder: "My API Key",
|
||||||
|
buttonText: "Generate API Key",
|
||||||
|
generatedKeyMessage: (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
Please save this secret key somewhere safe and accessible. For security
|
||||||
|
reasons, <strong>you won't be able to view it again</strong> through
|
||||||
|
your account. If you lose this secret key, you'll need to generate a new
|
||||||
|
one.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
showIcon: true,
|
||||||
|
inputLabel: (
|
||||||
|
<>
|
||||||
|
<span className="text-sm">Description</span>{" "}
|
||||||
|
<span className="text-xs text-muted-foreground">(optional)</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return modalProps;
|
||||||
|
};
|
||||||
|
|
@ -18,7 +18,6 @@ import ApiKeyHeaderComponent from "./components/ApiKeyHeader";
|
||||||
import { getColumnDefs } from "./helpers/column-defs";
|
import { getColumnDefs } from "./helpers/column-defs";
|
||||||
|
|
||||||
export default function ApiKeysPage() {
|
export default function ApiKeysPage() {
|
||||||
const [loadingKeys, setLoadingKeys] = useState(true);
|
|
||||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||||
|
|
|
||||||
|
|
@ -35,4 +35,7 @@ export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
|
||||||
setTags: (tags: Tag[]) => set({ tags }),
|
setTags: (tags: Tag[]) => set({ tags }),
|
||||||
featureFlags: {},
|
featureFlags: {},
|
||||||
setFeatureFlags: (featureFlags: Record<string, any>) => set({ featureFlags }),
|
setFeatureFlags: (featureFlags: Record<string, any>) => set({ featureFlags }),
|
||||||
|
webhookPollingInterval: 5000,
|
||||||
|
setWebhookPollingInterval: (webhookPollingInterval: number) =>
|
||||||
|
set({ webhookPollingInterval }),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
@apply flex gap-2;
|
@apply flex gap-2;
|
||||||
}
|
}
|
||||||
.primary-input {
|
.primary-input {
|
||||||
@apply form-input block w-full truncate rounded-md border border-border bg-background px-3 text-left text-sm placeholder:text-muted-foreground hover:border-muted-foreground focus:border-foreground focus:placeholder-transparent focus:ring-0 focus:ring-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted disabled:opacity-100 placeholder:disabled:text-muted-foreground;
|
@apply form-input block w-full truncate rounded-md border border-border bg-background px-3 text-left text-sm placeholder:text-muted-foreground hover:border-muted-foreground focus:border-foreground focus:placeholder-transparent focus:ring-0 focus:ring-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-muted disabled:text-muted-foreground disabled:opacity-100 placeholder:disabled:text-muted-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-card {
|
.skeleton-card {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export type APIClassType = {
|
||||||
flow?: FlowType;
|
flow?: FlowType;
|
||||||
field_order?: string[];
|
field_order?: string[];
|
||||||
tool_mode?: boolean;
|
tool_mode?: boolean;
|
||||||
|
type?: string;
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| Array<string>
|
| Array<string>
|
||||||
| string
|
| string
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,29 @@ export type UserInputType = {
|
||||||
export type ApiKeyType = {
|
export type ApiKeyType = {
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
data?: any;
|
data?: any;
|
||||||
onCloseModal: () => void;
|
onCloseModal?: () => void;
|
||||||
|
modalProps?: {
|
||||||
|
title?: string;
|
||||||
|
description?: string | ReactElement | HTMLElement;
|
||||||
|
inputLabel?: string | ReactElement | HTMLElement | ReactNode;
|
||||||
|
inputPlaceholder?: string;
|
||||||
|
buttonText?: string;
|
||||||
|
generatedKeyMessage?: string | ReactElement | HTMLElement;
|
||||||
|
showIcon?: boolean;
|
||||||
|
size?:
|
||||||
|
| "x-small"
|
||||||
|
| "smaller"
|
||||||
|
| "small"
|
||||||
|
| "medium"
|
||||||
|
| "medium-tall"
|
||||||
|
| "large"
|
||||||
|
| "three-cards"
|
||||||
|
| "large-thin"
|
||||||
|
| "large-h-full"
|
||||||
|
| "templates"
|
||||||
|
| "small-h-full"
|
||||||
|
| "medium-h-full";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StoreApiKeyType = {
|
export type StoreApiKeyType = {
|
||||||
|
|
@ -574,6 +596,7 @@ export type iconsType = {
|
||||||
export type modalHeaderType = {
|
export type modalHeaderType = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
description: string | JSX.Element | null;
|
description: string | JSX.Element | null;
|
||||||
|
clampDescription?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type codeAreaModalPropsType = {
|
export type codeAreaModalPropsType = {
|
||||||
|
|
@ -640,6 +663,7 @@ export type textModalPropsType = {
|
||||||
changeVisibility?: () => void;
|
changeVisibility?: () => void;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
setOpen?: (open: boolean) => void;
|
setOpen?: (open: boolean) => void;
|
||||||
|
onCloseModal?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type newFlowModalPropsType = {
|
export type newFlowModalPropsType = {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export type GetCodeType = {
|
||||||
flowId: string;
|
flowId: string;
|
||||||
flowName: string;
|
flowName: string;
|
||||||
isAuth: boolean;
|
isAuth: boolean;
|
||||||
tweaksBuildedObject: {};
|
tweaksBuildedObject?: {};
|
||||||
endpointName?: string | null;
|
endpointName?: string | null;
|
||||||
activeTweaks: boolean;
|
activeTweaks?: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ export type UtilityStoreType = {
|
||||||
setTags: (tags: Tag[]) => void;
|
setTags: (tags: Tag[]) => void;
|
||||||
featureFlags: Record<string, any>;
|
featureFlags: Record<string, any>;
|
||||||
setFeatureFlags: (featureFlags: Record<string, any>) => void;
|
setFeatureFlags: (featureFlags: Record<string, any>) => void;
|
||||||
|
webhookPollingInterval: number;
|
||||||
|
setWebhookPollingInterval: (webhookPollingInterval: number) => void;
|
||||||
chatValueStore: string;
|
chatValueStore: string;
|
||||||
setChatValueStore: (value: string) => void;
|
setChatValueStore: (value: string) => void;
|
||||||
dismissAll: boolean;
|
dismissAll: boolean;
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ export function checkChatInput(nodes: Node[]) {
|
||||||
return nodes.some((node) => node.data.type === "ChatInput");
|
return nodes.some((node) => node.data.type === "ChatInput");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkWebhookInput(nodes: Node[]) {
|
||||||
|
return nodes.some((node) => node.data.type === "Webhook");
|
||||||
|
}
|
||||||
|
|
||||||
export function cleanEdges(nodes: AllNodeType[], edges: EdgeType[]) {
|
export function cleanEdges(nodes: AllNodeType[], edges: EdgeType[]) {
|
||||||
let newEdges: EdgeType[] = cloneDeep(
|
let newEdges: EdgeType[] = cloneDeep(
|
||||||
edges.map((edge) => ({ ...edge, selected: false, animated: false })),
|
edges.map((edge) => ({ ...edge, selected: false, animated: false })),
|
||||||
|
|
|
||||||
127
src/frontend/tests/core/unit/webhookComponent.spec.ts
Normal file
127
src/frontend/tests/core/unit/webhookComponent.spec.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { adjustScreenView } from "../../utils/adjust-screen-view";
|
||||||
|
import { awaitBootstrapTest } from "../../utils/await-bootstrap-test";
|
||||||
|
|
||||||
|
test(
|
||||||
|
"user should be able to create an api key within a webhook component",
|
||||||
|
{ tag: ["@release", "@workspace"] },
|
||||||
|
async ({ page }) => {
|
||||||
|
const randomApiKeyDescription =
|
||||||
|
Math.random().toString(36).substring(2, 15) +
|
||||||
|
Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
|
await awaitBootstrapTest(page);
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.getByTestId("blank-flow").click();
|
||||||
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
|
await page.getByTestId("sidebar-search-input").fill("webhook");
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="dataWebhook"]', {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("dataWebhook")
|
||||||
|
.hover()
|
||||||
|
.then(async () => {
|
||||||
|
await page.getByTestId("add-component-button-webhook").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await adjustScreenView(page);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("data_webhook_draggable")
|
||||||
|
.hover()
|
||||||
|
.then(async () => {
|
||||||
|
await page.waitForSelector("text=Webhook already added", {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("btn_copy_str_endpoint").click();
|
||||||
|
await page.waitForSelector("text=Endpoint URL copied", { timeout: 30000 });
|
||||||
|
|
||||||
|
await page.getByTestId("title-Webhook").click();
|
||||||
|
await page.getByTestId("edit-button-modal").click();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("button_open_text_area_modal_str_edit_curl_advanced")
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const curl = await page.getByTestId("text-area-modal").inputValue();
|
||||||
|
|
||||||
|
const currentUrl = page.url();
|
||||||
|
|
||||||
|
const flowId = currentUrl.split("/")[2];
|
||||||
|
|
||||||
|
expect(curl).toContain(flowId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"user should be able to poll a webhook",
|
||||||
|
{ tag: ["@release", "@workspace"] },
|
||||||
|
async ({ page, request }) => {
|
||||||
|
await page.route("**/api/v1/config", (route) => {
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
webhook_polling_interval: 1000,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
...route.request().headers(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const randomApiKeyDescription =
|
||||||
|
Math.random().toString(36).substring(2, 15) +
|
||||||
|
Math.random().toString(36).substring(2, 15);
|
||||||
|
|
||||||
|
await awaitBootstrapTest(page);
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.getByTestId("blank-flow").click();
|
||||||
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
|
await page.getByTestId("sidebar-search-input").fill("webhook");
|
||||||
|
|
||||||
|
await page.waitForSelector('[data-testid="dataWebhook"]', {
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("dataWebhook")
|
||||||
|
.hover()
|
||||||
|
.then(async () => {
|
||||||
|
await page.getByTestId("add-component-button-webhook").click();
|
||||||
|
});
|
||||||
|
|
||||||
|
await adjustScreenView(page);
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByTestId("data_webhook_draggable")
|
||||||
|
.hover()
|
||||||
|
.then(async () => {
|
||||||
|
await page.waitForSelector("text=Webhook already added", {
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("btn_copy_str_endpoint").click();
|
||||||
|
await page.waitForSelector("text=Endpoint URL copied", { timeout: 30000 });
|
||||||
|
|
||||||
|
const monitorBuildPromise = page.waitForRequest((request) =>
|
||||||
|
request.url().includes("/monitor/build"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const monitorBuildRequest = await monitorBuildPromise;
|
||||||
|
expect(monitorBuildRequest).toBeTruthy();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -165,14 +165,12 @@ test(
|
||||||
await page.getByText("Langflow API").first().click();
|
await page.getByText("Langflow API").first().click();
|
||||||
await page.getByText("Langflow API", { exact: true }).nth(1).isVisible();
|
await page.getByText("Langflow API", { exact: true }).nth(1).isVisible();
|
||||||
await page.getByText("Add New").click();
|
await page.getByText("Add New").click();
|
||||||
await page.getByPlaceholder("Insert a name for your API Key").isVisible();
|
await page.getByPlaceholder("My API Key").isVisible();
|
||||||
|
|
||||||
const randomName = Math.random().toString(36).substring(2);
|
const randomName = Math.random().toString(36).substring(2);
|
||||||
|
|
||||||
await page
|
await page.getByPlaceholder("My API Key").fill(randomName);
|
||||||
.getByPlaceholder("Insert a name for your API Key")
|
await page.getByText("Generate API Key", { exact: true }).click();
|
||||||
.fill(randomName);
|
|
||||||
await page.getByText("Create Secret Key", { exact: true }).click();
|
|
||||||
|
|
||||||
// Wait for api key creation to complete and render the next form element
|
// Wait for api key creation to complete and render the next form element
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue