mirror of https://github.com/langgenius/dify.git
feat/trigger plugin apikey (#25388)
This commit is contained in:
parent
1c8850fc95
commit
b433322e8d
|
|
@ -7,6 +7,7 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi
|
|||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
import { useFetchDynamicOptions } from '@/service/use-plugins'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import FormInputTypeSwitch from './form-input-type-switch'
|
||||
|
|
@ -50,8 +51,8 @@ const FormInputItem: FC<Props> = ({
|
|||
providerType,
|
||||
}) => {
|
||||
const language = useLanguage()
|
||||
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
|
||||
const [isLoadingOptions, setIsLoadingOptions] = useState(false)
|
||||
const [toolsOptions, setToolsOptions] = useState<FormOption[] | null>(null)
|
||||
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(false)
|
||||
|
||||
const {
|
||||
placeholder,
|
||||
|
|
@ -136,7 +137,7 @@ const FormInputItem: FC<Props> = ({
|
|||
return VarKindType.mixed
|
||||
}
|
||||
|
||||
// Fetch dynamic options hook
|
||||
// Fetch dynamic options hook for tools
|
||||
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
|
||||
currentProvider?.plugin_id || '',
|
||||
currentProvider?.name || '',
|
||||
|
|
@ -146,27 +147,48 @@ const FormInputItem: FC<Props> = ({
|
|||
extraParams,
|
||||
)
|
||||
|
||||
// Fetch dynamic options when component mounts or dependencies change
|
||||
// Fetch dynamic options hook for triggers
|
||||
const { data: triggerDynamicOptions, isLoading: isTriggerOptionsLoading } = useTriggerPluginDynamicOptions({
|
||||
plugin_id: currentProvider?.plugin_id || '',
|
||||
provider: currentProvider?.name || '',
|
||||
action: currentResource?.name || '',
|
||||
parameter: variable || '',
|
||||
extra: extraParams,
|
||||
}, isDynamicSelect && providerType === 'trigger' && !!currentResource && !!currentProvider)
|
||||
|
||||
// Computed values for dynamic options (unified for triggers and tools)
|
||||
const dynamicOptions = providerType === 'trigger' ? triggerDynamicOptions?.options || [] : toolsOptions
|
||||
const isLoadingOptions = providerType === 'trigger' ? isTriggerOptionsLoading : isLoadingToolsOptions
|
||||
|
||||
// Fetch dynamic options for tools only (triggers use hook directly)
|
||||
useEffect(() => {
|
||||
const fetchOptions = async () => {
|
||||
if (isDynamicSelect && currentResource && currentProvider) {
|
||||
setIsLoadingOptions(true)
|
||||
const fetchToolOptions = async () => {
|
||||
if (isDynamicSelect && currentResource && currentProvider && providerType === 'tool') {
|
||||
setIsLoadingToolsOptions(true)
|
||||
try {
|
||||
const data = await fetchDynamicOptions()
|
||||
setDynamicOptions(data?.options || [])
|
||||
setToolsOptions(data?.options || [])
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch dynamic options:', error)
|
||||
setDynamicOptions([])
|
||||
setToolsOptions([])
|
||||
}
|
||||
finally {
|
||||
setIsLoadingOptions(false)
|
||||
setIsLoadingToolsOptions(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetchOptions()
|
||||
}, [isDynamicSelect, currentResource?.name, currentProvider?.name, variable, extraParams])
|
||||
fetchToolOptions()
|
||||
}, [
|
||||
isDynamicSelect,
|
||||
currentResource?.name,
|
||||
currentProvider?.name,
|
||||
variable,
|
||||
extraParams,
|
||||
providerType,
|
||||
fetchDynamicOptions,
|
||||
])
|
||||
|
||||
const handleTypeChange = (newType: string) => {
|
||||
if (newType === VarKindType.variable) {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workfl
|
|||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import Tab, { TabType } from './tab'
|
||||
import { useAllTriggerPlugins, useTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import AuthMethodSelector from '@/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector'
|
||||
import LastRun from './last-run'
|
||||
import useLastRun from './last-run/use-last-run'
|
||||
|
|
@ -238,23 +238,8 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
|
||||
}, [buildInTools, data.provider_id])
|
||||
|
||||
// For trigger plugins, check if they have existing subscriptions (authenticated)
|
||||
const triggerProvider = useMemo(() => {
|
||||
if (data.type === BlockEnum.TriggerPlugin) {
|
||||
if (data.provider_name)
|
||||
return data.provider_name
|
||||
return data.provider_id || ''
|
||||
}
|
||||
return ''
|
||||
}, [data.type, data.provider_id, data.provider_name])
|
||||
|
||||
const { data: triggerSubscriptions = [] } = useTriggerSubscriptions(
|
||||
triggerProvider,
|
||||
data.type === BlockEnum.TriggerPlugin && !!triggerProvider,
|
||||
)
|
||||
|
||||
// For trigger plugins, get basic provider info
|
||||
const { data: triggerProviders = [] } = useAllTriggerPlugins()
|
||||
|
||||
const currentTriggerProvider = useMemo(() => {
|
||||
if (data.type !== BlockEnum.TriggerPlugin || !data.provider_id || !data.provider_name)
|
||||
return undefined
|
||||
|
|
@ -271,26 +256,20 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
return methods
|
||||
}, [currentTriggerProvider])
|
||||
|
||||
const isTriggerAuthenticated = useMemo(() => {
|
||||
if (data.type !== BlockEnum.TriggerPlugin) return true
|
||||
if (!triggerSubscriptions.length) return false
|
||||
// Simplified: Always show auth selector for trigger plugins
|
||||
const shouldShowTriggerAuthSelector = useMemo(() => {
|
||||
return data.type === BlockEnum.TriggerPlugin && currentTriggerProvider && supportedAuthMethods.length > 0
|
||||
}, [data.type, currentTriggerProvider, supportedAuthMethods.length])
|
||||
|
||||
const subscription = triggerSubscriptions[0]
|
||||
return subscription.credential_type !== 'unauthorized'
|
||||
}, [data.type, triggerSubscriptions])
|
||||
// Simplified: Always show tab for trigger plugins
|
||||
const shouldShowTriggerTab = useMemo(() => {
|
||||
return data.type === BlockEnum.TriggerPlugin && currentTriggerProvider
|
||||
}, [data.type, currentTriggerProvider])
|
||||
|
||||
const shouldShowAuthSelector = useMemo(() => {
|
||||
return data.type === BlockEnum.TriggerPlugin
|
||||
&& !isTriggerAuthenticated
|
||||
&& supportedAuthMethods.length > 0
|
||||
&& !!currentTriggerProvider
|
||||
}, [data.type, isTriggerAuthenticated, supportedAuthMethods.length, currentTriggerProvider])
|
||||
|
||||
// Unified check for any node that needs authentication UI
|
||||
const needsAuth = useMemo(() => {
|
||||
// Unified check for tool authentication UI
|
||||
const needsToolAuth = useMemo(() => {
|
||||
return (data.type === BlockEnum.Tool && currCollection?.allow_delete)
|
||||
|| (data.type === BlockEnum.TriggerPlugin && isTriggerAuthenticated)
|
||||
}, [data.type, currCollection?.allow_delete, isTriggerAuthenticated])
|
||||
}, [data.type, currCollection?.allow_delete])
|
||||
|
||||
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
|
||||
handleNodeDataUpdateWithSyncDraft({
|
||||
|
|
@ -433,7 +412,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
/>
|
||||
</div>
|
||||
{
|
||||
needsAuth && data.type === BlockEnum.Tool && currCollection?.allow_delete && (
|
||||
needsToolAuth && (
|
||||
<PluginAuth
|
||||
className='px-4 pb-2'
|
||||
pluginPayload={{
|
||||
|
|
@ -455,7 +434,15 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
)
|
||||
}
|
||||
{
|
||||
needsAuth && data.type !== BlockEnum.Tool && (
|
||||
shouldShowTriggerAuthSelector && (
|
||||
<AuthMethodSelector
|
||||
provider={currentTriggerProvider!}
|
||||
supportedMethods={supportedAuthMethods}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
shouldShowTriggerTab && (
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
<Tab
|
||||
value={tabType}
|
||||
|
|
@ -469,15 +456,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
)
|
||||
}
|
||||
{
|
||||
shouldShowAuthSelector && (
|
||||
<AuthMethodSelector
|
||||
provider={currentTriggerProvider!}
|
||||
supportedMethods={supportedAuthMethods}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!needsAuth && data.type !== BlockEnum.TriggerPlugin && (
|
||||
!needsToolAuth && data.type !== BlockEnum.TriggerPlugin && (
|
||||
<div className='flex items-center justify-between pl-4 pr-3'>
|
||||
<Tab
|
||||
value={tabType}
|
||||
|
|
|
|||
|
|
@ -11,15 +11,11 @@ import Loading from '@/app/components/base/loading'
|
|||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useInvalidateTriggerSubscriptions,
|
||||
useUpdateTriggerSubscriptionBuilder,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import { useInvalidateTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { findMissingRequiredField, sanitizeFormValues } from '../utils/form-helpers'
|
||||
import { useTriggerAuthFlow } from '../hooks/use-trigger-auth-flow'
|
||||
import ParametersForm from './parameters-form'
|
||||
|
||||
type ApiKeyConfigModalProps = {
|
||||
provider: TriggerWithProvider
|
||||
|
|
@ -35,29 +31,47 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
|
|||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const language = useLanguage()
|
||||
const [credentialSchema, setCredentialSchema] = useState<any[]>([])
|
||||
const [tempCredential, setTempCredential] = useState<Record<string, any>>({})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [subscriptionBuilderId, setSubscriptionBuilderId] = useState<string>('')
|
||||
|
||||
const createBuilder = useCreateTriggerSubscriptionBuilder()
|
||||
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
|
||||
const verifyBuilder = useVerifyTriggerSubscriptionBuilder()
|
||||
const buildSubscription = useBuildTriggerSubscription()
|
||||
const invalidateSubscriptions = useInvalidateTriggerSubscriptions()
|
||||
|
||||
const [credentialSchema, setCredentialSchema] = useState<any[]>([])
|
||||
const [credentials, setCredentials] = useState<Record<string, any>>({})
|
||||
const [parameters, setParameters] = useState<Record<string, any>>({})
|
||||
const [properties, setProperties] = useState<Record<string, any>>({})
|
||||
const [subscriptionName, setSubscriptionName] = useState<string>('')
|
||||
|
||||
const {
|
||||
step,
|
||||
builderId,
|
||||
isLoading,
|
||||
startAuth,
|
||||
verifyAuth,
|
||||
completeConfig,
|
||||
reset,
|
||||
} = useTriggerAuthFlow(provider)
|
||||
|
||||
useEffect(() => {
|
||||
if (provider.credentials_schema) {
|
||||
const schemas = toolCredentialToFormSchemas(provider.credentials_schema as any)
|
||||
setCredentialSchema(schemas)
|
||||
const defaultCredentials = addDefaultValue({}, schemas)
|
||||
// Use utility function for consistent data sanitization
|
||||
setTempCredential(sanitizeFormValues(defaultCredentials))
|
||||
setCredentials(sanitizeFormValues(defaultCredentials))
|
||||
}
|
||||
}, [provider.credentials_schema])
|
||||
|
||||
const handleSave = async () => {
|
||||
// Validate required fields using utility function
|
||||
useEffect(() => {
|
||||
startAuth().catch((err) => {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('workflow.nodes.triggerPlugin.failedToStart', { error: err.message }),
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
reset()
|
||||
}
|
||||
}, []) // Remove dependencies to run only once on mount
|
||||
|
||||
const handleCredentialsSubmit = async () => {
|
||||
const requiredFields = credentialSchema
|
||||
.filter(field => field.required)
|
||||
.map(field => ({
|
||||
|
|
@ -65,7 +79,7 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
|
|||
label: field.label[language] || field.label.en_US,
|
||||
}))
|
||||
|
||||
const missingField = findMissingRequiredField(tempCredential, requiredFields)
|
||||
const missingField = findMissingRequiredField(credentials, requiredFields)
|
||||
if (missingField) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
|
|
@ -76,56 +90,156 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
|
|||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
await verifyAuth(credentials)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('workflow.nodes.triggerPlugin.credentialsVerified'),
|
||||
})
|
||||
}
|
||||
catch (err: any) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('workflow.nodes.triggerPlugin.credentialVerificationFailed', {
|
||||
error: err.message,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleFinalSubmit = async () => {
|
||||
if (!subscriptionName.trim()) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('workflow.nodes.triggerPlugin.subscriptionNameRequired'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 1: Create subscription builder
|
||||
let builderId = subscriptionBuilderId
|
||||
if (!builderId) {
|
||||
const createResponse = await createBuilder.mutateAsync({
|
||||
provider: provider.name,
|
||||
credentials: tempCredential,
|
||||
})
|
||||
builderId = createResponse.subscription_builder.id
|
||||
setSubscriptionBuilderId(builderId)
|
||||
}
|
||||
else {
|
||||
// Update existing builder
|
||||
await updateBuilder.mutateAsync({
|
||||
provider: provider.name,
|
||||
subscriptionBuilderId: builderId,
|
||||
credentials: tempCredential,
|
||||
})
|
||||
}
|
||||
await completeConfig(parameters, properties, subscriptionName)
|
||||
|
||||
// Step 2: Verify credentials
|
||||
await verifyBuilder.mutateAsync({
|
||||
provider: provider.name,
|
||||
subscriptionBuilderId: builderId,
|
||||
})
|
||||
|
||||
// Step 3: Build final subscription
|
||||
await buildSubscription.mutateAsync({
|
||||
provider: provider.name,
|
||||
subscriptionBuilderId: builderId,
|
||||
})
|
||||
|
||||
// Step 4: Invalidate and notify success
|
||||
invalidateSubscriptions(provider.name)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('workflow.nodes.triggerPlugin.apiKeyConfigured'),
|
||||
message: t('workflow.nodes.triggerPlugin.configurationComplete'),
|
||||
})
|
||||
onSuccess()
|
||||
}
|
||||
catch (error: any) {
|
||||
catch (err: any) {
|
||||
notify({
|
||||
type: 'error',
|
||||
message: t('workflow.nodes.triggerPlugin.configurationFailed', { error: error.message }),
|
||||
message: t('workflow.nodes.triggerPlugin.configurationFailed', { error: err.message }),
|
||||
})
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const getTitle = () => {
|
||||
switch (step) {
|
||||
case 'auth':
|
||||
return t('workflow.nodes.triggerPlugin.configureApiKey')
|
||||
case 'params':
|
||||
return t('workflow.nodes.triggerPlugin.configureParameters')
|
||||
case 'complete':
|
||||
return t('workflow.nodes.triggerPlugin.configurationComplete')
|
||||
default:
|
||||
return t('workflow.nodes.triggerPlugin.configureApiKey')
|
||||
}
|
||||
}
|
||||
|
||||
const getDescription = () => {
|
||||
switch (step) {
|
||||
case 'auth':
|
||||
return t('workflow.nodes.triggerPlugin.apiKeyDescription')
|
||||
case 'params':
|
||||
return t('workflow.nodes.triggerPlugin.parametersDescription')
|
||||
case 'complete':
|
||||
return t('workflow.nodes.triggerPlugin.configurationCompleteDescription')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (credentialSchema.length === 0 && step === 'auth')
|
||||
return <Loading type='app' />
|
||||
|
||||
switch (step) {
|
||||
case 'auth':
|
||||
return (
|
||||
<>
|
||||
<Form
|
||||
value={credentials}
|
||||
onChange={setCredentials}
|
||||
formSchemas={credentialSchema}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='!bg-components-input-bg-normal'
|
||||
fieldMoreInfo={item => item.url ? (
|
||||
<a
|
||||
href={item.url}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-text-accent'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<LinkExternal02 className='ml-1 h-3 w-3' />
|
||||
</a>
|
||||
) : null}
|
||||
/>
|
||||
<div className='mt-4 flex justify-end space-x-2'>
|
||||
<Button onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
variant='primary'
|
||||
onClick={handleCredentialsSubmit}
|
||||
>
|
||||
{t('workflow.nodes.triggerPlugin.verifyAndContinue')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
case 'params':
|
||||
return (
|
||||
<ParametersForm
|
||||
provider={provider}
|
||||
builderId={builderId}
|
||||
parametersValue={parameters}
|
||||
propertiesValue={properties}
|
||||
subscriptionName={subscriptionName}
|
||||
onParametersChange={setParameters}
|
||||
onPropertiesChange={setProperties}
|
||||
onSubscriptionNameChange={setSubscriptionName}
|
||||
onSubmit={handleFinalSubmit}
|
||||
onCancel={onCancel}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'complete':
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<div className="bg-background-success-emphasis mb-4 flex h-12 w-12 items-center justify-center rounded-full">
|
||||
<svg className="h-6 w-6 text-text-success" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="mb-4 text-center text-text-primary">
|
||||
{t('workflow.nodes.triggerPlugin.configurationCompleteMessage')}
|
||||
</p>
|
||||
<Button variant="primary" onClick={onSuccess}>
|
||||
{t('common.operation.done')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,8 +247,8 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
|
|||
<Drawer
|
||||
isShow
|
||||
onHide={onCancel}
|
||||
title={t('workflow.nodes.triggerPlugin.configureApiKey')}
|
||||
titleDescription={t('workflow.nodes.triggerPlugin.apiKeyDescription')}
|
||||
title={getTitle()}
|
||||
titleDescription={getDescription()}
|
||||
panelClassName='mt-[64px] mb-2 !w-[420px] border-components-panel-border'
|
||||
maxWidthClassName='!max-w-[420px]'
|
||||
height='calc(100vh - 64px)'
|
||||
|
|
@ -142,45 +256,7 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
|
|||
headerClassName='!border-b-divider-subtle'
|
||||
body={
|
||||
<div className='h-full px-6 py-3'>
|
||||
{credentialSchema.length === 0 ? (
|
||||
<Loading type='app' />
|
||||
) : (
|
||||
<>
|
||||
<Form
|
||||
value={tempCredential}
|
||||
onChange={setTempCredential}
|
||||
formSchemas={credentialSchema}
|
||||
isEditMode={true}
|
||||
showOnVariableMap={{}}
|
||||
validating={false}
|
||||
inputClassName='!bg-components-input-bg-normal'
|
||||
fieldMoreInfo={item => item.url ? (
|
||||
<a
|
||||
href={item.url}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='inline-flex items-center text-xs text-text-accent'
|
||||
>
|
||||
{t('tools.howToGet')}
|
||||
<LinkExternal02 className='ml-1 h-3 w-3' />
|
||||
</a>
|
||||
) : null}
|
||||
/>
|
||||
<div className='mt-4 flex justify-end space-x-2'>
|
||||
<Button onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
variant='primary'
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
}
|
||||
isShowMask={true}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,171 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import type { Trigger } from '@/app/components/tools/types'
|
||||
import { toolCredentialToFormSchemas, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||
import TriggerForm from './trigger-form'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
|
||||
type ParametersFormProps = {
|
||||
provider: TriggerWithProvider
|
||||
trigger?: Trigger
|
||||
builderId: string
|
||||
parametersValue: Record<string, any>
|
||||
propertiesValue: Record<string, any>
|
||||
subscriptionName: string
|
||||
onParametersChange: (value: Record<string, any>) => void
|
||||
onPropertiesChange: (value: Record<string, any>) => void
|
||||
onSubscriptionNameChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
onCancel: () => void
|
||||
isLoading?: boolean
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
const ParametersForm: FC<ParametersFormProps> = ({
|
||||
provider,
|
||||
trigger,
|
||||
builderId,
|
||||
parametersValue,
|
||||
propertiesValue,
|
||||
subscriptionName,
|
||||
onParametersChange,
|
||||
onPropertiesChange,
|
||||
onSubscriptionNameChange,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
isLoading = false,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Use the first trigger if no specific trigger is provided
|
||||
// This is needed for dynamic options API which requires a trigger action
|
||||
const currentTrigger = trigger || provider.triggers?.[0]
|
||||
|
||||
const parametersSchema = useMemo(() => {
|
||||
if (!provider.subscription_schema?.parameters_schema) return []
|
||||
return toolParametersToFormSchemas(provider.subscription_schema.parameters_schema as any)
|
||||
}, [provider.subscription_schema?.parameters_schema])
|
||||
|
||||
const propertiesSchema = useMemo(() => {
|
||||
if (!provider.subscription_schema?.properties_schema) return []
|
||||
return toolCredentialToFormSchemas(provider.subscription_schema.properties_schema as any)
|
||||
}, [provider.subscription_schema?.properties_schema])
|
||||
|
||||
const hasParameters = parametersSchema.length > 0
|
||||
const hasProperties = propertiesSchema.length > 0
|
||||
|
||||
if (!hasParameters && !hasProperties) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8">
|
||||
<p className="mb-4 text-text-tertiary">
|
||||
{t('workflow.nodes.triggerPlugin.noConfigurationRequired')}
|
||||
</p>
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSubmit}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Subscription Name Section */}
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-primary">
|
||||
{t('workflow.nodes.triggerPlugin.subscriptionName')}
|
||||
</h3>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{t('workflow.nodes.triggerPlugin.subscriptionNameDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => onSubscriptionNameChange(e.target.value)}
|
||||
placeholder={t('workflow.nodes.triggerPlugin.subscriptionNamePlaceholder')}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Parameters Section */}
|
||||
{hasParameters && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-primary">
|
||||
{t('workflow.nodes.triggerPlugin.parameters')}
|
||||
</h3>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{t('workflow.nodes.triggerPlugin.parametersDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<TriggerForm
|
||||
readOnly={readOnly}
|
||||
nodeId=""
|
||||
schema={parametersSchema as any}
|
||||
value={parametersValue}
|
||||
onChange={onParametersChange}
|
||||
currentTrigger={currentTrigger}
|
||||
currentProvider={provider}
|
||||
extraParams={{ subscription_builder_id: builderId }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Properties Section */}
|
||||
{hasProperties && (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-text-primary">
|
||||
{t('workflow.nodes.triggerPlugin.properties')}
|
||||
</h3>
|
||||
<p className="text-xs text-text-tertiary">
|
||||
{t('workflow.nodes.triggerPlugin.propertiesDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<TriggerForm
|
||||
readOnly={readOnly}
|
||||
nodeId=""
|
||||
schema={propertiesSchema as any}
|
||||
value={propertiesValue}
|
||||
onChange={onPropertiesChange}
|
||||
currentTrigger={currentTrigger}
|
||||
currentProvider={provider}
|
||||
extraParams={{ subscription_builder_id: builderId }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-end space-x-2 border-t border-divider-subtle pt-4">
|
||||
<Button onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onSubmit}
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ParametersForm
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Trigger } from '@/app/components/tools/types'
|
||||
import type { FC } from 'react'
|
||||
import type { PluginTriggerVarInputs } from '../types'
|
||||
import type { PluginTriggerVarInputs } from '../../types'
|
||||
import TriggerFormItem from './item'
|
||||
import type { TriggerWithProvider } from '../../../block-selector/types'
|
||||
import type { TriggerWithProvider } from '../../../../block-selector/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
|
|
@ -3,7 +3,7 @@ import type { FC } from 'react'
|
|||
import {
|
||||
RiBracesLine,
|
||||
} from '@remixicon/react'
|
||||
import type { PluginTriggerVarInputs } from '../types'
|
||||
import type { PluginTriggerVarInputs } from '../../types'
|
||||
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
|
|
@ -13,7 +13,7 @@ import FormInputItem from '@/app/components/workflow/nodes/_base/components/form
|
|||
import { useBoolean } from 'ahooks'
|
||||
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
|
||||
import type { Trigger } from '@/app/components/tools/types'
|
||||
import type { TriggerWithProvider } from '../../../block-selector/types'
|
||||
import type { TriggerWithProvider } from '../../../../block-selector/types'
|
||||
|
||||
type Props = {
|
||||
readOnly: boolean
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useUpdateTriggerSubscriptionBuilder,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
// Helper function to serialize complex values to strings for backend encryption
|
||||
const serializeFormValues = (values: Record<string, any>): Record<string, string> => {
|
||||
const result: Record<string, string> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (value === null || value === undefined)
|
||||
result[key] = ''
|
||||
else if (typeof value === 'object')
|
||||
result[key] = JSON.stringify(value)
|
||||
else
|
||||
result[key] = String(value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export type AuthFlowStep = 'auth' | 'params' | 'complete'
|
||||
|
||||
export type AuthFlowState = {
|
||||
step: AuthFlowStep
|
||||
builderId: string
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export type AuthFlowActions = {
|
||||
startAuth: () => Promise<void>
|
||||
verifyAuth: (credentials: Record<string, any>) => Promise<void>
|
||||
completeConfig: (parameters: Record<string, any>, properties?: Record<string, any>, name?: string) => Promise<void>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState & AuthFlowActions => {
|
||||
const [step, setStep] = useState<AuthFlowStep>('auth')
|
||||
const [builderId, setBuilderId] = useState<string>('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const createBuilder = useCreateTriggerSubscriptionBuilder()
|
||||
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
|
||||
const verifyBuilder = useVerifyTriggerSubscriptionBuilder()
|
||||
const buildSubscription = useBuildTriggerSubscription()
|
||||
|
||||
const startAuth = useCallback(async () => {
|
||||
if (builderId) return // Prevent multiple calls if already started
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await createBuilder.mutateAsync({
|
||||
provider: provider.name,
|
||||
})
|
||||
setBuilderId(response.subscription_builder.id)
|
||||
setStep('auth')
|
||||
}
|
||||
catch (err: any) {
|
||||
setError(err.message || 'Failed to start authentication flow')
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [provider.name, createBuilder, builderId])
|
||||
|
||||
const verifyAuth = useCallback(async (credentials: Record<string, any>) => {
|
||||
if (!builderId) {
|
||||
setError('No builder ID available')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await updateBuilder.mutateAsync({
|
||||
provider: provider.name,
|
||||
subscriptionBuilderId: builderId,
|
||||
credentials: serializeFormValues(credentials),
|
||||
})
|
||||
|
||||
await verifyBuilder.mutateAsync({
|
||||
provider: provider.name,
|
||||
subscriptionBuilderId: builderId,
|
||||
})
|
||||
|
||||
setStep('params')
|
||||
}
|
||||
catch (err: any) {
|
||||
setError(err.message || 'Authentication verification failed')
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [provider.name, builderId, updateBuilder, verifyBuilder])
|
||||
|
||||
const completeConfig = useCallback(async (
|
||||
parameters: Record<string, any>,
|
||||
properties: Record<string, any> = {},
|
||||
name?: string,
|
||||
) => {
|
||||
if (!builderId) {
|
||||
setError('No builder ID available')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
await updateBuilder.mutateAsync({
|
||||
provider: provider.name,
|
||||
subscriptionBuilderId: builderId,
|
||||
parameters: serializeFormValues(parameters),
|
||||
properties: serializeFormValues(properties),
|
||||
name,
|
||||
})
|
||||
|
||||
await buildSubscription.mutateAsync({
|
||||
provider: provider.name,
|
||||
subscriptionBuilderId: builderId,
|
||||
})
|
||||
|
||||
setStep('complete')
|
||||
}
|
||||
catch (err: any) {
|
||||
setError(err.message || 'Configuration failed')
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [provider.name, builderId, updateBuilder, buildSubscription])
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setStep('auth')
|
||||
setBuilderId('')
|
||||
setIsLoading(false)
|
||||
setError(null)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
step,
|
||||
builderId,
|
||||
isLoading,
|
||||
error,
|
||||
startAuth,
|
||||
verifyAuth,
|
||||
completeConfig,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import Split from '@/app/components/workflow/nodes/_base/components/split'
|
|||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import useConfig from './use-config'
|
||||
import TriggerForm from './trigger-form'
|
||||
import TriggerForm from './components/trigger-form'
|
||||
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
|
||||
import { Type } from '../llm/types'
|
||||
|
||||
|
|
|
|||
|
|
@ -743,6 +743,23 @@ const translation = {
|
|||
apiKeyDescription: 'Configure API key credentials for authentication',
|
||||
apiKeyConfigured: 'API key configured successfully',
|
||||
configurationFailed: 'Configuration failed',
|
||||
failedToStart: 'Failed to start authentication flow',
|
||||
credentialsVerified: 'Credentials verified successfully',
|
||||
credentialVerificationFailed: 'Credential verification failed',
|
||||
verifyAndContinue: 'Verify & Continue',
|
||||
configureParameters: 'Configure Parameters',
|
||||
parametersDescription: 'Configure trigger parameters and properties',
|
||||
configurationComplete: 'Configuration Complete',
|
||||
configurationCompleteDescription: 'Your trigger has been configured successfully',
|
||||
configurationCompleteMessage: 'Your trigger configuration is now complete and ready to use.',
|
||||
parameters: 'Parameters',
|
||||
properties: 'Properties',
|
||||
propertiesDescription: 'Additional configuration properties for this trigger',
|
||||
noConfigurationRequired: 'No additional configuration required for this trigger.',
|
||||
subscriptionName: 'Subscription Name',
|
||||
subscriptionNameDescription: 'Enter a unique name for this trigger subscription',
|
||||
subscriptionNamePlaceholder: 'Enter subscription name...',
|
||||
subscriptionNameRequired: 'Subscription name is required',
|
||||
},
|
||||
questionClassifiers: {
|
||||
model: 'model',
|
||||
|
|
|
|||
|
|
@ -1053,6 +1053,23 @@ const translation = {
|
|||
apiKeyDescription: '認証のための API キー認証情報を設定してください',
|
||||
apiKeyConfigured: 'API キーが正常に設定されました',
|
||||
configurationFailed: '設定に失敗しました',
|
||||
failedToStart: '認証フローの開始に失敗しました',
|
||||
credentialsVerified: '認証情報が正常に検証されました',
|
||||
credentialVerificationFailed: '認証情報の検証に失敗しました',
|
||||
verifyAndContinue: '検証して続行',
|
||||
configureParameters: 'パラメーターを設定',
|
||||
parametersDescription: 'トリガーのパラメーターとプロパティを設定してください',
|
||||
configurationComplete: '設定完了',
|
||||
configurationCompleteDescription: 'トリガーが正常に設定されました',
|
||||
configurationCompleteMessage: 'トリガーの設定が完了し、使用する準備ができました。',
|
||||
parameters: 'パラメーター',
|
||||
properties: 'プロパティ',
|
||||
propertiesDescription: 'このトリガーの追加設定プロパティ',
|
||||
noConfigurationRequired: 'このトリガーには追加の設定は必要ありません。',
|
||||
subscriptionName: 'サブスクリプション名',
|
||||
subscriptionNameDescription: 'このトリガーサブスクリプションの一意な名前を入力してください',
|
||||
subscriptionNamePlaceholder: 'サブスクリプション名を入力...',
|
||||
subscriptionNameRequired: 'サブスクリプション名は必須です',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
|
|
|
|||
|
|
@ -1053,6 +1053,23 @@ const translation = {
|
|||
apiKeyDescription: '配置 API key 凭据进行身份验证',
|
||||
apiKeyConfigured: 'API key 配置成功',
|
||||
configurationFailed: '配置失败',
|
||||
failedToStart: '启动身份验证流程失败',
|
||||
credentialsVerified: '凭据验证成功',
|
||||
credentialVerificationFailed: '凭据验证失败',
|
||||
verifyAndContinue: '验证并继续',
|
||||
configureParameters: '配置参数',
|
||||
parametersDescription: '配置触发器参数和属性',
|
||||
configurationComplete: '配置完成',
|
||||
configurationCompleteDescription: '您的触发器已成功配置',
|
||||
configurationCompleteMessage: '您的触发器配置已完成,现在可以使用了。',
|
||||
parameters: '参数',
|
||||
properties: '属性',
|
||||
propertiesDescription: '此触发器的额外配置属性',
|
||||
noConfigurationRequired: '此触发器不需要额外配置。',
|
||||
subscriptionName: '订阅名称',
|
||||
subscriptionNameDescription: '为此触发器订阅输入一个唯一名称',
|
||||
subscriptionNamePlaceholder: '输入订阅名称...',
|
||||
subscriptionNameRequired: '订阅名称是必需的',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
|
|
|
|||
|
|
@ -268,7 +268,12 @@ export const useTriggerPluginDynamicOptions = (payload: {
|
|||
queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.extra],
|
||||
queryFn: () => get<{ options: Array<{ value: string; label: any }> }>(
|
||||
'/workspaces/current/plugin/parameters/dynamic-options',
|
||||
{ params: payload },
|
||||
{
|
||||
params: {
|
||||
...payload,
|
||||
provider_type: 'trigger', // Add required provider_type parameter
|
||||
},
|
||||
},
|
||||
),
|
||||
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter,
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue