feat: oauth config init

This commit is contained in:
yessenia
2025-09-16 16:56:23 +08:00
parent a173dc5c9d
commit 814b0e1fe8
23 changed files with 949 additions and 303 deletions

View File

@@ -15,6 +15,17 @@ import { useRenderI18nObject } from '@/hooks/use-i18n'
import Radio from '@/app/components/base/radio'
import RadioE from '@/app/components/base/radio/ui'
const getInputType = (type: FormTypeEnum) => {
switch (type) {
case FormTypeEnum.secretInput:
return 'password'
case FormTypeEnum.textNumber:
return 'number'
default:
return 'text'
}
}
export type BaseFieldProps = {
fieldClassName?: string
labelClassName?: string
@@ -24,6 +35,7 @@ export type BaseFieldProps = {
field: AnyFieldApi
disabled?: boolean
}
const BaseField = ({
fieldClassName,
labelClassName,
@@ -42,19 +54,19 @@ const BaseField = ({
labelClassName: formLabelClassName,
show_on = [],
disabled: formSchemaDisabled,
showRadioUI,
type: formItemType,
} = formSchema
const disabled = propsDisabled || formSchemaDisabled
const memorizedLabel = useMemo(() => {
if (isValidElement(label))
return label
if (typeof label === 'string')
if (isValidElement(label) || typeof label === 'string')
return label
if (typeof label === 'object' && label !== null)
return renderI18nObject(label as Record<string, string>)
}, [label, renderI18nObject])
const memorizedPlaceholder = useMemo(() => {
if (typeof placeholder === 'string')
return placeholder
@@ -62,25 +74,36 @@ const BaseField = ({
if (typeof placeholder === 'object' && placeholder !== null)
return renderI18nObject(placeholder as Record<string, string>)
}, [placeholder, renderI18nObject])
const optionValues = useStore(field.form.store, (s) => {
const watchedVariables = useMemo(() => {
const variables = new Set<string>()
for (const option of options || []) {
for (const condition of option.show_on || [])
variables.add(condition.variable)
}
for (const condition of show_on || [])
variables.add(condition.variable)
return Array.from(variables)
}, [options, show_on])
const watchedValues = useStore(field.form.store, (s) => {
const result: Record<string, any> = {}
options?.forEach((option) => {
if (option.show_on?.length) {
option.show_on.forEach((condition) => {
result[condition.variable] = s.values[condition.variable]
})
}
})
for (const variable of watchedVariables)
result[variable] = s.values[variable]
return result
})
const memorizedOptions = useMemo(() => {
return options?.filter((option) => {
if (!option.show_on || option.show_on.length === 0)
if (!option.show_on?.length)
return true
return option.show_on.every((condition) => {
const conditionValue = optionValues[condition.variable]
return conditionValue === condition.value
return watchedValues[condition.variable] === condition.value
})
}).map((option) => {
return {
@@ -88,20 +111,15 @@ const BaseField = ({
value: option.value,
}
}) || []
}, [options, renderI18nObject, optionValues])
}, [options, renderI18nObject, watchedValues])
const value = useStore(field.form.store, s => s.values[field.name])
const values = useStore(field.form.store, (s) => {
return show_on.reduce((acc, condition) => {
acc[condition.variable] = s.values[condition.variable]
return acc
}, {} as Record<string, any>)
})
const show = useMemo(() => {
return show_on.every((condition) => {
const conditionValue = values[condition.variable]
return conditionValue === condition.value
return watchedValues[condition.variable] === condition.value
})
}, [values, show_on])
}, [watchedValues, show_on])
const booleanRadioValue = useMemo(() => {
if (value === null || value === undefined)
@@ -124,7 +142,7 @@ const BaseField = ({
</div>
<div className={cn(inputContainerClassName)}>
{
formSchema.type === FormTypeEnum.textInput && (
[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
<Input
id={field.name}
name={field.name}
@@ -134,41 +152,12 @@ const BaseField = ({
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
type={getInputType(formItemType)}
/>
)
}
{
formSchema.type === FormTypeEnum.secretInput && (
<Input
id={field.name}
name={field.name}
type='password'
className={cn(inputClassName)}
value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
/>
)
}
{
formSchema.type === FormTypeEnum.textNumber && (
<Input
id={field.name}
name={field.name}
type='number'
className={cn(inputClassName)}
value={value || ''}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
/>
)
}
{
formSchema.type === FormTypeEnum.select && (
formItemType === FormTypeEnum.select && (
<PureSelect
value={value}
onChange={v => field.handleChange(v)}
@@ -180,7 +169,7 @@ const BaseField = ({
)
}
{
formSchema.type === FormTypeEnum.radio && (
formItemType === FormTypeEnum.radio && (
<div className={cn(
memorizedOptions.length < 3 ? 'flex items-center space-x-2' : 'space-y-2',
)}>
@@ -189,21 +178,14 @@ const BaseField = ({
<div
key={option.value}
className={cn(
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
disabled && 'cursor-not-allowed opacity-50',
inputClassName,
)}
onClick={() => !disabled && field.handleChange(option.value)}
>
{
formSchema.showRadioUI && (
<RadioE
className='mr-2'
isChecked={value === option.value}
/>
)
}
{showRadioUI && <RadioE isChecked={value === option.value} />}
{option.label}
</div>
))
@@ -212,13 +194,13 @@ const BaseField = ({
)
}
{
formSchema.type === FormTypeEnum.boolean && (
formItemType === FormTypeEnum.boolean && (
<Radio.Group
className='flex w-fit items-center'
className='flex w-fit items-center gap-1'
value={booleanRadioValue}
onChange={val => field.handleChange(val === 1)}
>
<Radio value={1} className='!mr-1'>True</Radio>
<Radio value={1}>True</Radio>
<Radio value={0}>False</Radio>
</Radio.Group>
)
@@ -233,9 +215,7 @@ const BaseField = ({
<span className='break-all'>
{renderI18nObject(formSchema?.help as any)}
</span>
{
<RiExternalLinkLine className='ml-1 h-3 w-3' />
}
<RiExternalLinkLine className='ml-1 h-3 w-3' />
</a>
)
}

View File

@@ -28,6 +28,7 @@ export type Item = {
name: string
isGroup?: boolean
disabled?: boolean
extra?: React.ReactNode
} & Record<string, any>
export type ISelectProps = {
@@ -402,6 +403,7 @@ const PortalSelect: FC<PortalSelectProps> = ({
{!hideChecked && item.value === value && (
<RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
)}
{item.extra}
</div>
))}
</div>

View File

@@ -2,6 +2,7 @@ export enum AuthCategory {
tool = 'tool',
datasource = 'datasource',
model = 'model',
trigger = 'trigger',
}
export type PluginPayload = {

View File

@@ -1,5 +1,5 @@
'use client'
import React from 'react'
import React, { useEffect } from 'react'
import type { FC } from 'react'
import DetailHeader from './detail-header'
import EndpointList from './endpoint-list'
@@ -11,6 +11,7 @@ import { TriggerEventsList } from './trigger-events-list'
import Drawer from '@/app/components/base/drawer'
import { type PluginDetail, PluginType } from '@/app/components/plugins/types'
import cn from '@/utils/classnames'
import { usePluginStore } from './store'
type Props = {
detail?: PluginDetail
@@ -28,6 +29,12 @@ const PluginDetailPanel: FC<Props> = ({
onHide()
onUpdate()
}
const { setDetail } = usePluginStore()
useEffect(() => {
if (detail)
setDetail(detail)
}, [detail])
if (!detail)
return null
@@ -52,8 +59,8 @@ const PluginDetailPanel: FC<Props> = ({
<div className='grow overflow-y-auto'>
{detail.declaration.category === PluginType.trigger && (
<>
<SubscriptionList detail={detail} />
<TriggerEventsList detail={detail} />
<SubscriptionList />
<TriggerEventsList />
</>
)}
{!!detail.declaration.tool && <ActionList detail={detail} />}

View File

@@ -0,0 +1,22 @@
import { create } from 'zustand'
import type { PluginDetail } from '../types'
type Shape = {
detail: PluginDetail | undefined
setDetail: (detail: PluginDetail) => void
}
export const usePluginStore = create<Shape>(set => ({
detail: undefined,
setDetail: (detail: PluginDetail) => set({ detail }),
}))
type ShapeSubscription = {
refresh?: () => void
setRefresh: (refresh: () => void) => void
}
export const usePluginSubscriptionStore = create<ShapeSubscription>(set => ({
refresh: undefined,
setRefresh: (refresh: () => void) => set({ refresh }),
}))

View File

@@ -17,11 +17,10 @@ import {
useCreateTriggerSubscriptionBuilder,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import type { PluginDetail } from '@/app/components/plugins/types'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import { usePluginStore } from '../../store'
type Props = {
pluginDetail: PluginDetail
onClose: () => void
onSuccess: () => void
}
@@ -31,9 +30,9 @@ enum ApiKeyStep {
Configuration = 'configuration',
}
const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
export const ApiKeyCreateModal = ({ onClose, onSuccess }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
// State
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(ApiKeyStep.Verify)
const [subscriptionName, setSubscriptionName] = useState('')
@@ -50,9 +49,9 @@ const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
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 providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
const credentialsSchema = detail?.declaration.trigger?.credentials_schema || []
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || []
const handleVerify = () => {
const credentialsFormValues = credentialsFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
@@ -310,5 +309,3 @@ const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
</Modal>
)
}
export default ApiKeyAddModal

View File

@@ -0,0 +1,296 @@
'use client'
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
import { BaseForm } from '@/app/components/base/form/components/base'
import type { FormRefObject } from '@/app/components/base/form/types'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal/modal'
import Toast from '@/app/components/base/toast'
import { SupportedCreationMethods } from '@/app/components/plugins/types'
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import {
useBuildTriggerSubscription,
useCreateTriggerSubscriptionBuilder,
useTriggerSubscriptionBuilderLogs,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import { RiLoader2Line } from '@remixicon/react'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePluginStore } from '../../store'
import LogViewer from '../log-viewer'
type Props = {
onClose: () => void
createType: SupportedCreationMethods
}
enum ApiKeyStep {
Verify = 'verify',
Configuration = 'configuration',
}
const StatusStep = ({ isActive, text }: { isActive: boolean, text: string }) => {
return <div className={`system-2xs-semibold-uppercase ${isActive
? 'text-state-accent-solid'
: 'text-text-tertiary'}`}>
{text}
</div>
}
const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => {
const { t } = useTranslation()
return <div className='mb-2 flex w-1/3 items-center gap-2'>
<StatusStep isActive={currentStep === ApiKeyStep.Verify} text={t('pluginTrigger.modal.steps.verify')} />
<div className='h-px w-3 bg-divider-subtle'></div>
<StatusStep isActive={currentStep === ApiKeyStep.Configuration} text={t('pluginTrigger.modal.steps.configuration')} />
</div>
}
export const CommonCreateModal = ({ onClose, createType }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const [currentStep, setCurrentStep] = useState<ApiKeyStep>(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration)
const [subscriptionName, setSubscriptionName] = useState('')
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
const [verificationError, setVerificationError] = useState<string>('')
const { mutate: verifyCredentials, isPending: isVerifyingCredentials } = useVerifyTriggerSubscriptionBuilder()
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
const propertiesSchema = detail?.declaration.trigger.subscription_schema.properties_schema || [] // manual
const propertiesFormRef = React.useRef<FormRefObject>(null)
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || [] // apikey and oauth
const parametersFormRef = React.useRef<FormRefObject>(null)
const credentialsSchema = detail?.declaration.trigger?.credentials_schema || []
const credentialsFormRef = React.useRef<FormRefObject>(null)
const { data: logData } = useTriggerSubscriptionBuilderLogs(
providerName,
subscriptionBuilder?.id || '',
{
enabled: createType === SupportedCreationMethods.MANUAL && !!subscriptionBuilder?.id,
refetchInterval: 3000,
},
)
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 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('')
verifyCredentials(
{
provider: providerName,
subscriptionBuilderId: subscriptionBuilder?.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'))
},
},
)
}
const handleCreate = () => {
if (!subscriptionName.trim()) {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.form.subscriptionName.required'),
})
return
}
if (!subscriptionBuilder)
return
const formValues = propertiesFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false }
if (!formValues.isCheckValidated) {
Toast.notify({
type: 'error',
message: t('pluginTrigger.modal.form.properties.required'),
})
return
}
buildSubscription(
{
provider: providerName,
subscriptionBuilderId: subscriptionBuilder.id,
params: {
name: subscriptionName,
properties: formValues.values,
},
},
{
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 handleConfirm = () => {
if (currentStep === ApiKeyStep.Verify)
handleVerify()
else
handleCreate()
}
return (
<Modal
title={t(`pluginTrigger.modal.${createType === SupportedCreationMethods.APIKEY ? 'apiKey' : createType.toLowerCase()}.title`)}
confirmButtonText={
currentStep === ApiKeyStep.Verify
? isVerifyingCredentials ? t('pluginTrigger.modal.common.verifying') : t('pluginTrigger.modal.common.verify')
: isBuilding ? t('pluginTrigger.modal.common.creating') : t('pluginTrigger.modal.common.create')
}
onClose={onClose}
onCancel={onClose}
onConfirm={handleConfirm}
>
{createType === SupportedCreationMethods.APIKEY && <MultiSteps currentStep={currentStep} />}
{currentStep === ApiKeyStep.Verify && (
<>
{credentialsSchema.length > 0 && (
<div className='mb-4'>
<BaseForm
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>
)}
</>
)}
{currentStep === ApiKeyStep.Configuration && <div className='max-h-[70vh] overflow-y-auto'>
<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>
<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>
{createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && (
<BaseForm
formSchemas={parametersSchema}
ref={parametersFormRef}
/>
)}
{createType === SupportedCreationMethods.MANUAL && <>
{propertiesSchema.length > 0 && (
<div className='mb-6'>
<BaseForm
formSchemas={propertiesSchema}
ref={propertiesFormRef}
/>
</div>
)}
<div className='mb-6'>
<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>
<div className='mb-1 flex items-center justify-center gap-1 rounded-lg bg-background-section p-3'>
<div className='h-3.5 w-3.5'>
<RiLoader2Line className='h-full w-full animate-spin' />
</div>
<div className='system-xs-regular text-text-tertiary'>
Awaiting request from {detail?.declaration.name}...
</div>
</div>
<LogViewer logs={logData?.logs || []} />
</div>
</>}
</div>}
</Modal>
)
}

View File

@@ -0,0 +1,209 @@
import { ActionButton } from '@/app/components/base/action-button'
import { Button } from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { PortalSelect } from '@/app/components/base/select'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { openOAuthPopup } from '@/hooks/use-oauth'
import { useInitiateTriggerOAuth, useTriggerOAuthConfig, useTriggerProviderInfo } from '@/service/use-triggers'
import cn from '@/utils/classnames'
import { RiAddLine, RiCloseLine, RiEqualizer2Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SupportedCreationMethods } from '../../../types'
import { usePluginStore } from '../../store'
import { CommonCreateModal } from './common-modal'
import { OAuthClientSettingsModal } from './oauth-client'
export const CreateModal = () => {
const { t } = useTranslation()
return (
<Modal
isShow
// onClose={onClose}
className='!max-w-[520px] p-6'
wrapperClassName='!z-[1002]'
>
<div className='flex items-center justify-between pb-3'>
<h3 className='text-lg font-semibold text-text-primary'>
{t('pluginTrigger.modal.oauth.title')}
</h3>
<ActionButton
// onClick={onClose}
>
<RiCloseLine className='h-4 w-4' />
</ActionButton>
</div>
</Modal>
)
}
export enum CreateButtonType {
FULL_BUTTON = 'full-button',
ICON_BUTTON = 'icon-button',
}
type Props = {
className?: string
buttonType?: CreateButtonType
}
export const DEFAULT_METHOD = 'default'
/**
* 区分创建订阅的授权方式有几种
* 1. 只有一种授权方式
* - 按钮直接显示授权方式,点击按钮展示创建订阅弹窗
* 2. 有多种授权方式
* - 下拉框显示授权方式,点击按钮展示下拉框,点击选项展示创建订阅弹窗
* 有订阅与无订阅时,按钮形态不同
* oauth 的授权类型:
* - 是否配置 client_id 和 client_secret
* - 未配置则点击按钮去配置
* - 已配置则点击按钮去创建
* - 固定展示设置按钮
*/
export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BUTTON }: Props) => {
const { t } = useTranslation()
const [selectedCreateType, setSelectedCreateType] = useState<SupportedCreationMethods | null>(null)
const detail = usePluginStore(state => state.detail)
const provider = `${detail?.plugin_id}/${detail?.declaration.name}`
const { data: providerInfo } = useTriggerProviderInfo(provider, !!detail?.plugin_id && !!detail?.declaration.name)
const supportedMethods = providerInfo?.supported_creation_methods || []
const { data: oauthConfig } = useTriggerOAuthConfig(provider, supportedMethods.includes(SupportedCreationMethods.OAUTH))
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
const methodType = supportedMethods.length === 1 ? supportedMethods[0] : DEFAULT_METHOD
const [isShowClientSettingsModal, {
setTrue: showClientSettingsModal,
setFalse: hideClientSettingsModal,
}] = useBoolean(false)
const buttonTextMap = useMemo(() => {
return {
[SupportedCreationMethods.OAUTH]: t('pluginTrigger.subscription.createButton.oauth'),
[SupportedCreationMethods.APIKEY]: t('pluginTrigger.subscription.createButton.apiKey'),
[SupportedCreationMethods.MANUAL]: t('pluginTrigger.subscription.createButton.manual'),
[DEFAULT_METHOD]: t('pluginTrigger.subscription.empty.button'),
}
}, [t])
const onClickClientSettings = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.preventDefault()
showClientSettingsModal()
}
const allOptions = [
{
value: SupportedCreationMethods.OAUTH,
name: t('pluginTrigger.subscription.addType.options.oauth.title'),
extra: <ActionButton onClick={onClickClientSettings}><RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /></ActionButton>,
show: supportedMethods.includes(SupportedCreationMethods.OAUTH),
},
{
value: SupportedCreationMethods.APIKEY,
name: t('pluginTrigger.subscription.addType.options.apiKey.title'),
show: supportedMethods.includes(SupportedCreationMethods.APIKEY),
},
{
value: SupportedCreationMethods.MANUAL,
name: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
tooltip: <Tooltip popupContent={t('pluginTrigger.subscription.addType.options.manual.tip')} />,
show: supportedMethods.includes(SupportedCreationMethods.MANUAL),
},
]
const onChooseCreateType = (type: SupportedCreationMethods) => {
if (type === SupportedCreationMethods.OAUTH) {
if (oauthConfig?.configured) {
initiateOAuth(provider, {
onSuccess: (response) => {
openOAuthPopup(response.authorization_url, (callbackData) => {
if (callbackData) {
Toast.notify({
type: 'success',
message: t('pluginTrigger.modal.oauth.authorized'),
})
setSelectedCreateType(SupportedCreationMethods.OAUTH)
}
})
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
})
},
})
}
else {
showClientSettingsModal()
}
}
else {
setSelectedCreateType(type)
}
}
const onClickCreate = (e: React.MouseEvent<HTMLButtonElement>) => {
if (methodType === DEFAULT_METHOD)
return
e.stopPropagation()
e.preventDefault()
onChooseCreateType(methodType)
}
if (!supportedMethods.length)
return null
return <>
<PortalSelect
readonly={methodType !== DEFAULT_METHOD}
renderTrigger={() => {
return buttonType === CreateButtonType.FULL_BUTTON ? (
<Button
variant='primary'
size='medium'
className='w-full'
onClick={onClickCreate}
>
<RiAddLine className='mr-2 h-4 w-4' />
{buttonTextMap[methodType]}
{methodType === SupportedCreationMethods.OAUTH
&& <ActionButton onClick={onClickClientSettings}>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</ActionButton>
}
</Button>
) : <ActionButton onClick={onClickCreate}>
<RiAddLine className='h-4 w-4' />
</ActionButton>
}}
triggerClassName='h-8'
popupClassName={cn('z-[1000]')}
popupInnerClassName={cn('w-[354px]')}
value={methodType}
items={allOptions.filter(option => option.show)}
onSelect={item => onChooseCreateType(item.value as any)}
/>
{selectedCreateType && (
<CommonCreateModal
createType={selectedCreateType}
onClose={() => setSelectedCreateType(null)}
/>
)}
{isShowClientSettingsModal && (
<OAuthClientSettingsModal
oauthConfig={oauthConfig}
onClose={hideClientSettingsModal}
/>
)}
</>
}

View File

@@ -14,23 +14,23 @@ import {
useCreateTriggerSubscriptionBuilder,
useTriggerSubscriptionBuilderLogs,
} from '@/service/use-triggers'
import type { PluginDetail } from '@/app/components/plugins/types'
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 ActionButton from '@/app/components/base/action-button'
import { CopyFeedbackNew } from '@/app/components/base/copy-feedback'
import type { FormRefObject } from '@/app/components/base/form/types'
import LogViewer from './log-viewer'
import LogViewer from '../log-viewer'
import { usePluginStore } from '../../store'
type Props = {
pluginDetail: PluginDetail
onClose: () => void
onSuccess: () => void
}
const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
export const ManualCreateModal = ({ onClose, onSuccess }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const [subscriptionName, setSubscriptionName] = useState('')
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
@@ -38,8 +38,8 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder()
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}`
const propertiesSchema = pluginDetail.declaration.trigger.subscription_schema.properties_schema || []
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
const propertiesSchema = detail?.declaration.trigger.subscription_schema.properties_schema || []
const propertiesFormRef = React.useRef<FormRefObject>(null)
const { data: logData } = useTriggerSubscriptionBuilderLogs(
@@ -193,7 +193,7 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
<RiLoader2Line className='h-full w-full animate-spin' />
</div>
<div className='system-xs-regular text-text-tertiary'>
Awaiting request from {pluginDetail.declaration.name}...
Awaiting request from {detail?.declaration.name}...
</div>
</div>
@@ -217,5 +217,3 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
</Modal>
)
}
export default ManualAddModal

View File

@@ -0,0 +1,236 @@
'use client'
import Button from '@/app/components/base/button'
import Form from '@/app/components/base/form/form-scenarios/auth'
import type { FormRefObject } from '@/app/components/base/form/types'
import Modal from '@/app/components/base/modal/modal'
import Toast from '@/app/components/base/toast'
import type { TriggerOAuthClientParams, TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import {
useConfigureTriggerOAuth,
useDeleteTriggerOAuth,
useInitiateTriggerOAuth,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import {
RiClipboardLine,
RiInformation2Fill,
} from '@remixicon/react'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { usePluginStore } from '../../store'
type Props = {
oauthConfig?: TriggerOAuthConfig
onClose: () => void
}
enum AuthorizationStatusEnum {
Pending = 'pending',
Success = 'success',
Failed = 'failed',
}
enum ClientTypeEnum {
Default = 'default',
Custom = 'custom',
}
export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const [authorizationUrl, setAuthorizationUrl] = useState('')
const [subscriptionBuilder, setSubscriptionBuilder] = useState<TriggerSubscriptionBuilder | undefined>()
const [authorizationStatus, setAuthorizationStatus] = useState<AuthorizationStatusEnum>()
const [clientType, setClientType] = useState<ClientTypeEnum>(oauthConfig?.custom_enabled ? ClientTypeEnum.Custom : ClientTypeEnum.Default)
const clientFormRef = React.useRef<FormRefObject>(null)
const providerName = useMemo(() => !detail ? '' : `${detail?.plugin_id}/${detail?.declaration.name}`, [detail])
const clientSchema = detail?.declaration.trigger?.oauth_schema?.client_schema || []
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
const { mutate: configureOAuth } = useConfigureTriggerOAuth()
const { mutate: deleteOAuth } = useDeleteTriggerOAuth()
useEffect(() => {
if (providerName && oauthConfig?.params.client_id && oauthConfig?.params.client_secret) {
initiateOAuth(providerName, {
onSuccess: (response) => {
setAuthorizationUrl(response.authorization_url)
setSubscriptionBuilder(response.subscription_builder)
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
})
},
})
}
}, [initiateOAuth, providerName, t, oauthConfig])
useEffect(() => {
if (providerName && subscriptionBuilder && authorizationStatus === AuthorizationStatusEnum.Pending) {
const pollInterval = setInterval(() => {
verifyBuilder(
{
provider: providerName,
subscriptionBuilderId: subscriptionBuilder.id,
},
{
onSuccess: () => {
setAuthorizationStatus(AuthorizationStatusEnum.Success)
// setCurrentStep(OAuthStepEnum.Configuration)
Toast.notify({
type: 'success',
message: t('pluginTrigger.modal.oauth.authorization.authSuccess'),
})
clearInterval(pollInterval)
},
onError: () => {
// Continue polling - auth might still be in progress
},
},
)
}, 3000)
return () => clearInterval(pollInterval)
}
}, [subscriptionBuilder, authorizationStatus, verifyBuilder, providerName, t])
const handleRemove = () => {
deleteOAuth(providerName, {
onSuccess: () => {
onClose()
Toast.notify({
type: 'success',
message: t('pluginTrigger.modal.oauth.configuration.success'),
})
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.oauth.configuration.failed'),
})
},
})
}
const handleSaveOnly = () => {
const clientParams = clientFormRef.current?.getFormValues({})?.values || {}
if (clientParams.client_id === oauthConfig?.params.client_id)
clientParams.client_id = '[__HIDDEN__]'
if (clientParams.client_secret === oauthConfig?.params.client_secret)
clientParams.client_secret = '[__HIDDEN__]'
configureOAuth({
provider: providerName,
client_params: clientParams as TriggerOAuthClientParams,
enabled: clientType === ClientTypeEnum.Custom,
}, {
onSuccess: () => {
onClose()
Toast.notify({
type: 'success',
message: t('pluginTrigger.modal.oauth.configuration.success'),
})
},
onError: (error: any) => {
Toast.notify({
type: 'error',
message: error?.message || t('pluginTrigger.modal.oauth.configuration.failed'),
})
},
})
}
const handleSaveAuthorize = () => {
handleSaveOnly()
if (authorizationUrl) {
setAuthorizationStatus(AuthorizationStatusEnum.Pending)
// Open authorization URL in new window
window.open(authorizationUrl, '_blank', 'width=500,height=600')
}
}
return (
<Modal
title={t('pluginTrigger.modal.oauth.title')}
confirmButtonText={t('plugin.auth.saveAndAuth')}
cancelButtonText={t('plugin.auth.saveOnly')}
extraButtonText={t('common.operation.cancel')}
showExtraButton
extraButtonVariant='secondary'
onExtraButtonClick={onClose}
onClose={onClose}
onCancel={handleSaveOnly}
onConfirm={handleSaveAuthorize}
footerSlot={
oauthConfig?.custom_enabled && oauthConfig?.params && (
<div className='grow'>
<Button
variant='secondary'
className='text-components-button-destructive-secondary-text'
// disabled={disabled || doingAction || !editValues}
onClick={handleRemove}
>
{t('common.operation.remove')}
</Button>
</div>
)
}
>
<span className='system-sm-semibold mb-2 text-text-secondary'>OAuth Client</span>
<div className='mb-4 flex w-full items-start justify-between gap-2'>
{[ClientTypeEnum.Default, ClientTypeEnum.Custom].map(option => (
<OptionCard
key={option}
title={option}
onSelect={() => setClientType(option)}
selected={clientType === option}
className="flex-1"
/>
))}
</div>
{oauthConfig?.redirect_uri && (
<div className='mb-4 flex items-start gap-3 rounded-xl bg-background-section-burn p-4'>
<div className='rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg p-2 shadow-xs shadow-shadow-shadow-3'>
<RiInformation2Fill className='h-5 w-5 shrink-0 text-text-accent' />
</div>
<div className='flex-1 text-text-secondary'>
<div className='system-sm-regular whitespace-pre-wrap leading-4'>
{t('pluginTrigger.modal.oauthRedirectInfo')}
</div>
<div className='system-sm-medium my-1.5 break-all leading-4'>
{oauthConfig.redirect_uri}
</div>
<Button
variant='secondary'
size='small'
onClick={() => {
navigator.clipboard.writeText(oauthConfig.redirect_uri)
Toast.notify({
type: 'success',
message: t('common.actionMsg.copySuccessfully'),
})
}}>
<RiClipboardLine className='mr-1 h-[14px] w-[14px]' />
{t('common.operation.copy')}
</Button>
</div>
</div>
)}
{clientType === ClientTypeEnum.Custom && clientSchema.length > 0 && (
<Form
formSchemas={clientSchema}
ref={clientFormRef}
defaultValues={oauthConfig?.params}
/>
)}
</Modal >
)
}

View File

@@ -15,15 +15,14 @@ import type { FormRefObject } from '@/app/components/base/form/types'
import {
useBuildTriggerSubscription,
useInitiateTriggerOAuth,
useTriggerOAuthConfig,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import type { PluginDetail } from '@/app/components/plugins/types'
import ActionButton from '@/app/components/base/action-button'
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import type { TriggerOAuthConfig, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { usePluginStore } from '../../store'
type Props = {
pluginDetail: PluginDetail
oauthConfig?: TriggerOAuthConfig
onClose: () => void
onSuccess: () => void
}
@@ -39,9 +38,9 @@ enum AuthorizationStatusEnum {
Failed = 'failed',
}
const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
export const OAuthCreateModal = ({ oauthConfig, onClose, onSuccess }: Props) => {
const { t } = useTranslation()
const detail = usePluginStore(state => state.detail)
const [currentStep, setCurrentStep] = useState<OAuthStepEnum>(OAuthStepEnum.Setup)
const [subscriptionName, setSubscriptionName] = useState('')
const [authorizationUrl, setAuthorizationUrl] = useState('')
@@ -51,16 +50,14 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
const clientFormRef = React.useRef<FormRefObject>(null)
const parametersFormRef = React.useRef<FormRefObject>(null)
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 || []
const providerName = `${detail?.plugin_id}/${detail?.declaration.name}`
const clientSchema = detail?.declaration.trigger?.oauth_schema?.client_schema || []
const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || []
const { mutate: initiateOAuth } = useInitiateTriggerOAuth()
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
const { data: oauthConfig } = useTriggerOAuthConfig(providerName)
useEffect(() => {
initiateOAuth(providerName, {
onSuccess: (response) => {
@@ -290,5 +287,3 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
</Modal>
)
}
export default OAuthAddModal

View File

@@ -5,21 +5,19 @@ import { RiEqualizer2Line } from '@remixicon/react'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import { ActionButton } from '@/app/components/base/action-button'
enum SubscriptionAddTypeEnum {
OAuth = 'oauth',
APIKey = 'api-key',
Manual = 'manual',
}
import { SupportedCreationMethods } from '../../../types'
import type { TriggerOAuthConfig } from '@/app/components/workflow/block-selector/types'
type Props = {
onSelect: (type: SubscriptionAddTypeEnum) => void
onSelect: (type: SupportedCreationMethods) => void
onClose: () => void
position?: 'bottom' | 'right'
className?: string
supportedMethods: SupportedCreationMethods[]
oauthConfig?: TriggerOAuthConfig
}
const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom', className }: Props) => {
export const CreateTypeDropdown = ({ onSelect, onClose, position = 'bottom', className, supportedMethods }: Props) => {
const { t } = useTranslation()
const dropdownRef = useRef<HTMLDivElement>(null)
@@ -37,24 +35,29 @@ const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom', className }:
// todo: show client settings
}
const options = [
const allOptions = [
{
key: SubscriptionAddTypeEnum.OAuth,
key: SupportedCreationMethods.OAUTH,
title: t('pluginTrigger.subscription.addType.options.oauth.title'),
extraContent: <ActionButton onClick={onClickClientSettings}><RiEqualizer2Line className='h-4 w-4 text-text-tertiary' /></ActionButton>,
show: supportedMethods.includes(SupportedCreationMethods.OAUTH),
},
{
key: SubscriptionAddTypeEnum.APIKey,
key: SupportedCreationMethods.APIKEY,
title: t('pluginTrigger.subscription.addType.options.apiKey.title'),
show: supportedMethods.includes(SupportedCreationMethods.APIKEY),
},
{
key: SubscriptionAddTypeEnum.Manual,
key: SupportedCreationMethods.MANUAL,
title: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题
tooltip: t('pluginTrigger.subscription.addType.options.manual.tip'),
show: supportedMethods.includes(SupportedCreationMethods.MANUAL),
},
]
const handleOptionClick = (type: SubscriptionAddTypeEnum) => {
const options = allOptions.filter(option => option.show)
const handleOptionClick = (type: SupportedCreationMethods) => {
onSelect(type)
}
@@ -100,5 +103,3 @@ const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom', className }:
</div>
)
}
export default AddTypeDropdown

View File

@@ -1,56 +1,27 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { RiAddLine } from '@remixicon/react'
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'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { usePluginStore, usePluginSubscriptionStore } from '../store'
import { CreateButtonType, CreateSubscriptionButton } from './create'
import SubscriptionCard from './subscription-card'
type Props = {
detail: PluginDetail
}
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
export const SubscriptionList = ({ detail }: Props) => {
export const SubscriptionList = () => {
const { t } = useTranslation()
const showTopBorder = detail.declaration.tool || detail.declaration.endpoint
const detail = usePluginStore(state => state.detail)
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(`${detail.plugin_id}/${detail.declaration.name}`)
const showTopBorder = detail?.declaration.tool || detail?.declaration.endpoint
const provider = `${detail?.plugin_id}/${detail?.declaration.name}`
const [isShowAddModal, {
setTrue: showAddModal,
setFalse: hideAddModal,
}] = useBoolean(false)
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(provider, !!detail?.plugin_id && !!detail?.declaration.name)
const [selectedAddType, setSelectedAddType] = React.useState<SubscriptionAddType | null>(null)
const { setRefresh } = usePluginSubscriptionStore()
const [isShowAddDropdown, {
setTrue: showAddDropdown,
setFalse: hideAddDropdown,
}] = useBoolean(false)
const handleAddTypeSelect = (type: SubscriptionAddType) => {
setSelectedAddType(type)
hideAddDropdown()
showAddModal()
}
const handleModalClose = () => {
hideAddModal()
setSelectedAddType(null)
}
const handleRefreshList = () => {
refetch()
}
useEffect(() => {
if (refetch)
setRefresh(refetch)
}, [refetch])
if (isLoading) {
return (
@@ -66,64 +37,28 @@ export const SubscriptionList = ({ detail }: Props) => {
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}
<div className='relative mb-3 flex items-center justify-between'>
{
hasSubscriptions
&& <div className='flex items-center gap-1'>
<span className='system-sm-semibold-uppercase text-text-secondary'>
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
</span>
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
</div>
}
<CreateSubscriptionButton buttonType={hasSubscriptions ? CreateButtonType.ICON_BUTTON : CreateButtonType.FULL_BUTTON} />
</div>
{hasSubscriptions
&& <div className='flex flex-col gap-1'>
{subscriptions?.map(subscription => (
<SubscriptionCard
key={subscription.id}
data={subscription}
/>
)}
</div>
) : (
<>
<div className='system-sm-semibold-uppercase relative mb-3 flex items-center justify-between'>
<div className='flex items-center gap-1'>
<span className='system-sm-semibold text-text-secondary'>
{t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })}
</span>
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
</div>
<ActionButton onClick={showAddDropdown}>
<RiAddLine className='h-4 w-4' />
</ActionButton>
{isShowAddDropdown && (
<AddTypeDropdown
onSelect={handleAddTypeSelect}
onClose={hideAddDropdown}
/>
)}
</div>
<div className='flex flex-col gap-1'>
{subscriptions?.map(subscription => (
<SubscriptionCard
key={subscription.id}
data={subscription}
onRefresh={handleRefreshList}
/>
))}
</div>
</>
)}
{isShowAddModal && selectedAddType && (
<SubscriptionAddModal
type={selectedAddType}
pluginDetail={detail}
onClose={handleModalClose}
onSuccess={handleRefreshList}
/>
)}
))}
</div>}
</div>
)
}

View File

@@ -1,56 +0,0 @@
'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

View File

@@ -1,30 +1,30 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm'
import Toast from '@/app/components/base/toast'
import Indicator from '@/app/components/header/indicator'
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
import { useDeleteTriggerSubscription } from '@/service/use-triggers'
import cn from '@/utils/classnames'
import {
RiDeleteBinLine,
RiWebhookLine,
} from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
import Indicator from '@/app/components/header/indicator'
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'
import { useBoolean } from 'ahooks'
import { useTranslation } from 'react-i18next'
import { usePluginSubscriptionStore } from '../store'
type Props = {
data: TriggerSubscription
onRefresh: () => void
}
const SubscriptionCard = ({ data, onRefresh }: Props) => {
const SubscriptionCard = ({ data }: Props) => {
const { t } = useTranslation()
const [isShowDeleteModal, {
setTrue: showDeleteModal,
setFalse: hideDeleteModal,
}] = useBoolean(false)
const { refresh } = usePluginSubscriptionStore()
const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription()
@@ -35,7 +35,7 @@ const SubscriptionCard = ({ data, onRefresh }: Props) => {
type: 'success',
message: t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title'),
})
onRefresh()
refresh?.()
hideDeleteModal()
},
onError: (error: any) => {

View File

@@ -1,17 +1,12 @@
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'
import { usePluginStore } from './store'
type Props = {
detail: PluginDetail
}
export const TriggerEventsList = ({
detail,
}: Props) => {
export const TriggerEventsList = () => {
const { t } = useTranslation()
const triggers = detail.declaration.trigger?.triggers || []
const detail = usePluginStore(state => state.detail)
const triggers = detail?.declaration.trigger?.triggers || []
if (!triggers.length)
return null
@@ -27,7 +22,7 @@ export const TriggerEventsList = ({
<div className='flex flex-col gap-2'>
{triggers.map(triggerEvent => (
<ToolItem
key={`${detail.plugin_id}${triggerEvent.identity.name}`}
key={`${detail?.plugin_id}${triggerEvent.identity.name}`}
disabled={false}
// collection={provider}
// @ts-expect-error triggerEvent.identity.label is Record<Locale, string>

View File

@@ -202,6 +202,12 @@ export type PluginManifestInMarket = {
from: Dependency['type']
}
export enum SupportedCreationMethods {
OAUTH = 'OAUTH',
APIKEY = 'APIKEY',
MANUAL = 'MANUAL',
}
export type PluginDetail = {
id: string
created_at: string

View File

@@ -1,4 +1,4 @@
import type { PluginMeta } from '../../plugins/types'
import type { PluginMeta, SupportedCreationMethods } from '../../plugins/types'
import type { Collection, Trigger } from '../../tools/types'
import type { TypeWithI18N } from '../../base/form/types'
@@ -151,6 +151,7 @@ export type TriggerProviderApiEntity = {
tags: string[]
plugin_id?: string
plugin_unique_identifier: string
supported_creation_methods: SupportedCreationMethods[]
credentials_schema: TriggerCredentialField[]
oauth_client_schema: TriggerCredentialField[]
subscription_schema: TriggerSubscriptionSchema

View File

@@ -7,6 +7,11 @@ const translation = {
description: 'Create your first subscription to start receiving events',
button: 'New subscription',
},
createButton: {
oauth: 'New subscription with OAuth',
apiKey: 'New subscription with API Key',
manual: 'Paste URL to create a new subscription',
},
list: {
title: 'Subscriptions',
addButton: 'Add',

View File

@@ -16,6 +16,7 @@ const translation = {
agent: 'Agent Strategy',
extension: 'Extension',
bundle: 'Bundle',
trigger: 'Trigger',
},
search: 'Search',
allCategories: 'All Categories',

View File

@@ -7,6 +7,11 @@ const translation = {
description: '创建您的第一个订阅以开始接收事件',
button: '新建订阅',
},
createButton: {
oauth: '通过 OAuth 新建订阅',
apiKey: '通过 API Key 新建订阅',
manual: '粘贴 URL 以创建新订阅',
},
list: {
title: '订阅列表',
addButton: '添加',

View File

@@ -16,6 +16,7 @@ const translation = {
agent: 'Agent 策略',
extension: '扩展',
bundle: '插件集',
trigger: '触发器',
},
search: '搜索',
allCategories: '所有类别',

View File

@@ -95,6 +95,15 @@ export const useInvalidateAllTriggerPlugins = () => {
}
// ===== Trigger Subscriptions Management =====
export const useTriggerProviderInfo = (provider: string, enabled = true) => {
return useQuery<TriggerProviderApiEntity>({
queryKey: [NAME_SPACE, 'provider-info', provider],
queryFn: () => get<TriggerProviderApiEntity>(`/workspaces/current/trigger-provider/${provider}/info`),
enabled: enabled && !!provider,
})
}
export const useTriggerSubscriptions = (provider: string, enabled = true) => {
return useQuery<TriggerSubscription[]>({
queryKey: [NAME_SPACE, 'list-subscriptions', provider],