mirror of
https://github.com/langgenius/dify.git
synced 2026-05-12 07:37:09 +08:00
# Conflicts: # .vite-hooks/pre-commit # api/controllers/console/__init__.py # api/core/agent/base_agent_runner.py # api/core/app/app_config/easy_ui_based_app/model_config/converter.py # api/core/app/apps/agent_chat/app_runner.py # api/core/entities/provider_configuration.py # api/core/helper/moderation.py # api/core/model_manager.py # api/core/rag/embedding/cached_embedding.py # api/core/rag/retrieval/dataset_retrieval.py # api/core/rag/splitter/fixed_text_splitter.py # api/core/workflow/nodes/datasource/datasource_node.py # api/core/workflow/nodes/knowledge_index/knowledge_index_node.py # api/models/human_input.py # api/providers/trace/trace-tencent/src/dify_trace_tencent/span_builder.py # api/services/workflow_service.py # api/tasks/trigger_processing_tasks.py # api/tests/integration_tests/core/workflow/nodes/datasource/test_datasource_node_integration.py # api/tests/integration_tests/workflow/nodes/test_http.py # api/tests/integration_tests/workflow/nodes/test_parameter_extractor.py # api/tests/unit_tests/controllers/service_api/app/test_conversation.py # api/tests/unit_tests/core/prompt/test_agent_history_prompt_transform.py # api/tests/unit_tests/core/variables/test_segment.py # api/tests/unit_tests/core/workflow/graph_engine/test_mock_factory.py # api/tests/unit_tests/core/workflow/nodes/answer/test_answer.py # api/tests/unit_tests/core/workflow/nodes/datasource/test_datasource_node.py # api/tests/unit_tests/core/workflow/nodes/http_request/test_http_request_node.py # api/tests/unit_tests/core/workflow/nodes/human_input/test_email_delivery_config.py # api/tests/unit_tests/services/workflow/test_workflow_human_input_delivery.py # web/app/(commonLayout)/layout.tsx # web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx # web/app/components/app/configuration/debug/debug-with-multiple-model/debug-item.tsx # web/app/components/app/workflow-log/__tests__/list.spec.tsx # web/app/components/apps/__tests__/list.spec.tsx # web/app/components/apps/list.tsx # web/app/components/base/chat/chat-with-history/header/operation.tsx # web/app/components/base/chat/chat-with-history/sidebar/operation.tsx # web/app/components/header/account-setting/data-source-page-new/operator.tsx # web/app/components/header/account-setting/members-page/operation/index.tsx # web/app/components/plugins/marketplace/sort-dropdown/__tests__/index.spec.tsx # web/app/components/plugins/marketplace/sort-dropdown/index.tsx # web/app/components/plugins/plugin-page/plugin-tasks/index.tsx # web/app/components/workflow/header/__tests__/test-run-menu.spec.tsx # web/app/components/workflow/header/test-run-menu.tsx # web/app/components/workflow/nodes/_base/components/next-step/operator.tsx # web/app/components/workflow/nodes/_base/components/panel-operator/index.tsx # web/app/components/workflow/nodes/assigner/components/__tests__/operation-selector.spec.tsx # web/app/components/workflow/nodes/assigner/components/operation-selector.tsx # web/app/components/workflow/operator/__tests__/more-actions.spec.tsx # web/app/components/workflow/operator/zoom-in-out.tsx # web/app/components/workflow/panel/version-history-panel/context-menu/menu-item.tsx # web/app/components/workflow/selection-contextmenu.tsx # web/eslint-suppressions.json Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com>
257 lines
9.9 KiB
TypeScript
257 lines
9.9 KiB
TypeScript
import type { FC } from 'react'
|
|
import type { HttpMethod, WebhookTriggerNodeType } from './types'
|
|
import type { NodePanelProps } from '@/app/components/workflow/types'
|
|
import {
|
|
NumberField,
|
|
NumberFieldControls,
|
|
NumberFieldDecrement,
|
|
NumberFieldGroup,
|
|
NumberFieldIncrement,
|
|
NumberFieldInput,
|
|
} from '@langgenius/dify-ui/number-field'
|
|
|
|
import { toast } from '@langgenius/dify-ui/toast'
|
|
import copy from 'copy-to-clipboard'
|
|
import * as React from 'react'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import InputWithCopy from '@/app/components/base/input-with-copy'
|
|
import { SimpleSelect } from '@/app/components/base/select'
|
|
import Tooltip from '@/app/components/base/tooltip'
|
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
|
import OutputVars from '@/app/components/workflow/nodes/_base/components/output-vars'
|
|
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
|
import { isPrivateOrLocalAddress } from '@/utils/urlValidation'
|
|
import HeaderTable from './components/header-table'
|
|
import ParagraphInput from './components/paragraph-input'
|
|
import ParameterTable from './components/parameter-table'
|
|
import { DEFAULT_STATUS_CODE, MAX_STATUS_CODE, normalizeStatusCode, useConfig } from './use-config'
|
|
import { OutputVariablesContent } from './utils/render-output-vars'
|
|
|
|
const i18nPrefix = 'nodes.triggerWebhook'
|
|
|
|
const HTTP_METHODS = [
|
|
{ name: 'GET', value: 'GET' },
|
|
{ name: 'POST', value: 'POST' },
|
|
{ name: 'PUT', value: 'PUT' },
|
|
{ name: 'DELETE', value: 'DELETE' },
|
|
{ name: 'PATCH', value: 'PATCH' },
|
|
{ name: 'HEAD', value: 'HEAD' },
|
|
]
|
|
|
|
const CONTENT_TYPES = [
|
|
{ name: 'application/json', value: 'application/json' },
|
|
{ name: 'application/x-www-form-urlencoded', value: 'application/x-www-form-urlencoded' },
|
|
{ name: 'text/plain', value: 'text/plain' },
|
|
{ name: 'application/octet-stream', value: 'application/octet-stream' },
|
|
{ name: 'multipart/form-data', value: 'multipart/form-data' },
|
|
]
|
|
|
|
const Panel: FC<NodePanelProps<WebhookTriggerNodeType>> = ({
|
|
id,
|
|
data,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
const [debugUrlCopied, setDebugUrlCopied] = React.useState(false)
|
|
const [outputVarsCollapsed, setOutputVarsCollapsed] = useState(false)
|
|
const {
|
|
readOnly,
|
|
inputs,
|
|
handleMethodChange,
|
|
handleContentTypeChange,
|
|
handleHeadersChange,
|
|
handleParamsChange,
|
|
handleBodyChange,
|
|
handleStatusCodeChange,
|
|
handleResponseBodyChange,
|
|
generateWebhookUrl,
|
|
} = useConfig(id, data)
|
|
|
|
// Ensure we only attempt to generate URL once for a newly created node without url
|
|
const hasRequestedUrlRef = useRef(false)
|
|
useEffect(() => {
|
|
if (!readOnly && !inputs.webhook_url && !hasRequestedUrlRef.current) {
|
|
hasRequestedUrlRef.current = true
|
|
void generateWebhookUrl()
|
|
}
|
|
}, [readOnly, inputs.webhook_url, generateWebhookUrl])
|
|
|
|
return (
|
|
<div className="mt-2">
|
|
<div className="space-y-4 px-4 pb-3 pt-2">
|
|
{/* Webhook URL Section */}
|
|
<Field title={t(`${i18nPrefix}.webhookUrl`, { ns: 'workflow' })}>
|
|
<div className="space-y-1">
|
|
<div className="flex gap-1" style={{ height: '32px' }}>
|
|
<div className="w-26 shrink-0">
|
|
<SimpleSelect
|
|
key={`${id}-method-${inputs.method}`}
|
|
items={HTTP_METHODS}
|
|
defaultValue={inputs.method}
|
|
onSelect={item => handleMethodChange(item.value as HttpMethod)}
|
|
disabled={readOnly}
|
|
className="h-8 pr-8 text-sm"
|
|
wrapperClassName="h-8"
|
|
optionWrapClassName="w-26 min-w-26 z-5"
|
|
allowSearch={false}
|
|
notClearable={true}
|
|
/>
|
|
</div>
|
|
<div className="flex-1" style={{ width: '284px' }}>
|
|
<InputWithCopy
|
|
value={inputs.webhook_url || ''}
|
|
placeholder={t(`${i18nPrefix}.webhookUrlPlaceholder`, { ns: 'workflow' })}
|
|
readOnly
|
|
onCopy={() => {
|
|
toast.success(t(`${i18nPrefix}.urlCopied`, { ns: 'workflow' }))
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{inputs.webhook_debug_url && (
|
|
<div className="space-y-2">
|
|
<Tooltip
|
|
popupContent={debugUrlCopied ? t(`${i18nPrefix}.debugUrlCopied`, { ns: 'workflow' }) : t(`${i18nPrefix}.debugUrlCopy`, { ns: 'workflow' })}
|
|
popupClassName="system-xs-regular text-text-primary bg-components-tooltip-bg border border-components-panel-border shadow-lg backdrop-blur-xs rounded-md px-1.5 py-1"
|
|
position="top"
|
|
offset={{ mainAxis: -20 }}
|
|
needsDelay={true}
|
|
>
|
|
<div
|
|
className="flex cursor-pointer gap-1.5 rounded-lg px-1 py-1.5 transition-colors"
|
|
style={{ width: '368px', height: '38px' }}
|
|
onClick={() => {
|
|
copy(inputs.webhook_debug_url || '')
|
|
setDebugUrlCopied(true)
|
|
setTimeout(() => setDebugUrlCopied(false), 2000)
|
|
}}
|
|
>
|
|
<div className="mt-0.5 w-0.5 bg-divider-regular" style={{ height: '28px' }}></div>
|
|
<div className="flex-1" style={{ width: '352px', height: '32px' }}>
|
|
<div className="text-xs leading-4 text-text-tertiary">
|
|
{t(`${i18nPrefix}.debugUrlTitle`, { ns: 'workflow' })}
|
|
</div>
|
|
<div className="truncate text-xs leading-4 text-text-primary">
|
|
{inputs.webhook_debug_url}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Tooltip>
|
|
{isPrivateOrLocalAddress(inputs.webhook_debug_url) && (
|
|
<div className="mt-1 px-0 py-[2px] text-text-warning system-xs-regular">
|
|
{t(`${i18nPrefix}.debugUrlPrivateAddressWarning`, { ns: 'workflow' })}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Field>
|
|
|
|
{/* Content Type */}
|
|
<Field title={t(`${i18nPrefix}.contentType`, { ns: 'workflow' })}>
|
|
<div className="w-full">
|
|
<SimpleSelect
|
|
key={`${id}-content-type-${inputs.content_type}`}
|
|
items={CONTENT_TYPES}
|
|
defaultValue={inputs.content_type}
|
|
onSelect={item => handleContentTypeChange(item.value as string)}
|
|
disabled={readOnly}
|
|
className="h-8 text-sm"
|
|
wrapperClassName="h-8"
|
|
optionWrapClassName="min-w-48 z-5"
|
|
allowSearch={false}
|
|
notClearable={true}
|
|
/>
|
|
</div>
|
|
</Field>
|
|
|
|
{/* Query Parameters */}
|
|
<ParameterTable
|
|
readonly={readOnly}
|
|
title="Query Parameters"
|
|
parameters={inputs.params}
|
|
onChange={handleParamsChange}
|
|
placeholder={t(`${i18nPrefix}.noQueryParameters`, { ns: 'workflow' })}
|
|
/>
|
|
|
|
{/* Header Parameters */}
|
|
<HeaderTable
|
|
readonly={readOnly}
|
|
headers={inputs.headers}
|
|
onChange={handleHeadersChange}
|
|
/>
|
|
|
|
{/* Request Body Parameters */}
|
|
<ParameterTable
|
|
readonly={readOnly}
|
|
title="Request Body Parameters"
|
|
parameters={inputs.body}
|
|
onChange={handleBodyChange}
|
|
placeholder={t(`${i18nPrefix}.noBodyParameters`, { ns: 'workflow' })}
|
|
contentType={inputs.content_type}
|
|
/>
|
|
|
|
<Split />
|
|
|
|
{/* Response Configuration */}
|
|
<Field title={t(`${i18nPrefix}.responseConfiguration`, { ns: 'workflow' })}>
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-text-tertiary system-sm-medium">
|
|
{t(`${i18nPrefix}.statusCode`, { ns: 'workflow' })}
|
|
</label>
|
|
<NumberField
|
|
className="w-[120px]"
|
|
min={DEFAULT_STATUS_CODE}
|
|
max={MAX_STATUS_CODE}
|
|
value={inputs.status_code ?? DEFAULT_STATUS_CODE}
|
|
disabled={readOnly}
|
|
onValueChange={value => value !== null && handleStatusCodeChange(value)}
|
|
onValueCommitted={(value, eventDetails) => {
|
|
if (eventDetails.reason === 'input-blur' || eventDetails.reason === 'input-clear')
|
|
handleStatusCodeChange(normalizeStatusCode(value ?? DEFAULT_STATUS_CODE))
|
|
}}
|
|
>
|
|
<NumberFieldGroup size="regular">
|
|
<NumberFieldInput
|
|
size="regular"
|
|
className="h-8"
|
|
/>
|
|
<NumberFieldControls>
|
|
<NumberFieldIncrement size="regular" />
|
|
<NumberFieldDecrement size="regular" />
|
|
</NumberFieldControls>
|
|
</NumberFieldGroup>
|
|
</NumberField>
|
|
</div>
|
|
<div>
|
|
<label className="mb-2 block text-text-tertiary system-sm-medium">
|
|
{t(`${i18nPrefix}.responseBody`, { ns: 'workflow' })}
|
|
</label>
|
|
<ParagraphInput
|
|
value={inputs.response_body}
|
|
onChange={handleResponseBodyChange}
|
|
placeholder={t(`${i18nPrefix}.responseBodyPlaceholder`, { ns: 'workflow' })}
|
|
disabled={readOnly}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Field>
|
|
</div>
|
|
|
|
<Split />
|
|
|
|
<div className="">
|
|
<OutputVars
|
|
collapsed={outputVarsCollapsed}
|
|
onCollapse={setOutputVarsCollapsed}
|
|
>
|
|
<OutputVariablesContent variables={inputs.variables} />
|
|
</OutputVars>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Panel
|