diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 35ca251a5b..1be94f1317 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -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) }, [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) }, [placeholder, renderI18nObject]) - const optionValues = useStore(field.form.store, (s) => { + + const watchedVariables = useMemo(() => { + const variables = new Set() + + 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 = {} - 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) - }) + 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 = ({
{ - formSchema.type === FormTypeEnum.textInput && ( + [FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && ( ) } { - formSchema.type === FormTypeEnum.secretInput && ( - field.handleChange(e.target.value)} - onBlur={field.handleBlur} - disabled={disabled} - placeholder={memorizedPlaceholder} - /> - ) - } - { - formSchema.type === FormTypeEnum.textNumber && ( - field.handleChange(e.target.value)} - onBlur={field.handleBlur} - disabled={disabled} - placeholder={memorizedPlaceholder} - /> - ) - } - { - formSchema.type === FormTypeEnum.select && ( + formItemType === FormTypeEnum.select && ( field.handleChange(v)} @@ -180,7 +169,7 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.radio && ( + formItemType === FormTypeEnum.radio && (
@@ -189,21 +178,14 @@ const BaseField = ({
!disabled && field.handleChange(option.value)} > - { - formSchema.showRadioUI && ( - - ) - } + {showRadioUI && } {option.label}
)) @@ -212,13 +194,13 @@ const BaseField = ({ ) } { - formSchema.type === FormTypeEnum.boolean && ( + formItemType === FormTypeEnum.boolean && ( field.handleChange(val === 1)} > - True + True False ) @@ -233,9 +215,7 @@ const BaseField = ({ {renderI18nObject(formSchema?.help as any)} - { - - } + ) } diff --git a/web/app/components/base/select/index.tsx b/web/app/components/base/select/index.tsx index d054a2f1d8..385c5a53e0 100644 --- a/web/app/components/base/select/index.tsx +++ b/web/app/components/base/select/index.tsx @@ -28,6 +28,7 @@ export type Item = { name: string isGroup?: boolean disabled?: boolean + extra?: React.ReactNode } & Record export type ISelectProps = { @@ -402,6 +403,7 @@ const PortalSelect: FC = ({ {!hideChecked && item.value === value && ( )} + {item.extra}
))}
diff --git a/web/app/components/plugins/plugin-auth/types.ts b/web/app/components/plugins/plugin-auth/types.ts index 1fb2c1a531..f2a9186993 100644 --- a/web/app/components/plugins/plugin-auth/types.ts +++ b/web/app/components/plugins/plugin-auth/types.ts @@ -2,6 +2,7 @@ export enum AuthCategory { tool = 'tool', datasource = 'datasource', model = 'model', + trigger = 'trigger', } export type PluginPayload = { diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index 956bb6bde8..55f22b77b1 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -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 = ({ onHide() onUpdate() } + const { setDetail } = usePluginStore() + + useEffect(() => { + if (detail) + setDetail(detail) + }, [detail]) if (!detail) return null @@ -52,8 +59,8 @@ const PluginDetailPanel: FC = ({
{detail.declaration.category === PluginType.trigger && ( <> - - + + )} {!!detail.declaration.tool && } diff --git a/web/app/components/plugins/plugin-detail-panel/store.ts b/web/app/components/plugins/plugin-detail-panel/store.ts new file mode 100644 index 0000000000..93e5ea634a --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/store.ts @@ -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(set => ({ + detail: undefined, + setDetail: (detail: PluginDetail) => set({ detail }), +})) + +type ShapeSubscription = { + refresh?: () => void + setRefresh: (refresh: () => void) => void +} + +export const usePluginSubscriptionStore = create(set => ({ + refresh: undefined, + setRefresh: (refresh: () => void) => set({ refresh }), +})) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/api-key-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/api-key.tsx similarity index 95% rename from web/app/components/plugins/plugin-detail-panel/subscription-list/api-key-add-modal.tsx rename to web/app/components/plugins/plugin-detail-panel/subscription-list/create/api-key.tsx index d55c8399c8..65be93a8fa 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/api-key-add-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/api-key.tsx @@ -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.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) => { ) } - -export default ApiKeyAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx new file mode 100644 index 0000000000..9e7e112c0a --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -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
+ {text} +
+} + +const MultiSteps = ({ currentStep }: { currentStep: ApiKeyStep }) => { + const { t } = useTranslation() + return
+ +
+ +
+} + +export const CommonCreateModal = ({ onClose, createType }: Props) => { + const { t } = useTranslation() + const detail = usePluginStore(state => state.detail) + + const [currentStep, setCurrentStep] = useState(createType === SupportedCreationMethods.APIKEY ? ApiKeyStep.Verify : ApiKeyStep.Configuration) + + const [subscriptionName, setSubscriptionName] = useState('') + const [subscriptionBuilder, setSubscriptionBuilder] = useState() + const [verificationError, setVerificationError] = useState('') + + 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(null) + const parametersSchema = detail?.declaration.trigger?.subscription_schema?.parameters_schema || [] // apikey and oauth + const parametersFormRef = React.useRef(null) + const credentialsSchema = detail?.declaration.trigger?.credentials_schema || [] + const credentialsFormRef = React.useRef(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 ( + + {createType === SupportedCreationMethods.APIKEY && } + {currentStep === ApiKeyStep.Verify && ( + <> + {credentialsSchema.length > 0 && ( +
+ +
+ )} + {verificationError && ( +
+
+ {verificationError} +
+
+ )} + + )} + {currentStep === ApiKeyStep.Configuration &&
+
+ + setSubscriptionName(e.target.value)} + placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')} + /> +
+ +
+ +
+ + +
+
+ {t('pluginTrigger.modal.form.callbackUrl.description')} +
+
+ {createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && ( + + )} + {createType === SupportedCreationMethods.MANUAL && <> + {propertiesSchema.length > 0 && ( +
+ +
+ )} +
+
+
+ REQUESTS HISTORY +
+
+
+ +
+
+ +
+
+ Awaiting request from {detail?.declaration.name}... +
+
+ + +
+ } + +
} + + ) +} diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx new file mode 100644 index 0000000000..a907b57fb1 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx @@ -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 ( + +
+

+ {t('pluginTrigger.modal.oauth.title')} +

+ + + +
+
+ ) +} + +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(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) => { + e.stopPropagation() + e.preventDefault() + showClientSettingsModal() + } + + const allOptions = [ + { + value: SupportedCreationMethods.OAUTH, + name: t('pluginTrigger.subscription.addType.options.oauth.title'), + extra: , + 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: , + 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) => { + if (methodType === DEFAULT_METHOD) + return + + e.stopPropagation() + e.preventDefault() + onChooseCreateType(methodType) + } + + if (!supportedMethods.length) + return null + + return <> + { + return buttonType === CreateButtonType.FULL_BUTTON ? ( + + ) : + + + }} + 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 && ( + setSelectedCreateType(null)} + /> + )} + {isShowClientSettingsModal && ( + + )} + +} diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/manual-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/manual.tsx similarity index 93% rename from web/app/components/plugins/plugin-detail-panel/subscription-list/manual-add-modal.tsx rename to web/app/components/plugins/plugin-detail-panel/subscription-list/create/manual.tsx index 71f3a7f209..63555570b4 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/manual-add-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/manual.tsx @@ -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() @@ -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(null) const { data: logData } = useTriggerSubscriptionBuilderLogs( @@ -193,7 +193,7 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
- Awaiting request from {pluginDetail.declaration.name}... + Awaiting request from {detail?.declaration.name}...
@@ -217,5 +217,3 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { ) } - -export default ManualAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx new file mode 100644 index 0000000000..4befe9f4d1 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx @@ -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() + const [authorizationStatus, setAuthorizationStatus] = useState() + + const [clientType, setClientType] = useState(oauthConfig?.custom_enabled ? ClientTypeEnum.Custom : ClientTypeEnum.Default) + + const clientFormRef = React.useRef(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 ( + + + + ) + } + > + OAuth Client +
+ {[ClientTypeEnum.Default, ClientTypeEnum.Custom].map(option => ( + setClientType(option)} + selected={clientType === option} + className="flex-1" + /> + ))} +
+ {oauthConfig?.redirect_uri && ( +
+
+ +
+
+
+ {t('pluginTrigger.modal.oauthRedirectInfo')} +
+
+ {oauthConfig.redirect_uri} +
+ +
+
+ )} + {clientType === ClientTypeEnum.Custom && clientSchema.length > 0 && ( +
+ )} + + ) +} diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/oauth-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth.tsx similarity index 93% rename from web/app/components/plugins/plugin-detail-panel/subscription-list/oauth-add-modal.tsx rename to web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth.tsx index 29d36dafdc..33bab4eb80 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/oauth-add-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/oauth.tsx @@ -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.Setup) const [subscriptionName, setSubscriptionName] = useState('') const [authorizationUrl, setAuthorizationUrl] = useState('') @@ -51,16 +50,14 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { const clientFormRef = React.useRef(null) const parametersFormRef = React.useRef(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) => { ) } - -export default OAuthAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/add-type-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/type-dropdown.tsx similarity index 75% rename from web/app/components/plugins/plugin-detail-panel/subscription-list/add-type-dropdown.tsx rename to web/app/components/plugins/plugin-detail-panel/subscription-list/create/type-dropdown.tsx index 604cd7b5ee..151d840107 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/add-type-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/type-dropdown.tsx @@ -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(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: , + 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 }: ) } - -export default AddTypeDropdown diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx index 42856ec768..875c662705 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx @@ -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(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 (
- {!hasSubscriptions ? ( -
- - {isShowAddDropdown && ( - + { + hasSubscriptions + &&
+ + {t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })} + + +
+ } + +
+ + {hasSubscriptions + &&
+ {subscriptions?.map(subscription => ( + - )} -
- ) : ( - <> -
-
- - {t('pluginTrigger.subscription.listNum', { num: subscriptions?.length || 0 })} - - -
- - - - {isShowAddDropdown && ( - - )} -
- -
- {subscriptions?.map(subscription => ( - - ))} -
- - )} - - {isShowAddModal && selectedAddType && ( - - )} + ))} +
} ) } diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-add-modal.tsx deleted file mode 100644 index 896642afa5..0000000000 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-add-modal.tsx +++ /dev/null @@ -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 ( - - ) - case 'api-key': - return ( - - ) - case 'oauth': - return ( - - ) - default: - return null - } - } - - return renderModalContent() -} - -export default SubscriptionAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx index efc982db44..b68a67efb6 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx @@ -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) => { diff --git a/web/app/components/plugins/plugin-detail-panel/trigger-events-list.tsx b/web/app/components/plugins/plugin-detail-panel/trigger-events-list.tsx index 0ec8b8350a..4a3815e07b 100644 --- a/web/app/components/plugins/plugin-detail-panel/trigger-events-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/trigger-events-list.tsx @@ -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 = ({
{triggers.map(triggerEvent => ( diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 5f4f603368..102be22c17 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -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 diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index bdbd24f7b3..4853520dd0 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -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 diff --git a/web/i18n/en-US/plugin-trigger.ts b/web/i18n/en-US/plugin-trigger.ts index 61d2ecb247..a28e33922d 100644 --- a/web/i18n/en-US/plugin-trigger.ts +++ b/web/i18n/en-US/plugin-trigger.ts @@ -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', diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 85bbd44bd5..4c64f5fda8 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -16,6 +16,7 @@ const translation = { agent: 'Agent Strategy', extension: 'Extension', bundle: 'Bundle', + trigger: 'Trigger', }, search: 'Search', allCategories: 'All Categories', diff --git a/web/i18n/zh-Hans/plugin-trigger.ts b/web/i18n/zh-Hans/plugin-trigger.ts index 0daddae271..4a47ba4b89 100644 --- a/web/i18n/zh-Hans/plugin-trigger.ts +++ b/web/i18n/zh-Hans/plugin-trigger.ts @@ -7,6 +7,11 @@ const translation = { description: '创建您的第一个订阅以开始接收事件', button: '新建订阅', }, + createButton: { + oauth: '通过 OAuth 新建订阅', + apiKey: '通过 API Key 新建订阅', + manual: '粘贴 URL 以创建新订阅', + }, list: { title: '订阅列表', addButton: '添加', diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index e37de6d69f..afd4eff2ca 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -16,6 +16,7 @@ const translation = { agent: 'Agent 策略', extension: '扩展', bundle: '插件集', + trigger: '触发器', }, search: '搜索', allCategories: '所有类别', diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index c4193ae650..bf23d80875 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -95,6 +95,15 @@ export const useInvalidateAllTriggerPlugins = () => { } // ===== Trigger Subscriptions Management ===== + +export const useTriggerProviderInfo = (provider: string, enabled = true) => { + return useQuery({ + queryKey: [NAME_SPACE, 'provider-info', provider], + queryFn: () => get(`/workspaces/current/trigger-provider/${provider}/info`), + enabled: enabled && !!provider, + }) +} + export const useTriggerSubscriptions = (provider: string, enabled = true) => { return useQuery({ queryKey: [NAME_SPACE, 'list-subscriptions', provider],