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.",
|
||||
show_default=False,
|
||||
),
|
||||
webhook_polling_interval: int | None = typer.Option( # noqa: ARG001
|
||||
None,
|
||||
help="Defines the polling interval for the webhook.",
|
||||
show_default=False,
|
||||
),
|
||||
) -> None:
|
||||
"""Run Langflow."""
|
||||
# Register SIGTERM handler
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ class ConfigResponse(BaseModel):
|
|||
auto_saving_interval: int
|
||||
health_check_max_retries: int
|
||||
max_file_size_upload: int
|
||||
webhook_polling_interval: int
|
||||
event_delivery: Literal["polling", "streaming"]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ FIELD_FORMAT_ATTRIBUTES = [
|
|||
"refresh_button_text",
|
||||
"options",
|
||||
"advanced",
|
||||
"copy_field",
|
||||
]
|
||||
|
||||
ORJSON_OPTIONS = orjson.OPT_INDENT_2 | orjson.OPT_SORT_KEYS | orjson.OPT_OMIT_MICROSECONDS
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from langflow.schema import Data
|
|||
|
||||
class WebhookComponent(Component):
|
||||
display_name = "Webhook"
|
||||
description = "Defines a webhook input for the flow."
|
||||
name = "Webhook"
|
||||
icon = "webhook"
|
||||
|
||||
|
|
@ -16,7 +15,23 @@ class WebhookComponent(Component):
|
|||
name="data",
|
||||
display_name="Payload",
|
||||
info="Receives a payload from external systems via HTTP POST.",
|
||||
)
|
||||
advanced=True,
|
||||
),
|
||||
MultilineInput(
|
||||
name="curl",
|
||||
display_name="cURL",
|
||||
value="CURL_WEBHOOK",
|
||||
advanced=True,
|
||||
input_types=[],
|
||||
),
|
||||
MultilineInput(
|
||||
name="endpoint",
|
||||
display_name="Endpoint",
|
||||
value="BACKEND_URL",
|
||||
advanced=False,
|
||||
copy_field=True,
|
||||
input_types=[],
|
||||
),
|
||||
]
|
||||
outputs = [
|
||||
Output(display_name="Data", name="output_data", method="build_data"),
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ from langflow.graph.graph.utils import (
|
|||
should_continue,
|
||||
)
|
||||
from langflow.graph.schema import InterfaceComponentTypes, RunOutputs
|
||||
from langflow.graph.utils import log_vertex_build
|
||||
from langflow.graph.vertex.base import Vertex, VertexStates
|
||||
from langflow.graph.vertex.schema import NodeData, NodeTypeEnum
|
||||
from langflow.graph.vertex.vertex_types import ComponentVertex, InterfaceVertex, StateVertex
|
||||
|
|
@ -1607,6 +1608,15 @@ class Graph:
|
|||
t.cancel()
|
||||
raise result
|
||||
if isinstance(result, VertexBuildResult):
|
||||
await log_vertex_build(
|
||||
flow_id=self.flow_id or "",
|
||||
vertex_id=result.vertex.id,
|
||||
valid=result.valid,
|
||||
params=result.params,
|
||||
data=result.result_dict,
|
||||
artifacts=result.artifacts,
|
||||
)
|
||||
|
||||
vertices.append(result.vertex)
|
||||
else:
|
||||
msg = f"Invalid result from task {task_name}: {result}"
|
||||
|
|
|
|||
|
|
@ -240,6 +240,7 @@ class MultilineInput(MessageTextInput, MultilineMixin, InputTraceMixin, ToolMode
|
|||
|
||||
field_type: SerializableFieldTypes = FieldTypes.TEXT
|
||||
multiline: CoalesceBool = True
|
||||
copy_field: CoalesceBool = False
|
||||
|
||||
|
||||
class MultilineSecretInput(MessageTextInput, MultilineMixin, InputTraceMixin):
|
||||
|
|
|
|||
|
|
@ -215,6 +215,8 @@ class Settings(BaseSettings):
|
|||
"""The maximum number of vertex builds to keep in the database."""
|
||||
max_vertex_builds_per_vertex: int = 2
|
||||
"""The maximum number of builds to keep per vertex. Older builds will be deleted."""
|
||||
webhook_polling_interval: int = 5000
|
||||
"""The polling interval for the webhook in ms."""
|
||||
|
||||
# MCP Server
|
||||
mcp_server_enabled: bool = True
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ class Input(BaseModel):
|
|||
|
||||
refresh_button: bool | None = None
|
||||
"""Specifies if the field should have a refresh button. Defaults to False."""
|
||||
|
||||
refresh_button_text: str | None = None
|
||||
"""Specifies the text for the refresh button. Defaults to None."""
|
||||
|
||||
|
|
@ -97,6 +98,7 @@ class Input(BaseModel):
|
|||
|
||||
load_from_db: bool = False
|
||||
"""Specifies if the field should be loaded from the database. Defaults to False."""
|
||||
|
||||
title_case: bool = False
|
||||
"""Specifies if the field should be displayed in title case. Defaults to True."""
|
||||
|
||||
|
|
|
|||
|
|
@ -415,6 +415,7 @@ async def update_settings(
|
|||
auto_saving_interval: int = 1000,
|
||||
health_check_max_retries: int = 5,
|
||||
max_file_size_upload: int = 100,
|
||||
webhook_polling_interval: int = 5000,
|
||||
) -> None:
|
||||
"""Update the settings from a config file."""
|
||||
# Check for database_url in the environment variables
|
||||
|
|
@ -448,6 +449,9 @@ async def update_settings(
|
|||
if max_file_size_upload is not None:
|
||||
logger.debug(f"Setting max_file_size_upload to {max_file_size_upload}")
|
||||
settings_service.settings.update_settings(max_file_size_upload=max_file_size_upload)
|
||||
if webhook_polling_interval is not None:
|
||||
logger.debug(f"Setting webhook_polling_interval to {webhook_polling_interval}")
|
||||
settings_service.settings.update_settings(webhook_polling_interval=webhook_polling_interval)
|
||||
|
||||
|
||||
def is_class_method(func, cls):
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export default function NodeDescription({
|
|||
description,
|
||||
selected,
|
||||
nodeId,
|
||||
emptyPlaceholder = "Double Click to Edit Description",
|
||||
emptyPlaceholder = "",
|
||||
placeholderClassName,
|
||||
charLimit,
|
||||
inputClassName,
|
||||
|
|
@ -69,23 +69,23 @@ export default function NodeDescription({
|
|||
}, [description]);
|
||||
|
||||
const MemoizedMarkdown = memo(Markdown);
|
||||
const renderedDescription = useMemo(
|
||||
() =>
|
||||
description === "" || !description ? (
|
||||
emptyPlaceholder
|
||||
) : (
|
||||
<MemoizedMarkdown
|
||||
linkTarget="_blank"
|
||||
className={cn(
|
||||
"markdown prose flex w-full flex-col text-[13px] leading-5 word-break-break-word [&_pre]:whitespace-break-spaces [&_pre]:!bg-code-description-background [&_pre_code]:!bg-code-description-background",
|
||||
mdClassName,
|
||||
)}
|
||||
>
|
||||
{String(description)}
|
||||
</MemoizedMarkdown>
|
||||
),
|
||||
[description, emptyPlaceholder, mdClassName],
|
||||
);
|
||||
|
||||
const renderedDescription = useMemo(() => {
|
||||
if (description === "" || !description) {
|
||||
return emptyPlaceholder;
|
||||
}
|
||||
return (
|
||||
<MemoizedMarkdown
|
||||
linkTarget="_blank"
|
||||
className={cn(
|
||||
"markdown prose flex w-full flex-col text-[13px] leading-5 word-break-break-word [&_pre]:whitespace-break-spaces [&_pre]:!bg-code-description-background [&_pre_code]:!bg-code-description-background",
|
||||
mdClassName,
|
||||
)}
|
||||
>
|
||||
{String(description)}
|
||||
</MemoizedMarkdown>
|
||||
);
|
||||
}, [description, emptyPlaceholder, mdClassName]);
|
||||
|
||||
const handleBlurFn = () => {
|
||||
setNodeDescription(nodeDescription);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
|
||||
import { NodeInfoType } from "@/components/core/parameterRenderComponent/types";
|
||||
import { usePostTemplateValue } from "@/controllers/API/queries/nodes/use-post-template-value";
|
||||
import {
|
||||
CustomParameterComponent,
|
||||
CustomParameterLabel,
|
||||
getCustomParameterTitle,
|
||||
} from "@/customization/components/custom-parameter";
|
||||
import useAuthStore from "@/stores/authStore";
|
||||
import { cn } from "@/utils/utils";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { default as IconComponent } from "../../../../components/common/genericIconComponent";
|
||||
import ShadTooltip from "../../../../components/common/shadTooltipComponent";
|
||||
import {
|
||||
|
|
@ -44,6 +46,8 @@ export default function NodeInputField({
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
const nodes = useFlowStore((state) => state.nodes);
|
||||
const edges = useFlowStore((state) => state.edges);
|
||||
const isAuth = useAuthStore((state) => state.isAuthenticated);
|
||||
const currentFlow = useFlowStore((state) => state.currentFlow);
|
||||
const myData = useTypesStore((state) => state.data);
|
||||
const postTemplateValue = usePostTemplateValue({
|
||||
node: data.node!,
|
||||
|
|
@ -64,6 +68,16 @@ export default function NodeInputField({
|
|||
name,
|
||||
});
|
||||
|
||||
const nodeInformationMetadata: NodeInfoType = useMemo(() => {
|
||||
return {
|
||||
flowId: currentFlow?.id ?? "",
|
||||
nodeType: data?.type?.toLowerCase() ?? "",
|
||||
flowName: currentFlow?.name ?? "",
|
||||
isAuth,
|
||||
variableName: name,
|
||||
};
|
||||
}, [data?.node?.id, isAuth, name]);
|
||||
|
||||
useFetchDataOnMount(data.node!, handleNodeClass, name, postTemplateValue);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -193,6 +207,7 @@ export default function NodeInputField({
|
|||
: data.node?.template[name].placeholder
|
||||
}
|
||||
isToolMode={isToolMode}
|
||||
nodeInformationMetadata={nodeInformationMetadata}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,14 +49,6 @@ const SnowflakeIcon = memo(() => (
|
|||
<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
|
||||
const HideShowButton = memo(
|
||||
({
|
||||
|
|
|
|||
|
|
@ -252,6 +252,13 @@ function GenericNode({
|
|||
const [hasChangedNodeDescription, setHasChangedNodeDescription] =
|
||||
useState(false);
|
||||
|
||||
const editedNameDescription =
|
||||
editNameDescription && hasChangedNodeDescription;
|
||||
|
||||
const hasDescription = useMemo(() => {
|
||||
return data.node?.description && data.node?.description !== "";
|
||||
}, [data.node?.description]);
|
||||
|
||||
const memoizedNodeToolbarComponent = useMemo(() => {
|
||||
return selected ? (
|
||||
<>
|
||||
|
|
@ -294,25 +301,21 @@ function GenericNode({
|
|||
showNode
|
||||
? "top-2 translate-x-[10.4rem]"
|
||||
: "top-0 translate-x-[6.4rem]",
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
editedNameDescription
|
||||
? "bg-accent-emerald"
|
||||
: "bg-zinc-foreground",
|
||||
)}
|
||||
data-testid={
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
editedNameDescription
|
||||
? "save-name-description-button"
|
||||
: "edit-name-description-button"
|
||||
}
|
||||
>
|
||||
<ForwardedIconComponent
|
||||
name={
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
? "Check"
|
||||
: "PencilLine"
|
||||
}
|
||||
name={editedNameDescription ? "Check" : "PencilLine"}
|
||||
strokeWidth={ICON_STROKE_WIDTH}
|
||||
className={cn(
|
||||
editNameDescription && hasChangedNodeDescription
|
||||
editedNameDescription
|
||||
? "text-accent-emerald-foreground"
|
||||
: "text-muted-foreground",
|
||||
"icon-size",
|
||||
|
|
@ -490,8 +493,9 @@ function GenericNode({
|
|||
<div
|
||||
data-testid={`${data.id}-main-node`}
|
||||
className={cn(
|
||||
"grid gap-3 text-wrap p-4 leading-5",
|
||||
"grid truncate text-wrap p-4 leading-5",
|
||||
showNode ? "border-b" : "relative",
|
||||
hasDescription && "gap-3",
|
||||
)}
|
||||
>
|
||||
<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 CopyFieldAreaComponent from "../copyFieldAreaComponent";
|
||||
import DropdownComponent from "../dropdownComponent";
|
||||
import InputGlobalComponent from "../inputGlobalComponent";
|
||||
import TextAreaComponent from "../textAreaComponent";
|
||||
import WebhookFieldComponent from "../webhookFieldComponent";
|
||||
|
||||
export function StrRenderComponent({
|
||||
templateData,
|
||||
|
|
@ -10,25 +12,43 @@ export function StrRenderComponent({
|
|||
placeholder,
|
||||
...baseInputProps
|
||||
}: InputProps<string, StrRenderComponentType>) {
|
||||
const { handleOnNewValue, id, isToolMode } = baseInputProps;
|
||||
const { handleOnNewValue, id, isToolMode, nodeInformationMetadata } =
|
||||
baseInputProps;
|
||||
|
||||
if (!templateData.options) {
|
||||
return templateData.multiline ? (
|
||||
<TextAreaComponent
|
||||
{...baseInputProps}
|
||||
// password={templateData.password}
|
||||
updateVisibility={() => {
|
||||
if (templateData.password !== undefined) {
|
||||
handleOnNewValue(
|
||||
{ password: !templateData.password },
|
||||
{ skipSnapshot: true },
|
||||
);
|
||||
}
|
||||
}}
|
||||
id={`textarea_${id}`}
|
||||
isToolMode={isToolMode}
|
||||
/>
|
||||
) : (
|
||||
const noOptions = !templateData.options;
|
||||
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
|
||||
{...baseInputProps}
|
||||
updateVisibility={() => {
|
||||
if (templateData.password !== undefined) {
|
||||
handleOnNewValue(
|
||||
{ password: !templateData.password },
|
||||
{ skipSnapshot: true },
|
||||
);
|
||||
}
|
||||
}}
|
||||
id={`textarea_${id}`}
|
||||
isToolMode={isToolMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGlobalComponent
|
||||
{...baseInputProps}
|
||||
password={templateData.password}
|
||||
|
|
@ -41,7 +61,7 @@ export function StrRenderComponent({
|
|||
);
|
||||
}
|
||||
|
||||
if (!!templateData.options) {
|
||||
if (hasOptions) {
|
||||
return (
|
||||
<DropdownComponent
|
||||
{...baseInputProps}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import useHandleOnNewValue from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import useHandleNodeClass from "@/CustomNodes/hooks/use-handle-node-class";
|
||||
import { ParameterRenderComponent } from "@/components/core/parameterRenderComponent";
|
||||
import { NodeInfoType } from "@/components/core/parameterRenderComponent/types";
|
||||
import useAuthStore from "@/stores/authStore";
|
||||
import useFlowStore from "@/stores/flowStore";
|
||||
import { useTweaksStore } from "@/stores/tweaksStore";
|
||||
import { APIClassType } from "@/types/api";
|
||||
import { isTargetHandleConnected } from "@/utils/reactflowUtils";
|
||||
import { CustomCellRendererProps } from "ag-grid-react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function TableNodeCellRender({
|
||||
value: { nodeId, parameterId, isTweaks },
|
||||
|
|
@ -15,6 +18,8 @@ export default function TableNodeCellRender({
|
|||
? useTweaksStore((state) => state.getNode(nodeId))
|
||||
: useFlowStore((state) => state.getNode(nodeId));
|
||||
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);
|
||||
|
||||
|
|
@ -37,6 +42,16 @@ export default function TableNodeCellRender({
|
|||
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 (
|
||||
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">
|
||||
|
|
@ -50,6 +65,7 @@ export default function TableNodeCellRender({
|
|||
handleNodeClass={handleNodeClass}
|
||||
nodeClass={node?.data.node}
|
||||
disabled={disabled}
|
||||
nodeInformationMetadata={nodeInformationMetadata}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { GRADIENT_CLASS } from "@/constants/constants";
|
||||
import { getCurlWebhookCode } from "@/modals/apiModal/utils/get-curl-code";
|
||||
import ComponentTextModal from "@/modals/textAreaModal";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { cn } from "../../../../../utils/utils";
|
||||
import IconComponent from "../../../../common/genericIconComponent";
|
||||
import { Input } from "../../../../ui/input";
|
||||
|
|
@ -18,6 +19,8 @@ const inputClasses = {
|
|||
password: "password",
|
||||
};
|
||||
|
||||
const WEBHOOK_VALUE = "CURL_WEBHOOK";
|
||||
|
||||
const externalLinkIconClasses = {
|
||||
gradient: ({
|
||||
disabled,
|
||||
|
|
@ -61,12 +64,29 @@ export default function TextAreaComponent({
|
|||
password,
|
||||
placeholder,
|
||||
isToolMode = false,
|
||||
nodeInformationMetadata,
|
||||
}: InputProps<string, TextAreaComponentType>): JSX.Element {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [isFocused, setIsFocused] = 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 = () => {
|
||||
return cn(
|
||||
inputClasses.base({ isFocused, password: password! }),
|
||||
|
|
@ -81,9 +101,21 @@ export default function TextAreaComponent({
|
|||
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 = () => (
|
||||
<div>
|
||||
{!disabled && (
|
||||
{!disabled && !isFocused && (
|
||||
<div
|
||||
className={cn(
|
||||
externalLinkIconClasses.gradient({
|
||||
|
|
@ -139,6 +171,7 @@ export default function TextAreaComponent({
|
|||
aria-label={disabled ? value : undefined}
|
||||
ref={inputRef}
|
||||
type={password ? (passwordVisible ? "text" : "password") : "text"}
|
||||
readOnly={isWebhook}
|
||||
/>
|
||||
|
||||
<ComponentTextModal
|
||||
|
|
@ -146,8 +179,14 @@ export default function TextAreaComponent({
|
|||
value={value}
|
||||
setValue={(newValue) => handleOnNewValue({ value: newValue })}
|
||||
disabled={disabled}
|
||||
onCloseModal={() => changeWebhookFormat("singleline")}
|
||||
>
|
||||
<div className="relative w-full">{renderIcon()}</div>
|
||||
<div
|
||||
onClick={() => changeWebhookFormat("multiline")}
|
||||
className="relative w-full"
|
||||
>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
</ComponentTextModal>
|
||||
{password && !isFocused && (
|
||||
<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 { StrRenderComponent } from "./components/strRenderComponent";
|
||||
import ToggleShadComponent from "./components/toggleShadComponent";
|
||||
import { InputProps } from "./types";
|
||||
import { InputProps, NodeInfoType } from "./types";
|
||||
|
||||
export function ParameterRenderComponent({
|
||||
handleOnNewValue,
|
||||
|
|
@ -32,6 +32,7 @@ export function ParameterRenderComponent({
|
|||
disabled,
|
||||
placeholder,
|
||||
isToolMode,
|
||||
nodeInformationMetadata,
|
||||
}: {
|
||||
handleOnNewValue:
|
||||
| handleOnNewValueType
|
||||
|
|
@ -46,6 +47,7 @@ export function ParameterRenderComponent({
|
|||
disabled: boolean;
|
||||
placeholder?: string;
|
||||
isToolMode?: boolean;
|
||||
nodeInformationMetadata?: NodeInfoType;
|
||||
}) {
|
||||
const id = (
|
||||
templateData.type +
|
||||
|
|
@ -67,6 +69,7 @@ export function ParameterRenderComponent({
|
|||
placeholder,
|
||||
isToolMode,
|
||||
nodeId,
|
||||
nodeInformationMetadata,
|
||||
};
|
||||
|
||||
if (TEXT_FIELD_TYPES.includes(templateData.type ?? "")) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type BaseInputProps<valueType = any> = {
|
|||
tooltip?: string;
|
||||
metadata?: any;
|
||||
nodeId?: string;
|
||||
nodeInformationMetadata?: NodeInfoType;
|
||||
};
|
||||
|
||||
// Generic type for composing input props
|
||||
|
|
@ -101,3 +102,11 @@ export type MultiselectComponentType = {
|
|||
options: string[];
|
||||
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 =
|
||||
"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 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) => {
|
||||
const { query } = UseRequestProcessor();
|
||||
|
||||
//@TODO: Request API key from DSLF endpoint
|
||||
const getApiKeysFn = async () => {
|
||||
return await api.get<IApiQueryResponse>(`${getURL("API_KEY")}/`);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface ConfigResponse {
|
|||
health_check_max_retries: number;
|
||||
max_file_size_upload: number;
|
||||
feature_flags: Record<string, any>;
|
||||
webhook_polling_interval: number;
|
||||
event_delivery: EventDeliveryType;
|
||||
}
|
||||
|
||||
|
|
@ -31,6 +32,9 @@ export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (
|
|||
(state) => state.setMaxFileSizeUpload,
|
||||
);
|
||||
const setFeatureFlags = useUtilityStore((state) => state.setFeatureFlags);
|
||||
const setWebhookPollingInterval = useUtilityStore(
|
||||
(state) => state.setWebhookPollingInterval,
|
||||
);
|
||||
|
||||
const { query } = UseRequestProcessor();
|
||||
|
||||
|
|
@ -48,6 +52,7 @@ export const useGetConfig: useQueryFunctionType<undefined, ConfigResponse> = (
|
|||
setHealthCheckMaxRetries(data.health_check_max_retries);
|
||||
setMaxFileSizeUpload(data.max_file_size_upload);
|
||||
setFeatureFlags(data.feature_flags);
|
||||
setWebhookPollingInterval(data.webhook_polling_interval);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ParameterRenderComponent } from "@/components/core/parameterRenderComponent";
|
||||
import { NodeInfoType } from "@/components/core/parameterRenderComponent/types";
|
||||
import { handleOnNewValueType } from "@/CustomNodes/hooks/use-handle-new-value";
|
||||
import { APIClassType, InputFieldType } from "@/types/api";
|
||||
import { cn } from "@/utils/utils";
|
||||
|
|
@ -15,6 +16,7 @@ export function CustomParameterComponent({
|
|||
disabled,
|
||||
placeholder,
|
||||
isToolMode,
|
||||
nodeInformationMetadata,
|
||||
}: {
|
||||
handleOnNewValue: handleOnNewValueType;
|
||||
name: string;
|
||||
|
|
@ -27,6 +29,7 @@ export function CustomParameterComponent({
|
|||
disabled: boolean;
|
||||
placeholder?: string;
|
||||
isToolMode?: boolean;
|
||||
nodeInformationMetadata?: NodeInfoType;
|
||||
}) {
|
||||
return (
|
||||
<ParameterRenderComponent
|
||||
|
|
@ -41,6 +44,7 @@ export function CustomParameterComponent({
|
|||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
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,
|
||||
isAuth,
|
||||
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 \\
|
||||
"${window.location.protocol}//${window.location.host}/api/v1/webhook/${
|
||||
endpointName || flowId
|
||||
}" \\
|
||||
"${baseUrl}" \\
|
||||
-H 'Content-Type: application/json'\\${
|
||||
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
|
||||
}
|
||||
-d '{"any": "data"}'
|
||||
`;
|
||||
`.trim();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,13 +74,20 @@ const Trigger: React.FC<TriggerProps> = ({
|
|||
const Header: React.FC<{
|
||||
children: ReactNode;
|
||||
description: string | JSX.Element | null;
|
||||
}> = ({ children, description }: modalHeaderType): JSX.Element => {
|
||||
clampDescription?: number;
|
||||
}> = ({
|
||||
children,
|
||||
description,
|
||||
clampDescription,
|
||||
}: modalHeaderType): JSX.Element => {
|
||||
return (
|
||||
<DialogHeader>
|
||||
<DialogTitle className="line-clamp-1 flex items-center pb-0.5 text-base">
|
||||
{children}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="line-clamp-2 text-sm">
|
||||
<DialogDescription
|
||||
className={`line-clamp-${clampDescription ?? 2} text-sm`}
|
||||
>
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</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 { Label } from "@radix-ui/react-form";
|
||||
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 { COPIED_NOTICE_ALERT } from "../../constants/alerts_constants";
|
||||
import { createApiKey } from "../../controllers/API";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { ApiKeyType } from "../../types/components";
|
||||
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({
|
||||
children,
|
||||
data,
|
||||
onCloseModal,
|
||||
}: ApiKeyType) {
|
||||
modalProps,
|
||||
}: ApiKeyType & { modalProps?: ModalProps }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
|
||||
const [apiKeyValue, setApiKeyValue] = useState("");
|
||||
|
|
@ -27,7 +35,7 @@ export default function SecretKeyModal({
|
|||
setRenderKey(false);
|
||||
resetForm();
|
||||
} else {
|
||||
onCloseModal();
|
||||
onCloseModal?.();
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
|
|
@ -72,78 +80,47 @@ export default function SecretKeyModal({
|
|||
return (
|
||||
<BaseModal
|
||||
onSubmit={handleSubmitForm}
|
||||
size="small-h-full"
|
||||
size={modalProps?.size ?? "small-h-full"}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
>
|
||||
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header
|
||||
clampDescription={3}
|
||||
description={
|
||||
renderKey ? (
|
||||
<>
|
||||
{" "}
|
||||
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.
|
||||
</>
|
||||
<>{modalProps?.generatedKeyMessage}</>
|
||||
) : (
|
||||
<>Create a secret API Key to use Langflow API.</>
|
||||
<>{modalProps?.description}</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<span className="pr-2">Create API Key</span>
|
||||
<IconComponent
|
||||
name="Key"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
<HeaderRender
|
||||
title={modalProps?.title}
|
||||
showIcon={modalProps?.showIcon}
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
{renderKey ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-full">
|
||||
<Input ref={inputRef} readOnly={true} value={apiKeyValue} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleCopyClick();
|
||||
}}
|
||||
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>
|
||||
</>
|
||||
<ContentRenderKey
|
||||
inputLabel={String(modalProps?.inputLabel ?? "")}
|
||||
inputRef={inputRef}
|
||||
apiKeyValue={apiKeyValue}
|
||||
handleCopyClick={handleCopyClick}
|
||||
textCopied={textCopied}
|
||||
renderKey={renderKey}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
<FormKeyRender
|
||||
modalProps={modalProps}
|
||||
apiKeyName={apiKeyName}
|
||||
inputRef={inputRef}
|
||||
setApiKeyName={setApiKeyName}
|
||||
/>
|
||||
)}
|
||||
</BaseModal.Content>
|
||||
<BaseModal.Footer
|
||||
submit={{ label: renderKey ? "Done" : "Create Secret Key" }}
|
||||
submit={{ label: renderKey ? "Done" : (modalProps?.buttonText ?? "") }}
|
||||
/>
|
||||
</BaseModal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export default function ComponentTextModal({
|
|||
readonly = false,
|
||||
password,
|
||||
changeVisibility,
|
||||
onCloseModal,
|
||||
}: textModalPropsType): JSX.Element {
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
|
@ -28,6 +29,12 @@ export default function ComponentTextModal({
|
|||
if (typeof value === "string") setInputValue(value);
|
||||
}, [value, modalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!modalOpen) {
|
||||
onCloseModal?.();
|
||||
}
|
||||
}, [modalOpen]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
onChangeOpenModal={(open) => {}}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
} from "@/components/ui/disclosure";
|
||||
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
|
||||
import { memo } from "react";
|
||||
import { BundleItemProps } from "../../types";
|
||||
import SidebarItemsList from "../sidebarItemsList";
|
||||
|
||||
export const BundleItem = memo(
|
||||
|
|
@ -15,21 +16,11 @@ export const BundleItem = memo(
|
|||
onOpenChange,
|
||||
dataFilter,
|
||||
nodeColors,
|
||||
chatInputAdded,
|
||||
uniqueInputsComponents,
|
||||
onDragStart,
|
||||
sensitiveSort,
|
||||
handleKeyDownInput,
|
||||
}: {
|
||||
item: any;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
dataFilter: any;
|
||||
nodeColors: any;
|
||||
chatInputAdded: any;
|
||||
onDragStart: any;
|
||||
sensitiveSort: any;
|
||||
handleKeyDownInput: any;
|
||||
}) => {
|
||||
}: BundleItemProps) => {
|
||||
if (
|
||||
!dataFilter[item.name] ||
|
||||
Object.keys(dataFilter[item.name]).length === 0
|
||||
|
|
@ -68,7 +59,7 @@ export const BundleItem = memo(
|
|||
item={item}
|
||||
dataFilter={dataFilter}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
uniqueInputsComponents={uniqueInputsComponents}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export const CategoryDisclosure = memo(function CategoryDisclosure({
|
|||
setOpenCategories,
|
||||
dataFilter,
|
||||
nodeColors,
|
||||
chatInputAdded,
|
||||
uniqueInputsComponents,
|
||||
onDragStart,
|
||||
sensitiveSort,
|
||||
}: {
|
||||
|
|
@ -24,7 +24,10 @@ export const CategoryDisclosure = memo(function CategoryDisclosure({
|
|||
setOpenCategories;
|
||||
dataFilter: any;
|
||||
nodeColors: any;
|
||||
chatInputAdded: boolean;
|
||||
uniqueInputsComponents: {
|
||||
chatInput: boolean;
|
||||
webhookInput: boolean;
|
||||
};
|
||||
onDragStart: (
|
||||
event: React.DragEvent<any>,
|
||||
data: { type: string; node?: APIClassType },
|
||||
|
|
@ -84,7 +87,7 @@ export const CategoryDisclosure = memo(function CategoryDisclosure({
|
|||
item={item}
|
||||
dataFilter={dataFilter}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
uniqueInputsComponents={uniqueInputsComponents}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const CategoryGroup = memo(function CategoryGroup({
|
|||
setOpenCategories,
|
||||
search,
|
||||
nodeColors,
|
||||
chatInputAdded,
|
||||
uniqueInputsComponents,
|
||||
onDragStart,
|
||||
sensitiveSort,
|
||||
}: CategoryGroupProps) {
|
||||
|
|
@ -65,9 +65,9 @@ export const CategoryGroup = memo(function CategoryGroup({
|
|||
setOpenCategories={setOpenCategories}
|
||||
dataFilter={dataFilter}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
uniqueInputsComponents={uniqueInputsComponents}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
SidebarMenu,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { memo, useMemo } from "react";
|
||||
import { SidebarGroupProps } from "../../types";
|
||||
import { BundleItem } from "../bundleItems";
|
||||
|
||||
export const MemoizedSidebarGroup = memo(
|
||||
|
|
@ -14,26 +15,13 @@ export const MemoizedSidebarGroup = memo(
|
|||
sortedCategories,
|
||||
dataFilter,
|
||||
nodeColors,
|
||||
chatInputAdded,
|
||||
onDragStart,
|
||||
sensitiveSort,
|
||||
openCategories,
|
||||
setOpenCategories,
|
||||
handleKeyDownInput,
|
||||
}: {
|
||||
BUNDLES: any;
|
||||
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
|
||||
uniqueInputsComponents,
|
||||
}: SidebarGroupProps) => {
|
||||
const sortedBundles = useMemo(() => {
|
||||
return BUNDLES.toSorted((a, b) => {
|
||||
const referenceArray = search !== "" ? sortedCategories : BUNDLES;
|
||||
|
|
@ -63,7 +51,7 @@ export const MemoizedSidebarGroup = memo(
|
|||
}}
|
||||
dataFilter={dataFilter}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
uniqueInputsComponents={uniqueInputsComponents}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
handleKeyDownInput={handleKeyDownInput}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ export const SidebarDraggableComponent = forwardRef(
|
|||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
className="m-[1px] rounded-md outline-none ring-ring focus-visible:ring-1"
|
||||
data-testid={`${sectionName.toLowerCase()}_${display_name.toLowerCase()}_draggable`}
|
||||
>
|
||||
<div
|
||||
data-testid={sectionName + display_name}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import { removeCountFromString } from "@/utils/utils";
|
||||
import { disableItem } from "../../helpers/disable-item";
|
||||
import { getDisabledTooltip } from "../../helpers/get-disabled-tooltip";
|
||||
import SidebarDraggableComponent from "../sidebarDraggableComponent";
|
||||
|
||||
const SidebarItemsList = ({
|
||||
item,
|
||||
dataFilter,
|
||||
nodeColors,
|
||||
chatInputAdded,
|
||||
uniqueInputsComponents,
|
||||
onDragStart,
|
||||
sensitiveSort,
|
||||
}) => {
|
||||
|
|
@ -46,8 +48,11 @@ const SidebarItemsList = ({
|
|||
official={currentItem.official === false ? false : true}
|
||||
beta={currentItem.beta ?? false}
|
||||
legacy={currentItem.legacy ?? false}
|
||||
disabled={SBItemName === "ChatInput" && chatInputAdded}
|
||||
disabledTooltip="Chat input already added"
|
||||
disabled={disableItem(SBItemName, uniqueInputsComponents)}
|
||||
disabledTooltip={getDisabledTooltip(
|
||||
SBItemName,
|
||||
uniqueInputsComponents,
|
||||
)}
|
||||
/>
|
||||
</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 { useShortcutsStore } from "@/stores/shortcuts";
|
||||
import { useStoreStore } from "@/stores/storeStore";
|
||||
import { checkChatInput } from "@/utils/reactflowUtils";
|
||||
import { checkChatInput, checkWebhookInput } from "@/utils/reactflowUtils";
|
||||
import {
|
||||
nodeColors,
|
||||
SIDEBAR_BUNDLES,
|
||||
|
|
@ -36,16 +36,18 @@ import { combinedResultsFn } from "./helpers/combined-results";
|
|||
import { filteredDataFn } from "./helpers/filtered-data";
|
||||
import { normalizeString } from "./helpers/normalize-string";
|
||||
import { traditionalSearchMetadata } from "./helpers/traditional-search-metadata";
|
||||
import { UniqueInputsComponents } from "./types";
|
||||
|
||||
const CATEGORIES = SIDEBAR_CATEGORIES;
|
||||
const BUNDLES = SIDEBAR_BUNDLES;
|
||||
|
||||
interface FlowSidebarComponentProps {
|
||||
isLoading?: boolean;
|
||||
showLegacy: boolean;
|
||||
setShowLegacy: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
||||
export function FlowSidebarComponent({ isLoading }: FlowSidebarComponentProps) {
|
||||
const { data, templates } = useTypesStore(
|
||||
useCallback(
|
||||
(state) => ({
|
||||
|
|
@ -86,6 +88,13 @@ export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
|||
const searchInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
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(() => {
|
||||
return data?.["custom_component"]?.["CustomComponent"] ?? null;
|
||||
|
|
@ -347,9 +356,9 @@ export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
|||
setOpenCategories={setOpenCategories}
|
||||
search={search}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
uniqueInputsComponents={uniqueInputsComponents}
|
||||
/>
|
||||
|
||||
{hasBundleItems && (
|
||||
|
|
@ -359,12 +368,12 @@ export function FlowSidebarComponent({ isLoading }: { isLoading?: boolean }) {
|
|||
sortedCategories={sortedCategories}
|
||||
dataFilter={dataFilter}
|
||||
nodeColors={nodeColors}
|
||||
chatInputAdded={chatInputAdded}
|
||||
onDragStart={onDragStart}
|
||||
sensitiveSort={sensitiveSort}
|
||||
openCategories={openCategories}
|
||||
setOpenCategories={setOpenCategories}
|
||||
handleKeyDownInput={handleKeyDownInput}
|
||||
uniqueInputsComponents={uniqueInputsComponents}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { APIClassType, APIDataType } from "@/types/api";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
export interface NodeColors {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface CategoryGroupProps {
|
||||
dataFilter: APIDataType;
|
||||
sortedCategories: string[];
|
||||
|
|
@ -12,15 +16,57 @@ export interface CategoryGroupProps {
|
|||
openCategories: string[];
|
||||
setOpenCategories: (categories: string[]) => void;
|
||||
search: string;
|
||||
nodeColors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
chatInputAdded: boolean;
|
||||
nodeColors: NodeColors;
|
||||
onDragStart: (
|
||||
event: React.DragEvent<any>,
|
||||
data: { type: string; node?: APIClassType },
|
||||
) => void;
|
||||
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 {
|
||||
|
|
@ -50,3 +96,8 @@ export interface SidebarHeaderComponentProps {
|
|||
setFilterData: Dispatch<SetStateAction<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 { API_PAGE_PARAGRAPH } from "../../../../../../constants/constants";
|
||||
import SecretKeyModal from "../../../../../../modals/secretKeyModal";
|
||||
import { getModalPropsApiKey } from "../../helpers/get-modal-props";
|
||||
|
||||
type ApiKeyHeaderComponentProps = {
|
||||
selectedRows: string[];
|
||||
|
|
@ -13,6 +14,7 @@ const ApiKeyHeaderComponent = ({
|
|||
fetchApiKeys,
|
||||
userId,
|
||||
}: ApiKeyHeaderComponentProps) => {
|
||||
const modalProps = getModalPropsApiKey();
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
<ForwardedIconComponent name="Plus" className="w-4" />
|
||||
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";
|
||||
|
||||
export default function ApiKeysPage() {
|
||||
const [loadingKeys, setLoadingKeys] = useState(true);
|
||||
const [selectedRows, setSelectedRows] = useState<string[]>([]);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
|
|
|
|||
|
|
@ -35,4 +35,7 @@ export const useUtilityStore = create<UtilityStoreType>((set, get) => ({
|
|||
setTags: (tags: Tag[]) => set({ tags }),
|
||||
featureFlags: {},
|
||||
setFeatureFlags: (featureFlags: Record<string, any>) => set({ featureFlags }),
|
||||
webhookPollingInterval: 5000,
|
||||
setWebhookPollingInterval: (webhookPollingInterval: number) =>
|
||||
set({ webhookPollingInterval }),
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@
|
|||
@apply flex gap-2;
|
||||
}
|
||||
.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 {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export type APIClassType = {
|
|||
flow?: FlowType;
|
||||
field_order?: string[];
|
||||
tool_mode?: boolean;
|
||||
type?: string;
|
||||
[key: string]:
|
||||
| Array<string>
|
||||
| string
|
||||
|
|
|
|||
|
|
@ -421,7 +421,29 @@ export type UserInputType = {
|
|||
export type ApiKeyType = {
|
||||
children: ReactElement;
|
||||
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 = {
|
||||
|
|
@ -574,6 +596,7 @@ export type iconsType = {
|
|||
export type modalHeaderType = {
|
||||
children: ReactNode;
|
||||
description: string | JSX.Element | null;
|
||||
clampDescription?: number;
|
||||
};
|
||||
|
||||
export type codeAreaModalPropsType = {
|
||||
|
|
@ -640,6 +663,7 @@ export type textModalPropsType = {
|
|||
changeVisibility?: () => void;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
onCloseModal?: () => void;
|
||||
};
|
||||
|
||||
export type newFlowModalPropsType = {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export type GetCodeType = {
|
|||
flowId: string;
|
||||
flowName: string;
|
||||
isAuth: boolean;
|
||||
tweaksBuildedObject: {};
|
||||
tweaksBuildedObject?: {};
|
||||
endpointName?: string | null;
|
||||
activeTweaks: boolean;
|
||||
activeTweaks?: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export type UtilityStoreType = {
|
|||
setTags: (tags: Tag[]) => void;
|
||||
featureFlags: Record<string, any>;
|
||||
setFeatureFlags: (featureFlags: Record<string, any>) => void;
|
||||
webhookPollingInterval: number;
|
||||
setWebhookPollingInterval: (webhookPollingInterval: number) => void;
|
||||
chatValueStore: string;
|
||||
setChatValueStore: (value: string) => void;
|
||||
dismissAll: boolean;
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ export function checkChatInput(nodes: Node[]) {
|
|||
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[]) {
|
||||
let newEdges: EdgeType[] = cloneDeep(
|
||||
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", { exact: true }).nth(1).isVisible();
|
||||
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);
|
||||
|
||||
await page
|
||||
.getByPlaceholder("Insert a name for your API Key")
|
||||
.fill(randomName);
|
||||
await page.getByText("Create Secret Key", { exact: true }).click();
|
||||
await page.getByPlaceholder("My API Key").fill(randomName);
|
||||
await page.getByText("Generate API Key", { exact: true }).click();
|
||||
|
||||
// Wait for api key creation to complete and render the next form element
|
||||
await page.waitForTimeout(1000);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue