mirror of https://github.com/langgenius/dify.git
feat: add subscription
This commit is contained in:
parent
bd5cf1c272
commit
50bff270b6
|
|
@ -6,6 +6,7 @@ import type {
|
|||
AnyFormApi,
|
||||
FieldValidators,
|
||||
} from '@tanstack/react-form'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
|
||||
export type TypeWithI18N<T = string> = {
|
||||
en_US: T
|
||||
|
|
@ -36,7 +37,7 @@ export enum FormTypeEnum {
|
|||
}
|
||||
|
||||
export type FormOption = {
|
||||
label: TypeWithI18N | string
|
||||
label: string | TypeWithI18N | Record<Locale, string>
|
||||
value: string
|
||||
show_on?: FormShowOnObject[]
|
||||
icon?: string
|
||||
|
|
@ -47,15 +48,15 @@ export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, a
|
|||
export type FormSchema = {
|
||||
type: FormTypeEnum
|
||||
name: string
|
||||
label: string | ReactNode | TypeWithI18N
|
||||
label: string | ReactNode | TypeWithI18N | Record<Locale, string>
|
||||
required: boolean
|
||||
default?: any
|
||||
tooltip?: string | TypeWithI18N
|
||||
tooltip?: string | TypeWithI18N | Record<Locale, string>
|
||||
show_on?: FormShowOnObject[]
|
||||
url?: string
|
||||
scope?: string
|
||||
help?: string | TypeWithI18N
|
||||
placeholder?: string | TypeWithI18N
|
||||
help?: string | TypeWithI18N | Record<Locale, string>
|
||||
placeholder?: string | TypeWithI18N | Record<Locale, string>
|
||||
options?: FormOption[]
|
||||
labelClassName?: string
|
||||
validators?: AnyValidators
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import EndpointList from './endpoint-list'
|
|||
import ActionList from './action-list'
|
||||
import ModelList from './model-list'
|
||||
import AgentStrategyList from './agent-strategy-list'
|
||||
import { SubscriptionList } from './subscription-list'
|
||||
import { TriggerEventsList } from './trigger-events-list'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { type PluginDetail, PluginType } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
|
|
@ -48,6 +50,12 @@ const PluginDetailPanel: FC<Props> = ({
|
|||
onUpdate={handleUpdate}
|
||||
/>
|
||||
<div className='grow overflow-y-auto'>
|
||||
{detail.declaration.category === PluginType.trigger && (
|
||||
<>
|
||||
<SubscriptionList detail={detail} />
|
||||
<TriggerEventsList detail={detail} />
|
||||
</>
|
||||
)}
|
||||
{!!detail.declaration.tool && <ActionList detail={detail} />}
|
||||
{!!detail.declaration.agent_strategy && <AgentStrategyList detail={detail} />}
|
||||
{!!detail.declaration.endpoint && <EndpointList detail={detail} />}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
'use client'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiQuestionLine,
|
||||
RiSettings4Line,
|
||||
} from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
type Props = {
|
||||
onSelect: (type: SubscriptionAddType) => void
|
||||
onClose: () => void
|
||||
position?: 'bottom' | 'right'
|
||||
}
|
||||
|
||||
const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom' }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node))
|
||||
onClose()
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [onClose])
|
||||
|
||||
const options = [
|
||||
{
|
||||
key: 'oauth' as const,
|
||||
title: t('pluginTrigger.subscription.addType.options.oauth.title'),
|
||||
rightIcon: RiSettings4Line,
|
||||
hasRightIcon: true,
|
||||
},
|
||||
{
|
||||
key: 'api-key' as const,
|
||||
title: t('pluginTrigger.subscription.addType.options.apiKey.title'),
|
||||
hasRightIcon: false,
|
||||
},
|
||||
{
|
||||
key: 'manual' as const,
|
||||
title: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
|
||||
rightIcon: RiQuestionLine,
|
||||
hasRightIcon: true,
|
||||
tooltip: t('pluginTrigger.subscription.addType.options.manual.tip'),
|
||||
},
|
||||
]
|
||||
|
||||
const handleOptionClick = (type: SubscriptionAddType) => {
|
||||
onSelect(type)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className={cn(
|
||||
'absolute z-50 w-full rounded-xl border-[0.5px] border-components-panel-border bg-white/95 shadow-xl backdrop-blur-sm',
|
||||
position === 'bottom'
|
||||
? 'left-1/2 top-full mt-2 -translate-x-1/2'
|
||||
: 'right-full top-0 mr-2',
|
||||
)}
|
||||
>
|
||||
{/* Context Menu Content */}
|
||||
<div className="flex flex-col">
|
||||
{/* First Group - OAuth & API Key */}
|
||||
<div className="p-1">
|
||||
{options.slice(0, 2).map((option, index) => {
|
||||
const RightIconComponent = option.rightIcon
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
onClick={() => handleOptionClick(option.key)}
|
||||
className={cn(
|
||||
'flex h-8 w-full items-center gap-1 rounded-lg px-2 py-1 text-left transition-colors hover:bg-state-base-hover',
|
||||
)}
|
||||
>
|
||||
{/* Label */}
|
||||
<div className="flex grow items-center px-1 py-0.5">
|
||||
<div className="grow truncate text-[14px] leading-[20px] text-[#354052]">
|
||||
{option.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Icon */}
|
||||
{option.hasRightIcon && RightIconComponent && (
|
||||
<div className="flex items-center justify-center rounded-md p-0.5">
|
||||
<div className="flex h-5 w-5 items-center justify-center">
|
||||
<div className="relative h-4 w-4">
|
||||
<RightIconComponent className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="h-px bg-[rgba(16,24,40,0.04)]" />
|
||||
|
||||
{/* Second Group - Manual */}
|
||||
<div className="p-1">
|
||||
{options.slice(2).map((option) => {
|
||||
const RightIconComponent = option.rightIcon
|
||||
return (
|
||||
<button
|
||||
key={option.key}
|
||||
onClick={() => handleOptionClick(option.key)}
|
||||
className="flex h-8 w-full items-center gap-1 rounded-lg px-2 py-1 text-left transition-colors hover:bg-[rgba(200,206,218,0.2)]"
|
||||
title={option.tooltip}
|
||||
>
|
||||
{/* Label */}
|
||||
<div className="flex grow items-center px-1 py-0.5">
|
||||
<div className="grow truncate text-[14px] leading-[20px] text-[#354052]">
|
||||
{option.title}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Icon */}
|
||||
{option.hasRightIcon && RightIconComponent && (
|
||||
<div className="relative h-4 w-4 shrink-0">
|
||||
<div className="absolute inset-0 flex items-center justify-center p-0.5">
|
||||
<div className="relative h-4 w-4">
|
||||
<div className="absolute inset-[8.333%]">
|
||||
<RightIconComponent className="h-full w-full text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Border overlay */}
|
||||
<div className="pointer-events-none absolute inset-0 rounded-xl border-[0.5px] border-[rgba(16,24,40,0.08)]" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddTypeDropdown
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowLeftLine,
|
||||
RiArrowRightLine,
|
||||
RiCloseLine,
|
||||
} from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Form from '@/app/components/base/form/form-scenarios/auth'
|
||||
import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
enum ApiKeyStep {
|
||||
Verify = 'verify',
|
||||
Configuration = 'configuration',
|
||||
}
|
||||
|
||||
const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// State
|
||||
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(ApiKeyStep.Verify)
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<any>(null)
|
||||
const [verificationError, setVerificationError] = useState<string>('')
|
||||
|
||||
// Form refs
|
||||
const credentialsFormRef = React.useRef<FormRefObject>(null)
|
||||
const parametersFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
// API mutations
|
||||
const { mutate: createBuilder, isPending: isCreatingBuilder } = useCreateTriggerSubscriptionBuilder()
|
||||
const { mutate: verifyBuilder, isPending: isVerifying } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
// Get provider name and schemas
|
||||
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
|
||||
const credentialsSchema = pluginDetail.declaration.trigger?.credentials_schema || []
|
||||
const parametersSchema = pluginDetail.declaration.trigger?.subscription_schema?.parameters_schema || []
|
||||
|
||||
const handleVerify = () => {
|
||||
const credentialsFormValues = credentialsFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
const credentials = credentialsFormValues.values
|
||||
|
||||
if (!Object.keys(credentials).length) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: 'Please fill in all required credentials',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setVerificationError('')
|
||||
|
||||
// First create builder
|
||||
createBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
credential_type: TriggerCredentialTypeEnum.ApiKey,
|
||||
},
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
const builder = response.subscription_builder
|
||||
setSubscriptionBuilder(builder)
|
||||
|
||||
// setCurrentStep('configuration')
|
||||
|
||||
verifyBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: builder.id,
|
||||
credentials,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.apiKey.verify.success'),
|
||||
})
|
||||
setCurrentStep(ApiKeyStep.Configuration)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setVerificationError(error?.message || t('pluginTrigger.modal.apiKey.verify.error'))
|
||||
},
|
||||
},
|
||||
)
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.verifyFailed'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!subscriptionName.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.form.subscriptionName.required'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!subscriptionBuilder)
|
||||
return
|
||||
|
||||
buildSubscription(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: 'Subscription created successfully',
|
||||
})
|
||||
onSuccess()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('modal.errors.createFailed'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setCurrentStep(ApiKeyStep.Verify)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onClose}
|
||||
className='!max-w-[520px] !p-0'
|
||||
wrapperClassName='!z-[1002]'
|
||||
>
|
||||
<div className='flex items-center justify-between border-b border-divider-subtle p-6 pb-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
{currentStep === ApiKeyStep.Configuration && (
|
||||
<Button variant='ghost' size='small' onClick={handleBack}>
|
||||
<RiArrowLeftLine className='h-4 w-4' />
|
||||
</Button>
|
||||
)}
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{t('pluginTrigger.modal.apiKey.title')}
|
||||
</h3>
|
||||
</div>
|
||||
<Button variant='ghost' size='small' onClick={onClose}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Step indicator */}
|
||||
<div className='border-b border-divider-subtle px-6 py-4'>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className={`flex items-center gap-2 ${currentStep === ApiKeyStep.Verify ? 'text-text-accent' : currentStep === ApiKeyStep.Configuration ? 'text-text-success' : 'text-text-tertiary'}`}>
|
||||
<div className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${currentStep === ApiKeyStep.Verify
|
||||
? 'bg-util-accent-light-blue text-util-accent-blue'
|
||||
: currentStep === ApiKeyStep.Configuration
|
||||
? 'bg-state-success-bg text-state-success-text'
|
||||
: 'bg-background-default-subtle text-text-tertiary'}`}>
|
||||
1
|
||||
</div>
|
||||
<span className='system-sm-medium'>{t('pluginTrigger.modal.steps.verify')}</span>
|
||||
</div>
|
||||
|
||||
<div className='h-px flex-1 bg-divider-subtle'></div>
|
||||
|
||||
<div className={`flex items-center gap-2 ${currentStep === ApiKeyStep.Configuration ? 'text-text-accent' : 'text-text-tertiary'}`}>
|
||||
<div className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${currentStep === ApiKeyStep.Configuration
|
||||
? 'bg-util-accent-light-blue text-util-accent-blue'
|
||||
: 'bg-background-default-subtle text-text-tertiary'}`}>
|
||||
2
|
||||
</div>
|
||||
<span className='system-sm-medium'>{t('pluginTrigger.modal.steps.configuration')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='p-6'>
|
||||
{currentStep === ApiKeyStep.Verify ? (
|
||||
// Step 1: Verify Credentials
|
||||
<div>
|
||||
|
||||
{credentialsSchema.length > 0 && (
|
||||
<div className='mb-4'>
|
||||
<Form
|
||||
formSchemas={credentialsSchema}
|
||||
ref={credentialsFormRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verificationError && (
|
||||
<div className='bg-state-destructive-bg mb-4 rounded-lg border border-state-destructive-border p-3'>
|
||||
<div className='text-state-destructive-text system-xs-medium'>
|
||||
{verificationError}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// Step 2: Configuration
|
||||
<div>
|
||||
{/* <div className='mb-4'>
|
||||
<h4 className='system-sm-semibold mb-2 text-text-primary'>
|
||||
{t('pluginTrigger.modal.apiKey.configuration.title')}
|
||||
</h4>
|
||||
<p className='system-xs-regular text-text-secondary'>
|
||||
{t('pluginTrigger.modal.apiKey.configuration.description')}
|
||||
</p>
|
||||
</div> */}
|
||||
|
||||
{/* Subscription Name */}
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.subscriptionName.label')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => setSubscriptionName(e.target.value)}
|
||||
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Callback URL (read-only) */}
|
||||
{subscriptionBuilder?.endpoint && (
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.label')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionBuilder.endpoint}
|
||||
readOnly
|
||||
className='bg-background-section'
|
||||
/>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.description')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dynamic Parameters Form */}
|
||||
{parametersSchema.length > 0 && (
|
||||
<div className='mb-4'>
|
||||
<div className='system-sm-medium mb-3 text-text-primary'>
|
||||
Subscription Parameters
|
||||
</div>
|
||||
<Form
|
||||
formSchemas={parametersSchema}
|
||||
ref={parametersFormRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='flex justify-end gap-2 border-t border-divider-subtle p-6 pt-4'>
|
||||
<Button variant='secondary' onClick={onClose}>
|
||||
{t('pluginTrigger.modal.common.cancel')}
|
||||
</Button>
|
||||
|
||||
{currentStep === ApiKeyStep.Verify ? (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleVerify}
|
||||
loading={isCreatingBuilder || isVerifying}
|
||||
// disabled={credentialsSchema.length > 0}
|
||||
>
|
||||
{isVerifying ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')}
|
||||
<RiArrowRightLine className='ml-2 h-4 w-4' />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreate}
|
||||
loading={isBuilding}
|
||||
disabled={!subscriptionName.trim()}
|
||||
>
|
||||
{isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiKeyAddModal
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
RiAddLine,
|
||||
RiBookOpenLine,
|
||||
RiWebhookLine,
|
||||
} from '@remixicon/react'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import SubscriptionCard from './subscription-card'
|
||||
import SubscriptionAddModal from './subscription-add-modal'
|
||||
import AddTypeDropdown from './add-type-dropdown'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useTriggerSubscriptions } from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
detail: PluginDetail
|
||||
}
|
||||
|
||||
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
export const SubscriptionList = ({ detail }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const showTopBorder = detail.declaration.tool || detail.declaration.endpoint
|
||||
|
||||
// Fetch subscriptions
|
||||
const { data: subscriptions, isLoading } = useTriggerSubscriptions(`${detail.plugin_id}/${detail.declaration.name}`)
|
||||
|
||||
// Modal states
|
||||
const [isShowAddModal, {
|
||||
setTrue: showAddModal,
|
||||
setFalse: hideAddModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const [selectedAddType, setSelectedAddType] = React.useState<SubscriptionAddType | null>(null)
|
||||
|
||||
// Dropdown state for add button
|
||||
const [isShowAddDropdown, {
|
||||
setTrue: showAddDropdown,
|
||||
setFalse: hideAddDropdown,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleAddTypeSelect = (type: SubscriptionAddType) => {
|
||||
setSelectedAddType(type)
|
||||
hideAddDropdown()
|
||||
showAddModal()
|
||||
}
|
||||
|
||||
const handleModalClose = () => {
|
||||
hideAddModal()
|
||||
setSelectedAddType(null)
|
||||
}
|
||||
|
||||
const handleRefreshList = () => {
|
||||
// This will be called after successful operations
|
||||
// The query will auto-refresh due to React Query
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
<div className='flex items-center justify-center py-8'>
|
||||
<div className='text-text-tertiary'>{t('common.dataLoading')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const hasSubscriptions = subscriptions && subscriptions.length > 0
|
||||
|
||||
return (
|
||||
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
|
||||
{!hasSubscriptions ? (
|
||||
<div className='relative w-full'>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='medium'
|
||||
className='w-full'
|
||||
onClick={showAddDropdown}
|
||||
>
|
||||
<RiAddLine className='mr-2 h-4 w-4' />
|
||||
{t('pluginTrigger.subscription.empty.button')}
|
||||
</Button>
|
||||
{isShowAddDropdown && (
|
||||
<AddTypeDropdown
|
||||
onSelect={handleAddTypeSelect}
|
||||
onClose={hideAddDropdown}
|
||||
position='bottom'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// List state with header and secondary add button
|
||||
<>
|
||||
<div className='system-sm-semibold-uppercase mb-3 flex h-6 items-center justify-between text-text-secondary'>
|
||||
<div className='flex items-center gap-0.5'>
|
||||
{t('pluginTrigger.subscription.list.title')}
|
||||
<Tooltip
|
||||
position='right'
|
||||
popupClassName='w-[240px] p-4 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border'
|
||||
popupContent={
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div className='flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle'>
|
||||
<RiWebhookLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('pluginTrigger.subscription.list.tooltip')}
|
||||
</div>
|
||||
<a
|
||||
href={docLink('/plugins/schema-definition/trigger')}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<div className='system-xs-regular inline-flex cursor-pointer items-center gap-1 text-text-accent'>
|
||||
<RiBookOpenLine className='h-3 w-3' />
|
||||
{t('pluginTrigger.subscription.list.tooltip.viewDocument')}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className='relative'>
|
||||
<ActionButton onClick={showAddDropdown}>
|
||||
<RiAddLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
{isShowAddDropdown && (
|
||||
<AddTypeDropdown
|
||||
onSelect={handleAddTypeSelect}
|
||||
onClose={hideAddDropdown}
|
||||
position='right'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-2'>
|
||||
{subscriptions?.map(subscription => (
|
||||
<SubscriptionCard
|
||||
key={subscription.id}
|
||||
data={subscription}
|
||||
onRefresh={handleRefreshList}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isShowAddModal && selectedAddType && (
|
||||
<SubscriptionAddModal
|
||||
type={selectedAddType}
|
||||
pluginDetail={detail}
|
||||
onClose={handleModalClose}
|
||||
onSuccess={handleRefreshList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,480 @@
|
|||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiArrowRightSLine,
|
||||
RiCheckboxCircleLine,
|
||||
RiCloseLine,
|
||||
RiErrorWarningLine,
|
||||
RiFileCopyLine,
|
||||
} from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useCreateTriggerSubscriptionBuilder,
|
||||
// useTriggerSubscriptionBuilderLogs,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
|
||||
// import { BaseForm } from '@/app/components/base/form/components/base'
|
||||
// import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
// type LogEntry = {
|
||||
// timestamp: string
|
||||
// method: string
|
||||
// path: string
|
||||
// status: number
|
||||
// headers: Record<string, any>
|
||||
// body: any
|
||||
// response: any
|
||||
// }
|
||||
|
||||
const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Form state
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
|
||||
const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set())
|
||||
// const formRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
// API mutations
|
||||
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
// Get provider name
|
||||
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
|
||||
|
||||
// const { data: logs, isLoading: isLoadingLogs } = useTriggerSubscriptionBuilderLogs(
|
||||
// providerName,
|
||||
// subscriptionBuilder?.id || '',
|
||||
// {
|
||||
// enabled: !!subscriptionBuilder?.id,
|
||||
// refetchInterval: 3000, // Poll every 3 seconds
|
||||
// },
|
||||
// )
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockLogs = [
|
||||
{
|
||||
id: '1',
|
||||
timestamp: '2024-01-15T18:09:14Z',
|
||||
method: 'POST',
|
||||
path: '/webhook',
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Slack-Hooks/1.0',
|
||||
'X-Slack-Signature': 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503',
|
||||
},
|
||||
body: {
|
||||
verification_token: 'secret_tMrlL1qK5vuQAhCh',
|
||||
event: {
|
||||
type: 'message',
|
||||
text: 'Hello world',
|
||||
user: 'U1234567890',
|
||||
},
|
||||
},
|
||||
response: {
|
||||
error: 'Internal server error',
|
||||
message: 'Failed to process webhook',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
timestamp: '2024-01-15T18:09:14Z',
|
||||
method: 'POST',
|
||||
path: '/webhook',
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Slack-Hooks/1.0',
|
||||
},
|
||||
body: {
|
||||
verification_token: 'secret_tMrlL1qK5vuQAhCh',
|
||||
},
|
||||
response: {
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
timestamp: '2024-01-15T18:09:14Z',
|
||||
method: 'POST',
|
||||
path: '/webhook',
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Slack-Hooks/1.0',
|
||||
},
|
||||
body: {
|
||||
verification_token: 'secret_tMrlL1qK5vuQAhCh',
|
||||
},
|
||||
response: {
|
||||
output: {
|
||||
output: 'I am the GPT-3 model from OpenAI, an artificial intelligence assistant.',
|
||||
},
|
||||
raw_output: 'I am the GPT-3 model from OpenAI, an artificial intelligence assistant.',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const logs = mockLogs
|
||||
const isLoadingLogs = false
|
||||
|
||||
// Create subscription builder on mount
|
||||
useEffect(() => {
|
||||
if (!subscriptionBuilder) {
|
||||
createBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
credential_type: TriggerCredentialTypeEnum.Unauthorized,
|
||||
},
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
const builder = response.subscription_builder
|
||||
setSubscriptionBuilder(builder)
|
||||
},
|
||||
onError: (error) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.errors.createFailed'),
|
||||
})
|
||||
console.error('Failed to create subscription builder:', error)
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}, [createBuilder, providerName, subscriptionBuilder, t])
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!subscriptionName.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.form.subscriptionName.required'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!subscriptionBuilder)
|
||||
return
|
||||
|
||||
// Get form values using the ref (for future use if needed)
|
||||
// const formValues = formRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
|
||||
buildSubscription(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: 'Subscription created successfully',
|
||||
})
|
||||
onSuccess()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.createFailed'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const toggleLogExpansion = (logId: string) => {
|
||||
const newExpanded = new Set(expandedLogs)
|
||||
if (newExpanded.has(logId))
|
||||
newExpanded.delete(logId)
|
||||
else
|
||||
newExpanded.add(logId)
|
||||
|
||||
setExpandedLogs(newExpanded)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onClose}
|
||||
className='!max-w-[640px] !p-0'
|
||||
wrapperClassName='!z-[1002]'
|
||||
>
|
||||
<div className='flex items-center justify-between p-6 pb-3'>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{t('pluginTrigger.modal.manual.title')}
|
||||
</h3>
|
||||
<ActionButton onClick={onClose} >
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<div className='max-h-[70vh] overflow-y-auto p-6 pt-2'>
|
||||
{/* Subscription Name */}
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.subscriptionName.label')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => setSubscriptionName(e.target.value)}
|
||||
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Callback URL */}
|
||||
<div className='mb-6'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.label')}
|
||||
</label>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
value={subscriptionBuilder?.endpoint}
|
||||
readOnly
|
||||
className='pr-12'
|
||||
placeholder={t('pluginTrigger.modal.form.callbackUrl.placeholder')}
|
||||
/>
|
||||
<CopyFeedbackNew className='absolute right-1 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary' content={subscriptionBuilder?.endpoint || ''} />
|
||||
</div>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.description')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic Parameters Form */}
|
||||
{/* {parametersSchema.length > 0 && (
|
||||
<div className='mb-6'>
|
||||
<div className='system-sm-medium mb-3 text-text-primary'>
|
||||
Subscription Parameters
|
||||
</div>
|
||||
<BaseForm
|
||||
formSchemas={parametersSchema}
|
||||
ref={formRef}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
|
||||
{/* Request Logs */}
|
||||
{subscriptionBuilder && (
|
||||
<div className='mb-6'>
|
||||
{/* Divider with Title */}
|
||||
<div className='mb-3 flex items-center gap-2'>
|
||||
<div className='system-xs-medium-uppercase text-text-tertiary'>
|
||||
REQUESTS HISTORY
|
||||
</div>
|
||||
<div className='h-px flex-1 bg-gradient-to-r from-divider-regular to-transparent' />
|
||||
</div>
|
||||
|
||||
{/* Request List */}
|
||||
<div className='flex flex-col gap-1'>
|
||||
{isLoadingLogs && (
|
||||
<div className='flex items-center justify-center gap-1 rounded-lg bg-background-section p-3'>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
<svg className='animate-spin' viewBox='0 0 24 24'>
|
||||
<circle cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='2' fill='none' strokeDasharray='31.416' strokeDashoffset='31.416'>
|
||||
<animate attributeName='stroke-dasharray' dur='2s' values='0 31.416;15.708 15.708;0 31.416' repeatCount='indefinite' />
|
||||
<animate attributeName='stroke-dashoffset' dur='2s' values='0;-15.708;-31.416' repeatCount='indefinite' />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
Awaiting request from Slack...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingLogs && logs && logs.length > 0 && (
|
||||
<>
|
||||
{logs.map((log, index) => {
|
||||
const logId = log.id || index.toString()
|
||||
const isExpanded = expandedLogs.has(logId)
|
||||
const isSuccess = log.status >= 200 && log.status < 300
|
||||
const isError = log.status >= 400
|
||||
|
||||
return (
|
||||
<div
|
||||
key={logId}
|
||||
className={cn(
|
||||
'relative rounded-lg border shadow-sm',
|
||||
isError && 'border-state-destructive-border bg-white',
|
||||
!isError && isExpanded && 'border-components-panel-border bg-white',
|
||||
!isError && !isExpanded && 'border-components-panel-border bg-background-section',
|
||||
)}
|
||||
>
|
||||
{/* Error background decoration */}
|
||||
{isError && (
|
||||
<div className='absolute -left-1 -top-4 h-16 w-16 opacity-10'>
|
||||
<div className='h-full w-full rounded-full bg-text-destructive' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Request Header */}
|
||||
<button
|
||||
onClick={() => toggleLogExpansion(logId)}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-between px-2 py-1.5 text-left',
|
||||
isExpanded ? 'pb-1 pt-2' : 'min-h-7',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-0'>
|
||||
{isExpanded ? (
|
||||
<RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
|
||||
) : (
|
||||
<RiArrowRightSLine className='h-4 w-4 text-text-tertiary' />
|
||||
)}
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
REQUEST #{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{new Date(log.timestamp).toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
{isSuccess ? (
|
||||
<RiCheckboxCircleLine className='text-state-success-text h-full w-full' />
|
||||
) : (
|
||||
<RiErrorWarningLine className='text-state-destructive-text h-full w-full' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className='flex flex-col gap-1 px-1 pb-1'>
|
||||
{/* Request Block */}
|
||||
<div className='rounded-md bg-components-input-bg-normal'>
|
||||
<div className='flex items-center justify-between px-2 py-1'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
REQUEST
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(JSON.stringify(log.body, null, 2))
|
||||
Toast.notify({ type: 'success', message: 'Copied to clipboard' })
|
||||
}}
|
||||
className='rounded-md p-0.5 hover:bg-components-panel-border'
|
||||
>
|
||||
<RiFileCopyLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex px-0 pb-2 pt-1'>
|
||||
<div className='w-7 pr-3 text-right'>
|
||||
<div className='code-xs-regular text-text-quaternary'>
|
||||
{JSON.stringify(log.body, null, 2).split('\n').map((_, i) => (
|
||||
<div key={i}>{String(i + 1).padStart(2, '0')}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 px-3'>
|
||||
<pre className='code-xs-regular text-text-secondary'>
|
||||
{JSON.stringify(log.body, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Response Block */}
|
||||
<div className='rounded-md bg-components-input-bg-normal'>
|
||||
<div className='flex items-center justify-between px-2 py-1'>
|
||||
<div className='system-xs-semibold-uppercase text-text-secondary'>
|
||||
RESPONSE
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
navigator.clipboard.writeText(JSON.stringify(log.response, null, 2))
|
||||
Toast.notify({ type: 'success', message: 'Copied to clipboard' })
|
||||
}}
|
||||
className='rounded-md p-0.5 hover:bg-components-panel-border'
|
||||
>
|
||||
<RiFileCopyLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='flex px-0 pb-2 pt-1'>
|
||||
<div className='w-7 pr-3 text-right'>
|
||||
<div className='code-xs-regular text-text-quaternary'>
|
||||
{JSON.stringify(log.response, null, 2).split('\n').map((_, i) => (
|
||||
<div key={i}>{String(i + 1).padStart(2, '0')}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-1 px-3'>
|
||||
<pre className='code-xs-regular text-text-secondary'>
|
||||
{JSON.stringify(log.response, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{!isLoadingLogs && (!logs || logs.length === 0) && (
|
||||
<div className='flex items-center justify-center gap-1 rounded-lg bg-background-section p-3'>
|
||||
<div className='h-3.5 w-3.5'>
|
||||
<svg className='animate-spin' viewBox='0 0 24 24'>
|
||||
<circle cx='12' cy='12' r='10' stroke='currentColor' strokeWidth='2' fill='none' strokeDasharray='31.416' strokeDashoffset='31.416'>
|
||||
<animate attributeName='stroke-dasharray' dur='2s' values='0 31.416;15.708 15.708;0 31.416' repeatCount='indefinite' />
|
||||
<animate attributeName='stroke-dashoffset' dur='2s' values='0;-15.708;-31.416' repeatCount='indefinite' />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
Awaiting request from Slack...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='flex justify-end gap-2 border-t border-divider-subtle p-6 pt-4'>
|
||||
<Button variant='secondary' onClick={onClose}>
|
||||
{t('pluginTrigger.modal.common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreate}
|
||||
loading={isBuilding}
|
||||
disabled={!subscriptionName.trim() || !subscriptionBuilder}
|
||||
>
|
||||
{isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default ManualAddModal
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
'use client'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiExternalLinkLine,
|
||||
} from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Form from '@/app/components/base/form/form-scenarios/auth'
|
||||
import type { FormRefObject } from '@/app/components/base/form/types'
|
||||
import {
|
||||
useBuildTriggerSubscription,
|
||||
useConfigureTriggerOAuth,
|
||||
useInitiateTriggerOAuth,
|
||||
useVerifyTriggerSubscriptionBuilder,
|
||||
} from '@/service/use-triggers'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
type OAuthStep = 'setup' | 'authorize' | 'configuration'
|
||||
|
||||
const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
// State
|
||||
const [currentStep, setCurrentStep] = useState<OAuthStep>('setup')
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [authorizationUrl, setAuthorizationUrl] = useState('')
|
||||
const [subscriptionBuilder, setSubscriptionBuilder] = useState<any>(null)
|
||||
const [redirectUrl, setRedirectUrl] = useState('')
|
||||
const [authorizationStatus, setAuthorizationStatus] = useState<'pending' | 'success' | 'failed'>('pending')
|
||||
|
||||
// Form refs
|
||||
const clientFormRef = React.useRef<FormRefObject>(null)
|
||||
const parametersFormRef = React.useRef<FormRefObject>(null)
|
||||
|
||||
// API mutations
|
||||
const { mutate: initiateOAuth, isPending: isInitiating } = useInitiateTriggerOAuth()
|
||||
const { mutate: configureOAuth, isPending: isConfiguring } = useConfigureTriggerOAuth()
|
||||
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
|
||||
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
|
||||
|
||||
// Get provider name and schemas
|
||||
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
|
||||
const clientSchema = pluginDetail.declaration.trigger?.oauth_schema?.client_schema || []
|
||||
const parametersSchema = pluginDetail.declaration.trigger?.subscription_schema?.parameters_schema || []
|
||||
|
||||
// Poll for authorization status
|
||||
useEffect(() => {
|
||||
if (currentStep === 'authorize' && subscriptionBuilder && authorizationStatus === 'pending') {
|
||||
const pollInterval = setInterval(() => {
|
||||
verifyBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAuthorizationStatus('success')
|
||||
setCurrentStep('configuration')
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.authorization.authSuccess'),
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
// Continue polling - auth might still be in progress
|
||||
},
|
||||
},
|
||||
)
|
||||
}, 3000)
|
||||
|
||||
return () => clearInterval(pollInterval)
|
||||
}
|
||||
}, [currentStep, subscriptionBuilder, authorizationStatus, verifyBuilder, providerName, t])
|
||||
|
||||
const handleSetupOAuth = () => {
|
||||
const clientFormValues = clientFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
|
||||
const clientParams = clientFormValues.values
|
||||
|
||||
if (!Object.keys(clientParams).length) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.oauth.authorization.authFailed'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// First configure OAuth client
|
||||
configureOAuth(
|
||||
{
|
||||
provider: providerName,
|
||||
client_params: clientParams as any,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Then get redirect URL and initiate OAuth
|
||||
const baseUrl = window.location.origin
|
||||
const redirectPath = `/plugins/oauth/callback/${providerName}`
|
||||
const fullRedirectUrl = `${baseUrl}${redirectPath}`
|
||||
setRedirectUrl(fullRedirectUrl)
|
||||
|
||||
// Initiate OAuth flow
|
||||
initiateOAuth(providerName, {
|
||||
onSuccess: (response) => {
|
||||
setAuthorizationUrl(response.authorization_url)
|
||||
setSubscriptionBuilder(response.subscription_builder)
|
||||
setCurrentStep('authorize')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const handleAuthorize = () => {
|
||||
if (authorizationUrl) {
|
||||
// Open authorization URL in new window
|
||||
window.open(authorizationUrl, '_blank', 'width=500,height=600')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
if (!subscriptionName.trim()) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('pluginTrigger.modal.form.subscriptionName.required'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!subscriptionBuilder)
|
||||
return
|
||||
|
||||
buildSubscription(
|
||||
{
|
||||
provider: providerName,
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.configuration.success'),
|
||||
})
|
||||
onSuccess()
|
||||
onClose()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.createFailed'),
|
||||
})
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onClose}
|
||||
className='!max-w-[520px] !p-0'
|
||||
wrapperClassName='!z-[1002]'
|
||||
>
|
||||
<div className='flex items-center justify-between border-b border-divider-subtle p-6 pb-4'>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.title')}
|
||||
</h3>
|
||||
<Button variant='ghost' size='small' onClick={onClose}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='p-6'>
|
||||
{currentStep === 'setup' && (
|
||||
<div>
|
||||
<div className='mb-4'>
|
||||
<h4 className='system-sm-semibold mb-2 text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.title')}
|
||||
</h4>
|
||||
<p className='system-xs-regular text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{clientSchema.length > 0 && (
|
||||
<div className='mb-4'>
|
||||
<Form
|
||||
formSchemas={clientSchema}
|
||||
ref={clientFormRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'authorize' && (
|
||||
<div>
|
||||
<div className='mb-4'>
|
||||
<h4 className='system-sm-semibold mb-2 text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.title')}
|
||||
</h4>
|
||||
<p className='system-xs-regular mb-4 text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Redirect URL */}
|
||||
{redirectUrl && (
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.redirectUrl')}
|
||||
</label>
|
||||
<div className='relative'>
|
||||
<Input
|
||||
value={redirectUrl}
|
||||
readOnly
|
||||
className='bg-background-section pr-12'
|
||||
/>
|
||||
<CopyFeedbackNew
|
||||
content={redirectUrl}
|
||||
className='absolute right-1 top-1/2 -translate-y-1/2 text-text-tertiary'
|
||||
/>
|
||||
</div>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.redirectUrlHelp')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authorization Status */}
|
||||
<div className='mb-4 rounded-lg bg-background-section p-4'>
|
||||
{authorizationStatus === 'pending' && (
|
||||
<div className='system-sm-regular text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.waitingAuth')}
|
||||
</div>
|
||||
)}
|
||||
{authorizationStatus === 'success' && (
|
||||
<div className='system-sm-regular text-text-success'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.authSuccess')}
|
||||
</div>
|
||||
)}
|
||||
{authorizationStatus === 'failed' && (
|
||||
<div className='system-sm-regular text-text-destructive'>
|
||||
{t('pluginTrigger.modal.oauth.authorization.authFailed')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Authorize Button */}
|
||||
{authorizationStatus === 'pending' && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleAuthorize}
|
||||
disabled={!authorizationUrl}
|
||||
className='w-full'
|
||||
>
|
||||
<RiExternalLinkLine className='mr-2 h-4 w-4' />
|
||||
{t('pluginTrigger.modal.oauth.authorization.authorizeButton', { provider: providerName })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 'configuration' && (
|
||||
<div>
|
||||
<div className='mb-4'>
|
||||
<h4 className='system-sm-semibold mb-2 text-text-primary'>
|
||||
{t('pluginTrigger.modal.oauth.configuration.title')}
|
||||
</h4>
|
||||
<p className='system-xs-regular text-text-secondary'>
|
||||
{t('pluginTrigger.modal.oauth.configuration.description')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Subscription Name */}
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.subscriptionName.label')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => setSubscriptionName(e.target.value)}
|
||||
placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Callback URL (read-only) */}
|
||||
{subscriptionBuilder?.endpoint && (
|
||||
<div className='mb-4'>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.label')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionBuilder.endpoint}
|
||||
readOnly
|
||||
className='bg-background-section'
|
||||
/>
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{t('pluginTrigger.modal.form.callbackUrl.description')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Dynamic Parameters Form */}
|
||||
{parametersSchema.length > 0 && (
|
||||
<div className='mb-4'>
|
||||
<Form
|
||||
formSchemas={parametersSchema}
|
||||
ref={parametersFormRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className='flex justify-end gap-2 border-t border-divider-subtle p-6 pt-4'>
|
||||
<Button variant='secondary' onClick={onClose}>
|
||||
{t('pluginTrigger.modal.common.cancel')}
|
||||
</Button>
|
||||
|
||||
{currentStep === 'setup' && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleSetupOAuth}
|
||||
loading={isConfiguring || isInitiating}
|
||||
// disabled={clientSchema.length > 0}
|
||||
>
|
||||
{(isConfiguring || isInitiating) ? t('pluginTrigger.modal.common.authorizing') : t('pluginTrigger.modal.common.authorize')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{currentStep === 'configuration' && (
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreate}
|
||||
loading={isBuilding}
|
||||
disabled={!subscriptionName.trim()}
|
||||
>
|
||||
{isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default OAuthAddModal
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
// import { useTranslation } from 'react-i18next'
|
||||
// import Modal from '@/app/components/base/modal'
|
||||
import ManualAddModal from './manual-add-modal'
|
||||
import ApiKeyAddModal from './api-key-add-modal'
|
||||
import OAuthAddModal from './oauth-add-modal'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
|
||||
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
type Props = {
|
||||
type: SubscriptionAddType
|
||||
pluginDetail: PluginDetail
|
||||
onClose: () => void
|
||||
onSuccess: () => void
|
||||
}
|
||||
|
||||
const SubscriptionAddModal = ({ type, pluginDetail, onClose, onSuccess }: Props) => {
|
||||
// const { t } = useTranslation()
|
||||
|
||||
const renderModalContent = () => {
|
||||
switch (type) {
|
||||
case 'manual':
|
||||
return (
|
||||
<ManualAddModal
|
||||
pluginDetail={pluginDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)
|
||||
case 'api-key':
|
||||
return (
|
||||
<ApiKeyAddModal
|
||||
pluginDetail={pluginDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)
|
||||
case 'oauth':
|
||||
return (
|
||||
<OAuthAddModal
|
||||
pluginDetail={pluginDetail}
|
||||
onClose={onClose}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return renderModalContent()
|
||||
}
|
||||
|
||||
export default SubscriptionAddModal
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiGitBranchLine,
|
||||
RiKeyLine,
|
||||
RiUserLine,
|
||||
RiWebhookLine,
|
||||
} from '@remixicon/react'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import Confirm from '@/app/components/base/confirm'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
|
||||
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
data: TriggerSubscription
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
const getProviderIcon = (provider: string) => {
|
||||
switch (provider) {
|
||||
case 'github':
|
||||
return <RiGitBranchLine className='h-4 w-4' />
|
||||
case 'gitlab':
|
||||
return <RiGitBranchLine className='h-4 w-4' />
|
||||
default:
|
||||
return <RiWebhookLine className='h-4 w-4' />
|
||||
}
|
||||
}
|
||||
|
||||
const getCredentialIcon = (credentialType: string) => {
|
||||
switch (credentialType) {
|
||||
case 'oauth2':
|
||||
return <RiUserLine className='h-4 w-4 text-text-accent' />
|
||||
case 'api_key':
|
||||
return <RiKeyLine className='h-4 w-4 text-text-warning' />
|
||||
case 'unauthorized':
|
||||
return <RiWebhookLine className='h-4 w-4 text-text-secondary' />
|
||||
default:
|
||||
return <RiWebhookLine className='h-4 w-4' />
|
||||
}
|
||||
}
|
||||
|
||||
const SubscriptionCard = ({ data, onRefresh }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isShowDeleteModal, {
|
||||
setTrue: showDeleteModal,
|
||||
setFalse: hideDeleteModal,
|
||||
}] = useBoolean(false)
|
||||
|
||||
// API mutations
|
||||
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
|
||||
|
||||
const handleDelete = () => {
|
||||
deleteSubscription(data.id, {
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title'),
|
||||
})
|
||||
onRefresh()
|
||||
hideDeleteModal()
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || 'Failed to delete subscription',
|
||||
})
|
||||
hideDeleteModal()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Determine if subscription is active/enabled based on properties
|
||||
const isActive = data.properties?.active !== false
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'group relative flex items-center justify-between rounded-lg border-[0.5px] p-3 transition-all',
|
||||
'hover:border-components-panel-border hover:bg-background-default-hover',
|
||||
isActive
|
||||
? 'bg-components-panel-on-panel-item border-components-panel-border-subtle'
|
||||
: 'bg-components-panel-on-panel-item-disabled border-components-panel-border-subtle opacity-60',
|
||||
)}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px]',
|
||||
isActive
|
||||
? 'border-components-panel-border-subtle bg-background-default-subtle text-text-secondary'
|
||||
: 'bg-background-default-disabled border-components-panel-border-subtle text-text-quaternary',
|
||||
)}>
|
||||
{getProviderIcon(data.provider)}
|
||||
</div>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className={cn(
|
||||
'system-sm-medium',
|
||||
isActive ? 'text-text-primary' : 'text-text-tertiary',
|
||||
)}>
|
||||
{data.name}
|
||||
</span>
|
||||
{getCredentialIcon(data.credential_type)}
|
||||
</div>
|
||||
<div className={cn(
|
||||
'system-xs-regular flex items-center gap-2',
|
||||
isActive ? 'text-text-tertiary' : 'text-text-quaternary',
|
||||
)}>
|
||||
<span>{data.provider}</span>
|
||||
<span>•</span>
|
||||
<span className={cn(
|
||||
'rounded px-2 py-0.5 text-xs font-medium',
|
||||
isActive
|
||||
? 'bg-state-success-bg text-state-success-text'
|
||||
: 'bg-background-default-subtle text-text-quaternary',
|
||||
)}>
|
||||
{isActive
|
||||
? t('pluginTrigger.subscription.list.item.status.active')
|
||||
: t('pluginTrigger.subscription.list.item.status.inactive')
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete button - only show on hover */}
|
||||
<div className={cn(
|
||||
'absolute right-3 top-1/2 -translate-y-1/2 transition-opacity',
|
||||
isHovered ? 'opacity-100' : 'opacity-0',
|
||||
)}>
|
||||
<ActionButton
|
||||
onClick={showDeleteModal}
|
||||
className='hover:text-state-destructive-text hover:bg-state-destructive-hover'
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isShowDeleteModal && (
|
||||
<Confirm
|
||||
title={t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title')}
|
||||
content={t('pluginTrigger.subscription.list.item.actions.deleteConfirm.content', { name: data.name })}
|
||||
isShow={isShowDeleteModal}
|
||||
onConfirm={handleDelete}
|
||||
onCancel={hideDeleteModal}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubscriptionCard
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiEditLine,
|
||||
RiKeyLine,
|
||||
RiUserLine,
|
||||
} from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
pluginDetail: PluginDetail
|
||||
onCancel: () => void
|
||||
onSaved: (data: any) => void
|
||||
}
|
||||
|
||||
type CreateMode = 'api-key' | 'oauth' | 'manual'
|
||||
|
||||
const SubscriptionModal = ({ pluginDetail, onCancel, onSaved }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [selectedMode, setSelectedMode] = useState<CreateMode | null>(null)
|
||||
const [subscriptionName, setSubscriptionName] = useState('')
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [webhookUrl, setWebhookUrl] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleModeSelect = (mode: CreateMode) => {
|
||||
setSelectedMode(mode)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setSelectedMode(null)
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!selectedMode || !subscriptionName.trim()) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const subscriptionData = {
|
||||
name: subscriptionName,
|
||||
mode: selectedMode,
|
||||
plugin_id: pluginDetail.plugin_id,
|
||||
...(selectedMode === 'api-key' && { api_key: apiKey }),
|
||||
...(selectedMode === 'manual' && { webhook_url: webhookUrl }),
|
||||
}
|
||||
|
||||
onSaved(subscriptionData)
|
||||
}
|
||||
finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const canCreate = subscriptionName.trim() && (
|
||||
selectedMode === 'oauth'
|
||||
|| (selectedMode === 'api-key' && apiKey.trim())
|
||||
|| (selectedMode === 'manual' && webhookUrl.trim())
|
||||
)
|
||||
|
||||
if (!selectedMode) {
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onCancel}
|
||||
className='!max-w-[520px] !p-0'
|
||||
>
|
||||
<div className='flex items-center justify-between p-6 pb-4'>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createSubscription')}
|
||||
</h3>
|
||||
<Button variant='ghost' size='small' onClick={onCancel}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='px-6 pb-2'>
|
||||
<p className='system-sm-regular mb-4 text-text-secondary'>
|
||||
{t('plugin.detailPanel.createSubscriptionDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className='px-6 pb-6'>
|
||||
<div className='space-y-3'>
|
||||
<button
|
||||
onClick={() => handleModeSelect('api-key')}
|
||||
className='flex w-full items-center gap-3 rounded-lg border border-components-panel-border p-4 text-left transition-colors hover:bg-background-default-hover'
|
||||
>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg bg-background-default-subtle'>
|
||||
<RiKeyLine className='h-5 w-5 text-text-warning' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='system-sm-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createViaApiKey')}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('plugin.detailPanel.createViaApiKeyDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleModeSelect('oauth')}
|
||||
className='flex w-full items-center gap-3 rounded-lg border border-components-panel-border p-4 text-left transition-colors hover:bg-background-default-hover'
|
||||
>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg bg-background-default-subtle'>
|
||||
<RiUserLine className='h-5 w-5 text-text-accent' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='system-sm-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createViaOAuth')}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('plugin.detailPanel.createViaOAuthDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleModeSelect('manual')}
|
||||
className='flex w-full items-center gap-3 rounded-lg border border-components-panel-border p-4 text-left transition-colors hover:bg-background-default-hover'
|
||||
>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-lg bg-background-default-subtle'>
|
||||
<RiEditLine className='h-5 w-5 text-text-secondary' />
|
||||
</div>
|
||||
<div className='flex-1'>
|
||||
<div className='system-sm-semibold text-text-primary'>
|
||||
{t('plugin.detailPanel.createManual')}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('plugin.detailPanel.createManualDesc')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={onCancel}
|
||||
className='!max-w-[520px] !p-0'
|
||||
>
|
||||
<div className='flex items-center justify-between p-6 pb-4'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Button variant='ghost' size='small' onClick={handleBack}>
|
||||
←
|
||||
</Button>
|
||||
<h3 className='text-lg font-semibold text-text-primary'>
|
||||
{selectedMode === 'api-key' && t('plugin.detailPanel.createViaApiKey')}
|
||||
{selectedMode === 'oauth' && t('plugin.detailPanel.createViaOAuth')}
|
||||
{selectedMode === 'manual' && t('plugin.detailPanel.createManual')}
|
||||
</h3>
|
||||
</div>
|
||||
<Button variant='ghost' size='small' onClick={onCancel}>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='px-6 pb-6'>
|
||||
<div className='space-y-4'>
|
||||
<div>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('plugin.detailPanel.subscriptionName')}
|
||||
</label>
|
||||
<Input
|
||||
value={subscriptionName}
|
||||
onChange={e => setSubscriptionName(e.target.value)}
|
||||
placeholder={t('plugin.detailPanel.subscriptionNamePlaceholder')}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedMode === 'api-key' && (
|
||||
<div>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('plugin.detailPanel.apiKey')}
|
||||
</label>
|
||||
<Input
|
||||
type='password'
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder={t('plugin.detailPanel.apiKeyPlaceholder')}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMode === 'oauth' && (
|
||||
<div className='rounded-lg bg-background-section p-4'>
|
||||
<p className='system-sm-regular text-text-secondary'>
|
||||
{t('plugin.detailPanel.oauthCreateNote')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedMode === 'manual' && (
|
||||
<div>
|
||||
<label className='system-sm-medium mb-2 block text-text-primary'>
|
||||
{t('plugin.detailPanel.webhookUrl')}
|
||||
</label>
|
||||
<Input
|
||||
value={webhookUrl}
|
||||
onChange={e => setWebhookUrl(e.target.value)}
|
||||
placeholder={t('plugin.detailPanel.webhookUrlPlaceholder')}
|
||||
className='w-full'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-end gap-2'>
|
||||
<Button variant='secondary' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleCreate}
|
||||
disabled={!canCreate}
|
||||
loading={isLoading}
|
||||
>
|
||||
{t('common.operation.create')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubscriptionModal
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ToolItem from '@/app/components/tools/provider/tool-item'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
|
||||
type Props = {
|
||||
detail: PluginDetail
|
||||
}
|
||||
|
||||
export const TriggerEventsList = ({
|
||||
detail,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const triggers = detail.declaration.trigger?.triggers || []
|
||||
|
||||
if (!triggers.length)
|
||||
return null
|
||||
|
||||
// todo: add collection & update ToolItem
|
||||
return (
|
||||
<div className='px-4 pb-4 pt-2'>
|
||||
<div className='mb-1 py-1'>
|
||||
<div className='system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary'>
|
||||
{t('pluginTrigger.events.actionNum', { num: triggers.length, event: triggers.length > 1 ? 'events' : 'event' })}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
{triggers.map(triggerEvent => (
|
||||
<ToolItem
|
||||
key={`${detail.plugin_id}${triggerEvent.identity.name}`}
|
||||
disabled={false}
|
||||
// collection={provider}
|
||||
// @ts-expect-error triggerEvent.identity.label is Record<Locale, string>
|
||||
tool={{
|
||||
label: triggerEvent.identity.label as any,
|
||||
description: triggerEvent.description.human,
|
||||
}}
|
||||
isBuiltIn={false}
|
||||
isModel={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,11 +3,14 @@ import type { ToolCredential } from '@/app/components/tools/types'
|
|||
import type { Locale } from '@/i18n-config'
|
||||
import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types'
|
||||
import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types'
|
||||
import type { FormTypeEnum } from '../base/form/types'
|
||||
|
||||
export enum PluginType {
|
||||
tool = 'tool',
|
||||
model = 'model',
|
||||
extension = 'extension',
|
||||
agent = 'agent-strategy',
|
||||
trigger = 'trigger',
|
||||
}
|
||||
|
||||
export enum PluginSource {
|
||||
|
|
@ -80,6 +83,103 @@ export type PluginDeclaration = {
|
|||
tags: string[]
|
||||
agent_strategy: any
|
||||
meta: PluginDeclarationMeta
|
||||
trigger: PluginTriggerDefinition
|
||||
}
|
||||
|
||||
export type PluginTriggerDefinition = {
|
||||
identity: Identity
|
||||
credentials_schema: CredentialsSchema[]
|
||||
oauth_schema: OauthSchema
|
||||
subscription_schema: SubscriptionSchema
|
||||
triggers: Trigger[]
|
||||
}
|
||||
|
||||
export type CredentialsSchema = {
|
||||
name: string
|
||||
label: Record<Locale, string>
|
||||
description: Record<Locale, string>
|
||||
type: FormTypeEnum
|
||||
scope: any
|
||||
required: boolean
|
||||
default: any
|
||||
options: any
|
||||
help: Record<Locale, string>
|
||||
url: string
|
||||
placeholder: Record<Locale, string>
|
||||
}
|
||||
|
||||
export type OauthSchema = {
|
||||
client_schema: CredentialsSchema[]
|
||||
credentials_schema: CredentialsSchema[]
|
||||
}
|
||||
|
||||
export type SubscriptionSchema = {
|
||||
parameters_schema: ParametersSchema[]
|
||||
properties_schema: PropertiesSchema[]
|
||||
}
|
||||
|
||||
export type ParametersSchema = {
|
||||
name: string
|
||||
label: Record<Locale, string>
|
||||
type: FormTypeEnum
|
||||
auto_generate: any
|
||||
template: any
|
||||
scope: any
|
||||
required: boolean
|
||||
multiple: boolean
|
||||
default?: string[]
|
||||
min: any
|
||||
max: any
|
||||
precision: any
|
||||
options?: Array<{
|
||||
value: string
|
||||
label: Record<Locale, string>
|
||||
icon?: string
|
||||
}>
|
||||
description: Record<Locale, string>
|
||||
}
|
||||
|
||||
export type PropertiesSchema = {
|
||||
type: FormTypeEnum
|
||||
name: string
|
||||
scope: any
|
||||
required: boolean
|
||||
default: any
|
||||
options: Array<{
|
||||
value: string
|
||||
label: Record<Locale, string>
|
||||
icon?: string
|
||||
}>
|
||||
label: Record<Locale, string>
|
||||
help: Record<Locale, string>
|
||||
url: any
|
||||
placeholder: any
|
||||
}
|
||||
|
||||
export type Trigger = {
|
||||
identity: Identity
|
||||
description: Record<Locale, string>
|
||||
parameters: {
|
||||
name: string
|
||||
label: Record<Locale, string>
|
||||
type: string
|
||||
auto_generate: any
|
||||
template: any
|
||||
scope: any
|
||||
required: boolean
|
||||
multiple: boolean
|
||||
default: any
|
||||
min: any
|
||||
max: any
|
||||
precision: any
|
||||
options?: Array<{
|
||||
value: string
|
||||
label: Record<Locale, string>
|
||||
icon?: string
|
||||
}>
|
||||
description?: Record<Locale, string>
|
||||
}[]
|
||||
output_schema: Record<string, any>
|
||||
}
|
||||
|
||||
export type PluginManifestInMarket = {
|
||||
|
|
@ -461,15 +561,18 @@ export type StrategyDetail = {
|
|||
features: AgentFeature[]
|
||||
}
|
||||
|
||||
export type Identity = {
|
||||
author: string
|
||||
name: string
|
||||
label: Record<Locale, string>
|
||||
description: Record<Locale, string>
|
||||
icon: string
|
||||
icon_dark?: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export type StrategyDeclaration = {
|
||||
identity: {
|
||||
author: string
|
||||
name: string
|
||||
description: Record<Locale, string>
|
||||
icon: string
|
||||
label: Record<Locale, string>
|
||||
tags: string[]
|
||||
},
|
||||
identity: Identity,
|
||||
plugin_id: string
|
||||
strategies: StrategyDetail[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export type TriggerParameter = {
|
|||
label: TypeWithI18N
|
||||
description?: TypeWithI18N
|
||||
type: 'string' | 'number' | 'boolean' | 'select' | 'file' | 'files'
|
||||
| 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select'
|
||||
| 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select'
|
||||
auto_generate?: {
|
||||
type: string
|
||||
value?: any
|
||||
|
|
@ -105,7 +105,7 @@ export type TriggerParameter = {
|
|||
|
||||
export type TriggerCredentialField = {
|
||||
type: 'secret-input' | 'text-input' | 'select' | 'boolean'
|
||||
| 'app-selector' | 'model-selector' | 'tools-selector'
|
||||
| 'app-selector' | 'model-selector' | 'tools-selector'
|
||||
name: string
|
||||
scope?: string | null
|
||||
required: boolean
|
||||
|
|
@ -173,28 +173,45 @@ export type TriggerWithProvider = Collection & {
|
|||
// ===== API Service Types =====
|
||||
|
||||
// Trigger subscription instance types
|
||||
export type TriggerSubscription = {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
credential_type: 'api_key' | 'oauth2' | 'unauthorized'
|
||||
credentials: Record<string, any>
|
||||
endpoint: string
|
||||
parameters: Record<string, any>
|
||||
properties: Record<string, any>
|
||||
|
||||
export enum TriggerCredentialTypeEnum {
|
||||
ApiKey = 'api-key',
|
||||
Oauth2 = 'oauth2',
|
||||
Unauthorized = 'unauthorized',
|
||||
}
|
||||
|
||||
export type TriggerSubscriptionBuilder = {
|
||||
type TriggerSubscriptionStructure = {
|
||||
id: string
|
||||
name: string
|
||||
provider: string
|
||||
credential_type: TriggerCredentialTypeEnum
|
||||
credentials: TriggerSubCredentials
|
||||
endpoint: string
|
||||
parameters: Record<string, any>
|
||||
properties: Record<string, any>
|
||||
credentials: Record<string, any>
|
||||
credential_type: 'api_key' | 'oauth2' | 'unauthorized'
|
||||
parameters: TriggerSubParameters
|
||||
properties: TriggerSubProperties
|
||||
}
|
||||
|
||||
export type TriggerSubscription = TriggerSubscriptionStructure
|
||||
|
||||
export type TriggerSubCredentials = {
|
||||
access_tokens: string
|
||||
}
|
||||
|
||||
export type TriggerSubParameters = {
|
||||
repository: string
|
||||
webhook_secret?: string
|
||||
}
|
||||
|
||||
export type TriggerSubProperties = {
|
||||
active: boolean
|
||||
events: string[]
|
||||
external_id: string
|
||||
repository: string
|
||||
webhook_secret?: string
|
||||
}
|
||||
|
||||
export type TriggerSubscriptionBuilder = TriggerSubscriptionStructure
|
||||
|
||||
// OAuth configuration types
|
||||
export type TriggerOAuthConfig = {
|
||||
configured: boolean
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const NAMESPACES = [
|
|||
'login',
|
||||
'oauth',
|
||||
'plugin-tags',
|
||||
'plugin-trigger',
|
||||
'plugin',
|
||||
'register',
|
||||
'run-log',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
const translation = {
|
||||
subscription: {
|
||||
title: 'Subscriptions',
|
||||
empty: {
|
||||
title: 'No subscriptions',
|
||||
description: 'Create your first subscription to start receiving events',
|
||||
button: 'New subscription',
|
||||
},
|
||||
list: {
|
||||
title: 'Subscriptions',
|
||||
addButton: 'Add',
|
||||
item: {
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
credentialType: {
|
||||
api_key: 'API Key',
|
||||
oauth2: 'OAuth',
|
||||
unauthorized: 'Manual',
|
||||
},
|
||||
actions: {
|
||||
delete: 'Delete',
|
||||
deleteConfirm: {
|
||||
title: 'Delete subscription',
|
||||
content: 'Are you sure you want to delete "{{name}}"?',
|
||||
contentWithApps: 'This subscription is being used in {{count}} apps. Are you sure you want to delete "{{name}}"?',
|
||||
confirm: 'Delete',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
active: 'Active',
|
||||
inactive: 'Inactive',
|
||||
},
|
||||
},
|
||||
},
|
||||
addType: {
|
||||
title: 'Add subscription',
|
||||
description: 'Choose how you want to create your trigger subscription',
|
||||
options: {
|
||||
apiKey: {
|
||||
title: 'Via API Key',
|
||||
description: 'Automatically create subscription using API credentials',
|
||||
},
|
||||
oauth: {
|
||||
title: 'Via OAuth',
|
||||
description: 'Authorize with third-party platform to create subscription',
|
||||
},
|
||||
manual: {
|
||||
title: 'Manual Setup',
|
||||
description: 'Manually configure webhook URL and settings',
|
||||
tip: 'Configure URL on third-party platform manually',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
steps: {
|
||||
verify: 'Verify',
|
||||
configuration: 'Configuration',
|
||||
},
|
||||
common: {
|
||||
cancel: 'Cancel',
|
||||
back: 'Back',
|
||||
next: 'Next',
|
||||
create: 'Create',
|
||||
verify: 'Verify',
|
||||
authorize: 'Authorize',
|
||||
creating: 'Creating...',
|
||||
verifying: 'Verifying...',
|
||||
authorizing: 'Authorizing...',
|
||||
},
|
||||
apiKey: {
|
||||
title: 'Create via API Key',
|
||||
verify: {
|
||||
title: 'Verify Credentials',
|
||||
description: 'Please provide your API credentials to verify access',
|
||||
error: 'Credential verification failed. Please check your API key.',
|
||||
success: 'Credentials verified successfully',
|
||||
},
|
||||
configuration: {
|
||||
title: 'Configure Subscription',
|
||||
description: 'Set up your subscription parameters',
|
||||
},
|
||||
},
|
||||
oauth: {
|
||||
title: 'Create via OAuth',
|
||||
authorization: {
|
||||
title: 'OAuth Authorization',
|
||||
description: 'Authorize Dify to access your account',
|
||||
redirectUrl: 'Redirect URL',
|
||||
redirectUrlHelp: 'Use this URL in your OAuth app configuration',
|
||||
authorizeButton: 'Authorize with {{provider}}',
|
||||
waitingAuth: 'Waiting for authorization...',
|
||||
authSuccess: 'Authorization successful',
|
||||
authFailed: 'Authorization failed',
|
||||
},
|
||||
configuration: {
|
||||
title: 'Configure Subscription',
|
||||
description: 'Set up your subscription parameters after authorization',
|
||||
},
|
||||
},
|
||||
manual: {
|
||||
title: 'Manual Setup',
|
||||
description: 'Configure your webhook subscription manually',
|
||||
instruction: {
|
||||
title: 'Setup Instructions',
|
||||
step1: '1. Copy the callback URL below',
|
||||
step2: '2. Go to your third-party platform webhook settings',
|
||||
step3: '3. Add the callback URL as a webhook endpoint',
|
||||
step4: '4. Configure the events you want to receive',
|
||||
step5: '5. Test the webhook by triggering an event',
|
||||
step6: '6. Return here to verify the webhook is working and complete setup',
|
||||
},
|
||||
logs: {
|
||||
title: 'Request Logs',
|
||||
description: 'Monitor incoming webhook requests',
|
||||
empty: 'No requests received yet. Make sure to test your webhook configuration.',
|
||||
status: {
|
||||
success: 'Success',
|
||||
error: 'Error',
|
||||
},
|
||||
expandAll: 'Expand All',
|
||||
collapseAll: 'Collapse All',
|
||||
timestamp: 'Timestamp',
|
||||
method: 'Method',
|
||||
path: 'Path',
|
||||
headers: 'Headers',
|
||||
body: 'Body',
|
||||
response: 'Response',
|
||||
},
|
||||
},
|
||||
form: {
|
||||
subscriptionName: {
|
||||
label: 'Subscription Name',
|
||||
placeholder: 'Enter subscription name',
|
||||
required: 'Subscription name is required',
|
||||
},
|
||||
callbackUrl: {
|
||||
label: 'Callback URL',
|
||||
description: 'This URL will receive webhook events',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied!',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
createFailed: 'Failed to create subscription',
|
||||
verifyFailed: 'Failed to verify credentials',
|
||||
authFailed: 'Authorization failed',
|
||||
networkError: 'Network error, please try again',
|
||||
},
|
||||
},
|
||||
events: {
|
||||
title: 'Available Events',
|
||||
description: 'Events that this trigger plugin can subscribe to',
|
||||
empty: 'No events available',
|
||||
actionNum: '{{num}} {{event}} INCLUDED',
|
||||
item: {
|
||||
parameters: '{{count}} parameters',
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
github: 'GitHub',
|
||||
gitlab: 'GitLab',
|
||||
notion: 'Notion',
|
||||
webhook: 'Webhook',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
const translation = {
|
||||
subscription: {
|
||||
title: '订阅',
|
||||
empty: {
|
||||
title: '暂无订阅',
|
||||
description: '创建您的第一个订阅以开始接收事件',
|
||||
button: '新建订阅',
|
||||
},
|
||||
list: {
|
||||
title: '订阅列表',
|
||||
addButton: '添加',
|
||||
item: {
|
||||
enabled: '已启用',
|
||||
disabled: '已禁用',
|
||||
credentialType: {
|
||||
api_key: 'API密钥',
|
||||
oauth2: 'OAuth',
|
||||
unauthorized: '手动',
|
||||
},
|
||||
actions: {
|
||||
delete: '删除',
|
||||
deleteConfirm: {
|
||||
title: '删除订阅',
|
||||
content: '确定要删除"{{name}}"吗?',
|
||||
contentWithApps: '该订阅正在被{{count}}个应用使用。确定要删除"{{name}}"吗?',
|
||||
confirm: '删除',
|
||||
cancel: '取消',
|
||||
},
|
||||
},
|
||||
status: {
|
||||
active: '活跃',
|
||||
inactive: '非活跃',
|
||||
},
|
||||
},
|
||||
},
|
||||
addType: {
|
||||
title: '添加订阅',
|
||||
description: '选择创建触发器订阅的方式',
|
||||
options: {
|
||||
apiKey: {
|
||||
title: '通过API密钥',
|
||||
description: '使用API凭据自动创建订阅',
|
||||
},
|
||||
oauth: {
|
||||
title: '通过OAuth',
|
||||
description: '与第三方平台授权以创建订阅',
|
||||
},
|
||||
manual: {
|
||||
title: '手动设置',
|
||||
description: '手动配置Webhook URL和设置',
|
||||
tip: '手动配置 URL 到第三方平台',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
steps: {
|
||||
verify: '验证',
|
||||
configuration: '配置',
|
||||
},
|
||||
common: {
|
||||
cancel: '取消',
|
||||
back: '返回',
|
||||
next: '下一步',
|
||||
create: '创建',
|
||||
verify: '验证',
|
||||
authorize: '授权',
|
||||
creating: '创建中...',
|
||||
verifying: '验证中...',
|
||||
authorizing: '授权中...',
|
||||
},
|
||||
apiKey: {
|
||||
title: '通过API密钥创建',
|
||||
verify: {
|
||||
title: '验证凭据',
|
||||
description: '请提供您的API凭据以验证访问权限',
|
||||
error: '凭据验证失败,请检查您的API密钥。',
|
||||
success: '凭据验证成功',
|
||||
},
|
||||
configuration: {
|
||||
title: '配置订阅',
|
||||
description: '设置您的订阅参数',
|
||||
},
|
||||
},
|
||||
oauth: {
|
||||
title: '通过OAuth创建',
|
||||
authorization: {
|
||||
title: 'OAuth授权',
|
||||
description: '授权Dify访问您的账户',
|
||||
redirectUrl: '重定向URL',
|
||||
redirectUrlHelp: '在您的OAuth应用配置中使用此URL',
|
||||
authorizeButton: '使用{{provider}}授权',
|
||||
waitingAuth: '等待授权中...',
|
||||
authSuccess: '授权成功',
|
||||
authFailed: '授权失败',
|
||||
},
|
||||
configuration: {
|
||||
title: '配置订阅',
|
||||
description: '授权完成后设置您的订阅参数',
|
||||
},
|
||||
},
|
||||
manual: {
|
||||
title: '手动设置',
|
||||
description: '手动配置您的Webhook订阅',
|
||||
instruction: {
|
||||
title: '设置说明',
|
||||
step1: '1. 复制下方的回调URL',
|
||||
step2: '2. 前往您的第三方平台Webhook设置',
|
||||
step3: '3. 将回调URL添加为Webhook端点',
|
||||
step4: '4. 配置您想要接收的事件',
|
||||
step5: '5. 通过触发事件来测试Webhook',
|
||||
step6: '6. 返回此处验证Webhook正常工作并完成设置',
|
||||
},
|
||||
logs: {
|
||||
title: '请求日志',
|
||||
description: '监控传入的Webhook请求',
|
||||
empty: '尚未收到任何请求。请确保测试您的Webhook配置。',
|
||||
status: {
|
||||
success: '成功',
|
||||
error: '错误',
|
||||
},
|
||||
expandAll: '展开全部',
|
||||
collapseAll: '收起全部',
|
||||
timestamp: '时间戳',
|
||||
method: '方法',
|
||||
path: '路径',
|
||||
headers: '请求头',
|
||||
body: '请求体',
|
||||
response: '响应',
|
||||
},
|
||||
},
|
||||
form: {
|
||||
subscriptionName: {
|
||||
label: '订阅名称',
|
||||
placeholder: '输入订阅名称',
|
||||
required: '订阅名称为必填项',
|
||||
},
|
||||
callbackUrl: {
|
||||
label: '回调URL',
|
||||
description: '此URL将接收Webhook事件',
|
||||
copy: '复制',
|
||||
copied: '已复制!',
|
||||
},
|
||||
},
|
||||
errors: {
|
||||
createFailed: '创建订阅失败',
|
||||
verifyFailed: '验证凭据失败',
|
||||
authFailed: '授权失败',
|
||||
networkError: '网络错误,请重试',
|
||||
},
|
||||
},
|
||||
events: {
|
||||
title: '可用事件',
|
||||
description: '此触发器插件可以订阅的事件',
|
||||
empty: '没有可用事件',
|
||||
eventNum: '包含 {{num}} 个 {{event}}',
|
||||
item: {
|
||||
parameters: '{{count}}个参数',
|
||||
},
|
||||
},
|
||||
provider: {
|
||||
github: 'GitHub',
|
||||
gitlab: 'GitLab',
|
||||
notion: 'Notion',
|
||||
webhook: 'Webhook',
|
||||
},
|
||||
}
|
||||
|
||||
export default translation
|
||||
|
|
@ -95,7 +95,7 @@ export const useInvalidateAllTriggerPlugins = () => {
|
|||
// ===== Trigger Subscriptions Management =====
|
||||
export const useTriggerSubscriptions = (provider: string, enabled = true) => {
|
||||
return useQuery<TriggerSubscription[]>({
|
||||
queryKey: [NAME_SPACE, 'subscriptions', provider],
|
||||
queryKey: [NAME_SPACE, 'list-subscriptions', provider],
|
||||
queryFn: () => get<TriggerSubscription[]>(`/workspaces/current/trigger-provider/${provider}/subscriptions/list`),
|
||||
enabled: enabled && !!provider,
|
||||
})
|
||||
|
|
@ -115,8 +115,7 @@ export const useCreateTriggerSubscriptionBuilder = () => {
|
|||
mutationKey: [NAME_SPACE, 'create-subscription-builder'],
|
||||
mutationFn: (payload: {
|
||||
provider: string
|
||||
name?: string
|
||||
credentials?: Record<string, any>
|
||||
credential_type?: string
|
||||
}) => {
|
||||
const { provider, ...body } = payload
|
||||
return post<{ subscription_builder: TriggerSubscriptionBuilder }>(
|
||||
|
|
@ -153,10 +152,12 @@ export const useVerifyTriggerSubscriptionBuilder = () => {
|
|||
mutationFn: (payload: {
|
||||
provider: string
|
||||
subscriptionBuilderId: string
|
||||
credentials?: Record<string, any>
|
||||
}) => {
|
||||
const { provider, subscriptionBuilderId } = payload
|
||||
const { provider, subscriptionBuilderId, ...body } = payload
|
||||
return post(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
|
||||
{ body },
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue