feat: oauth config

This commit is contained in:
yessenia
2025-09-16 16:56:23 +08:00
parent bd1fcd3525
commit 7b9d01bfca
13 changed files with 179 additions and 88 deletions

View File

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

View File

@ -31,7 +31,7 @@ enum ApiKeyStep {
Configuration = 'configuration', Configuration = 'configuration',
} }
const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { export const ApiKeyCreateModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
// State // State
@ -310,5 +310,3 @@ const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
</Modal> </Modal>
) )
} }
export default ApiKeyAddModal

View File

@ -0,0 +1,57 @@
'use client'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import { SupportedCreationMethods } from '@/app/components/plugins/types'
import ActionButton from '@/app/components/base/action-button'
import type { TriggerOAuthConfig } from '@/app/components/workflow/block-selector/types'
type Props = {
supportedMethods: SupportedCreationMethods[]
onClick: (type?: SupportedCreationMethods | typeof DEFAULT_METHOD) => void
className?: string
buttonType?: ButtonType
oauthConfig?: TriggerOAuthConfig
}
export enum ButtonType {
FULL_BUTTON = 'full-button',
ICON_BUTTON = 'icon-button',
}
export const DEFAULT_METHOD = 'default'
const CreateSubscriptionButton = ({ supportedMethods, onClick, className, buttonType = ButtonType.FULL_BUTTON }: Props) => {
const { t } = useTranslation()
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])
if (supportedMethods.length === 0)
return null
const methodType = supportedMethods.length === 1 ? supportedMethods[0] : DEFAULT_METHOD
return buttonType === ButtonType.FULL_BUTTON ? (
<Button
variant='primary'
size='medium'
className={className}
onClick={() => onClick(methodType)}
>
<RiAddLine className='mr-2 h-4 w-4' />
{buttonTextMap[methodType]}
</Button>
) : <ActionButton onClick={() => onClick(methodType)}>
<RiAddLine className='h-4 w-4' />
</ActionButton>
}
export default CreateSubscriptionButton

View File

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

View File

@ -2,50 +2,59 @@
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { RiAddLine } from '@remixicon/react'
import SubscriptionCard from './subscription-card' import SubscriptionCard from './subscription-card'
import SubscriptionAddModal from './subscription-add-modal' import { SubscriptionCreateModal } from './subscription-create-modal'
import AddTypeDropdown from './add-type-dropdown' import { CreateTypeDropdown } from './create-type-dropdown'
import ActionButton from '@/app/components/base/action-button' import CreateSubscriptionButton, { ButtonType, DEFAULT_METHOD } from './create-subscription-button'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { useTriggerSubscriptions } from '@/service/use-triggers' import { useTriggerOAuthConfig, useTriggerProviderInfo, useTriggerSubscriptions } from '@/service/use-triggers'
import type { PluginDetail } from '@/app/components/plugins/types' import type { PluginDetail } from '@/app/components/plugins/types'
import { SupportedCreationMethods } from '@/app/components/plugins/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
type Props = { type Props = {
detail: PluginDetail detail: PluginDetail
} }
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual'
export const SubscriptionList = ({ detail }: Props) => { export const SubscriptionList = ({ detail }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const showTopBorder = detail.declaration.tool || detail.declaration.endpoint const showTopBorder = detail.declaration.tool || detail.declaration.endpoint
const provider = `${detail.plugin_id}/${detail.declaration.name}`
const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(`${detail.plugin_id}/${detail.declaration.name}`) const { data: subscriptions, isLoading, refetch } = useTriggerSubscriptions(provider)
const { data: providerInfo } = useTriggerProviderInfo(provider)
const { data: oauthConfig } = useTriggerOAuthConfig(provider, providerInfo?.supported_creation_methods.includes(SupportedCreationMethods.OAUTH))
const [isShowAddModal, { const [isShowCreateDropdown, {
setTrue: showAddModal, setTrue: showCreateDropdown,
setFalse: hideAddModal, setFalse: hideCreateDropdown,
}] = useBoolean(false) }] = useBoolean(false)
const [selectedAddType, setSelectedAddType] = React.useState<SubscriptionAddType | null>(null) const [selectedCreateType, setSelectedCreateType] = React.useState<SupportedCreationMethods | null>(null)
const [isShowAddDropdown, { const [isShowCreateModal, {
setTrue: showAddDropdown, setTrue: showCreateModal,
setFalse: hideAddDropdown, setFalse: hideCreateModal,
}] = useBoolean(false) }] = useBoolean(false)
const handleAddTypeSelect = (type: SubscriptionAddType) => { const handleCreateSubscription = (type?: SupportedCreationMethods | typeof DEFAULT_METHOD) => {
setSelectedAddType(type) if (type === DEFAULT_METHOD) {
hideAddDropdown() showCreateDropdown()
showAddModal() return
}
setSelectedCreateType(type as SupportedCreationMethods)
showCreateModal()
}
const handleCreateTypeSelect = (type: SupportedCreationMethods) => {
setSelectedCreateType(type)
hideCreateDropdown()
showCreateModal()
} }
const handleModalClose = () => { const handleModalClose = () => {
hideAddModal() hideCreateModal()
setSelectedAddType(null) setSelectedCreateType(null)
} }
const handleRefreshList = () => { const handleRefreshList = () => {
@ -68,19 +77,18 @@ export const SubscriptionList = ({ detail }: Props) => {
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}> <div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
{!hasSubscriptions ? ( {!hasSubscriptions ? (
<div className='relative w-full'> <div className='relative w-full'>
<Button <CreateSubscriptionButton
variant='primary' supportedMethods={providerInfo?.supported_creation_methods || []}
size='medium' onClick={handleCreateSubscription}
className='w-full' className='w-full'
onClick={showAddDropdown} oauthConfig={oauthConfig}
> />
<RiAddLine className='mr-2 h-4 w-4' /> {isShowCreateDropdown && (
{t('pluginTrigger.subscription.empty.button')} <CreateTypeDropdown
</Button> onSelect={handleCreateTypeSelect}
{isShowAddDropdown && ( onClose={hideCreateDropdown}
<AddTypeDropdown supportedMethods={providerInfo?.supported_creation_methods || []}
onSelect={handleAddTypeSelect} oauthConfig={oauthConfig}
onClose={hideAddDropdown}
/> />
)} )}
</div> </div>
@ -93,13 +101,18 @@ export const SubscriptionList = ({ detail }: Props) => {
</span> </span>
<Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} /> <Tooltip popupContent={t('pluginTrigger.subscription.list.tip')} />
</div> </div>
<ActionButton onClick={showAddDropdown}> <CreateSubscriptionButton
<RiAddLine className='h-4 w-4' /> supportedMethods={providerInfo?.supported_creation_methods || []}
</ActionButton> onClick={handleCreateSubscription}
{isShowAddDropdown && ( buttonType={ButtonType.ICON_BUTTON}
<AddTypeDropdown oauthConfig={oauthConfig}
onSelect={handleAddTypeSelect} />
onClose={hideAddDropdown} {isShowCreateDropdown && (
<CreateTypeDropdown
onSelect={handleCreateTypeSelect}
onClose={hideCreateDropdown}
supportedMethods={providerInfo?.supported_creation_methods || []}
oauthConfig={oauthConfig}
/> />
)} )}
</div> </div>
@ -116,9 +129,10 @@ export const SubscriptionList = ({ detail }: Props) => {
</> </>
)} )}
{isShowAddModal && selectedAddType && ( {isShowCreateModal && selectedCreateType && (
<SubscriptionAddModal <SubscriptionCreateModal
type={selectedAddType} type={selectedCreateType}
oauthConfig={oauthConfig}
pluginDetail={detail} pluginDetail={detail}
onClose={handleModalClose} onClose={handleModalClose}
onSuccess={handleRefreshList} onSuccess={handleRefreshList}

View File

@ -29,7 +29,7 @@ type Props = {
onSuccess: () => void onSuccess: () => void
} }
const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { export const ManualCreateModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const [subscriptionName, setSubscriptionName] = useState('') const [subscriptionName, setSubscriptionName] = useState('')
@ -217,5 +217,3 @@ const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
</Modal> </Modal>
) )
} }
export default ManualAddModal

View File

@ -15,15 +15,15 @@ import type { FormRefObject } from '@/app/components/base/form/types'
import { import {
useBuildTriggerSubscription, useBuildTriggerSubscription,
useInitiateTriggerOAuth, useInitiateTriggerOAuth,
useTriggerOAuthConfig,
useVerifyTriggerSubscriptionBuilder, useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers' } from '@/service/use-triggers'
import type { PluginDetail } from '@/app/components/plugins/types' import type { PluginDetail } from '@/app/components/plugins/types'
import ActionButton from '@/app/components/base/action-button' 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'
type Props = { type Props = {
pluginDetail: PluginDetail pluginDetail: PluginDetail
oauthConfig?: TriggerOAuthConfig
onClose: () => void onClose: () => void
onSuccess: () => void onSuccess: () => void
} }
@ -39,7 +39,7 @@ enum AuthorizationStatusEnum {
Failed = 'failed', Failed = 'failed',
} }
const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { export const OAuthCreateModal = ({ pluginDetail, oauthConfig, onClose, onSuccess }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const [currentStep, setCurrentStep] = useState<OAuthStepEnum>(OAuthStepEnum.Setup) const [currentStep, setCurrentStep] = useState<OAuthStepEnum>(OAuthStepEnum.Setup)
@ -59,8 +59,6 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder() const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder()
const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription() const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription()
const { data: oauthConfig } = useTriggerOAuthConfig(providerName)
useEffect(() => { useEffect(() => {
initiateOAuth(providerName, { initiateOAuth(providerName, {
onSuccess: (response) => { onSuccess: (response) => {
@ -290,5 +288,3 @@ const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => {
</Modal> </Modal>
) )
} }
export default OAuthAddModal

View File

@ -2,45 +2,47 @@
import React from 'react' import React from 'react'
// import { useTranslation } from 'react-i18next' // import { useTranslation } from 'react-i18next'
// import Modal from '@/app/components/base/modal' // import Modal from '@/app/components/base/modal'
import ManualAddModal from './manual-add-modal' import { ManualCreateModal } from './manual-create-modal'
import ApiKeyAddModal from './api-key-add-modal' import { ApiKeyCreateModal } from './api-key-create-modal'
import OAuthAddModal from './oauth-add-modal' import { OAuthCreateModal } from './oauth-create-modal'
import type { PluginDetail } from '@/app/components/plugins/types' import type { PluginDetail } from '@/app/components/plugins/types'
import { SupportedCreationMethods } from '@/app/components/plugins/types'
type SubscriptionAddType = 'api-key' | 'oauth' | 'manual' import type { TriggerOAuthConfig } from '@/app/components/workflow/block-selector/types'
type Props = { type Props = {
type: SubscriptionAddType type: SupportedCreationMethods
pluginDetail: PluginDetail pluginDetail: PluginDetail
oauthConfig?: TriggerOAuthConfig
onClose: () => void onClose: () => void
onSuccess: () => void onSuccess: () => void
} }
const SubscriptionAddModal = ({ type, pluginDetail, onClose, onSuccess }: Props) => { export const SubscriptionCreateModal = ({ type, pluginDetail, oauthConfig, onClose, onSuccess }: Props) => {
// const { t } = useTranslation() // const { t } = useTranslation()
const renderModalContent = () => { const renderModalContent = () => {
switch (type) { switch (type) {
case 'manual': case SupportedCreationMethods.MANUAL:
return ( return (
<ManualAddModal <ManualCreateModal
pluginDetail={pluginDetail} pluginDetail={pluginDetail}
onClose={onClose} onClose={onClose}
onSuccess={onSuccess} onSuccess={onSuccess}
/> />
) )
case 'api-key': case SupportedCreationMethods.APIKEY:
return ( return (
<ApiKeyAddModal <ApiKeyCreateModal
pluginDetail={pluginDetail} pluginDetail={pluginDetail}
onClose={onClose} onClose={onClose}
onSuccess={onSuccess} onSuccess={onSuccess}
/> />
) )
case 'oauth': case SupportedCreationMethods.OAUTH:
return ( return (
<OAuthAddModal <OAuthCreateModal
pluginDetail={pluginDetail} pluginDetail={pluginDetail}
oauthConfig={oauthConfig}
onClose={onClose} onClose={onClose}
onSuccess={onSuccess} onSuccess={onSuccess}
/> />
@ -52,5 +54,3 @@ const SubscriptionAddModal = ({ type, pluginDetail, onClose, onSuccess }: Props)
return renderModalContent() return renderModalContent()
} }
export default SubscriptionAddModal

View File

@ -202,6 +202,12 @@ export type PluginManifestInMarket = {
from: Dependency['type'] from: Dependency['type']
} }
export enum SupportedCreationMethods {
OAUTH = 'OAUTH',
APIKEY = 'APIKEY',
MANUAL = 'MANUAL',
}
export type PluginDetail = { export type PluginDetail = {
id: string id: string
created_at: 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 { Collection, Trigger } from '../../tools/types'
import type { TypeWithI18N } from '../../base/form/types' import type { TypeWithI18N } from '../../base/form/types'
@ -151,6 +151,7 @@ export type TriggerProviderApiEntity = {
tags: string[] tags: string[]
plugin_id?: string plugin_id?: string
plugin_unique_identifier: string plugin_unique_identifier: string
supported_creation_methods: SupportedCreationMethods[]
credentials_schema: TriggerCredentialField[] credentials_schema: TriggerCredentialField[]
oauth_client_schema: TriggerCredentialField[] oauth_client_schema: TriggerCredentialField[]
subscription_schema: TriggerSubscriptionSchema subscription_schema: TriggerSubscriptionSchema

View File

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

View File

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

View File

@ -95,6 +95,15 @@ export const useInvalidateAllTriggerPlugins = () => {
} }
// ===== Trigger Subscriptions Management ===== // ===== 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) => { export const useTriggerSubscriptions = (provider: string, enabled = true) => {
return useQuery<TriggerSubscription[]>({ return useQuery<TriggerSubscription[]>({
queryKey: [NAME_SPACE, 'list-subscriptions', provider], queryKey: [NAME_SPACE, 'list-subscriptions', provider],