mirror of
https://github.com/langgenius/dify.git
synced 2026-01-27 06:02:10 +08:00
feat: oauth config opt & add dynamic options
This commit is contained in:
parent
0edf06329f
commit
eae65e55ce
@ -1,19 +1,21 @@
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
import { PortalSelect } from '@/app/components/base/select'
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
import type { AnyFieldApi } from '@tanstack/react-form'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import {
|
||||
isValidElement,
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
import type { AnyFieldApi } from '@tanstack/react-form'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
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) {
|
||||
@ -56,6 +58,7 @@ const BaseField = ({
|
||||
disabled: formSchemaDisabled,
|
||||
showRadioUI,
|
||||
type: formItemType,
|
||||
dynamicSelectParams,
|
||||
} = formSchema
|
||||
const disabled = propsDisabled || formSchemaDisabled
|
||||
|
||||
@ -115,6 +118,26 @@ const BaseField = ({
|
||||
|
||||
const value = useStore(field.form.store, s => s.values[field.name])
|
||||
|
||||
const { data: dynamicOptionsData, isLoading: isDynamicOptionsLoading } = useTriggerPluginDynamicOptions(
|
||||
dynamicSelectParams || {
|
||||
plugin_id: '',
|
||||
provider: '',
|
||||
action: '',
|
||||
parameter: '',
|
||||
credential_id: '',
|
||||
},
|
||||
formItemType === FormTypeEnum.dynamicSelect,
|
||||
)
|
||||
|
||||
const dynamicOptions = useMemo(() => {
|
||||
if (!dynamicOptionsData?.options)
|
||||
return []
|
||||
return dynamicOptionsData.options.map(option => ({
|
||||
name: typeof option.label === 'string' ? option.label : renderI18nObject(option.label),
|
||||
value: option.value,
|
||||
}))
|
||||
}, [dynamicOptionsData, renderI18nObject])
|
||||
|
||||
const show = useMemo(() => {
|
||||
return show_on.every((condition) => {
|
||||
return watchedValues[condition.variable] === condition.value
|
||||
@ -168,6 +191,22 @@ const BaseField = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.dynamicSelect && (
|
||||
<PortalSelect
|
||||
value={value}
|
||||
onSelect={(item: any) => field.handleChange(item.value)}
|
||||
readonly={disabled || isDynamicOptionsLoading}
|
||||
placeholder={
|
||||
isDynamicOptionsLoading
|
||||
? 'Loading options...'
|
||||
: memorizedPlaceholder || 'Select an option'
|
||||
}
|
||||
items={dynamicOptions}
|
||||
popupClassName="z-[9999]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.radio && (
|
||||
<div className={cn(
|
||||
|
||||
@ -62,6 +62,13 @@ export type FormSchema = {
|
||||
validators?: AnyValidators
|
||||
showRadioUI?: boolean
|
||||
disabled?: boolean
|
||||
dynamicSelectParams?: {
|
||||
plugin_id: string
|
||||
provider: string
|
||||
action: string
|
||||
parameter: string
|
||||
credential_id: string
|
||||
}
|
||||
}
|
||||
|
||||
export type FormValues = Record<string, any>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
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 { FormTypeEnum } 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'
|
||||
@ -25,6 +26,12 @@ type Props = {
|
||||
createType: SupportedCreationMethods
|
||||
}
|
||||
|
||||
const CREDENTIAL_TYPE_MAP: Record<SupportedCreationMethods, TriggerCredentialTypeEnum> = {
|
||||
[SupportedCreationMethods.APIKEY]: TriggerCredentialTypeEnum.ApiKey,
|
||||
[SupportedCreationMethods.OAUTH]: TriggerCredentialTypeEnum.Oauth2,
|
||||
[SupportedCreationMethods.MANUAL]: TriggerCredentialTypeEnum.Unauthorized,
|
||||
}
|
||||
|
||||
enum ApiKeyStep {
|
||||
Verify = 'verify',
|
||||
Configuration = 'configuration',
|
||||
@ -87,7 +94,7 @@ export const CommonCreateModal = ({ onClose, createType }: Props) => {
|
||||
createBuilder(
|
||||
{
|
||||
provider: providerName,
|
||||
credential_type: TriggerCredentialTypeEnum.Unauthorized,
|
||||
credential_type: CREDENTIAL_TYPE_MAP[createType],
|
||||
},
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
@ -262,7 +269,16 @@ export const CommonCreateModal = ({ onClose, createType }: Props) => {
|
||||
</div>
|
||||
{createType !== SupportedCreationMethods.MANUAL && parametersSchema.length > 0 && (
|
||||
<BaseForm
|
||||
formSchemas={parametersSchema}
|
||||
formSchemas={parametersSchema.map(schema => ({
|
||||
...schema,
|
||||
dynamicSelectParams: schema.type === FormTypeEnum.dynamicSelect ? {
|
||||
plugin_id: detail?.plugin_id || '',
|
||||
provider: providerName,
|
||||
action: 'provider',
|
||||
parameter: schema.name,
|
||||
credential_id: subscriptionBuilder?.id || '',
|
||||
} : undefined,
|
||||
}))}
|
||||
ref={parametersFormRef}
|
||||
labelClassName='system-sm-medium mb-2 block text-text-primary'
|
||||
/>
|
||||
|
||||
@ -202,6 +202,7 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
|
||||
<OAuthClientSettingsModal
|
||||
oauthConfig={oauthConfig}
|
||||
onClose={hideClientSettingsModal}
|
||||
showOAuthCreateModal={() => setSelectedCreateType(SupportedCreationMethods.OAUTH)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -6,6 +6,7 @@ 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 { openOAuthPopup } from '@/hooks/use-oauth'
|
||||
import {
|
||||
useConfigureTriggerOAuth,
|
||||
useDeleteTriggerOAuth,
|
||||
@ -23,6 +24,7 @@ import { usePluginStore } from '../../store'
|
||||
type Props = {
|
||||
oauthConfig?: TriggerOAuthConfig
|
||||
onClose: () => void
|
||||
showOAuthCreateModal: () => void
|
||||
}
|
||||
|
||||
enum AuthorizationStatusEnum {
|
||||
@ -36,10 +38,9 @@ enum ClientTypeEnum {
|
||||
Custom = 'custom',
|
||||
}
|
||||
|
||||
export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
|
||||
export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreateModal }: 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>()
|
||||
|
||||
@ -55,22 +56,30 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
|
||||
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])
|
||||
const handleAuthorization = () => {
|
||||
setAuthorizationStatus(AuthorizationStatusEnum.Pending)
|
||||
initiateOAuth(providerName, {
|
||||
onSuccess: (response) => {
|
||||
setSubscriptionBuilder(response.subscription_builder)
|
||||
openOAuthPopup(response.authorization_url, (callbackData) => {
|
||||
if (callbackData) {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.authorization.authSuccess'),
|
||||
})
|
||||
onClose()
|
||||
showOAuthCreateModal()
|
||||
}
|
||||
})
|
||||
},
|
||||
onError: (error: any) => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error?.message || t('pluginTrigger.modal.errors.authFailed'),
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (providerName && subscriptionBuilder && authorizationStatus === AuthorizationStatusEnum.Pending) {
|
||||
@ -81,14 +90,11 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
|
||||
subscriptionBuilderId: subscriptionBuilder.id,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setAuthorizationStatus(AuthorizationStatusEnum.Success)
|
||||
// setCurrentStep(OAuthStepEnum.Configuration)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.authorization.authSuccess'),
|
||||
})
|
||||
clearInterval(pollInterval)
|
||||
onSuccess: (response) => {
|
||||
if (response.verified) {
|
||||
setAuthorizationStatus(AuthorizationStatusEnum.Success)
|
||||
clearInterval(pollInterval)
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
// Continue polling - auth might still be in progress
|
||||
@ -119,7 +125,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveOnly = () => {
|
||||
const handleSave = (needAuth: boolean) => {
|
||||
const clientParams = clientFormRef.current?.getFormValues({})?.values || {}
|
||||
if (clientParams.client_id === oauthConfig?.params.client_id)
|
||||
clientParams.client_id = '[__HIDDEN__]'
|
||||
@ -133,7 +139,11 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
|
||||
enabled: clientType === ClientTypeEnum.Custom,
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
onClose()
|
||||
if (needAuth)
|
||||
handleAuthorization()
|
||||
else
|
||||
onClose()
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('pluginTrigger.modal.oauth.configuration.success'),
|
||||
@ -148,27 +158,19 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose }: Props) => {
|
||||
})
|
||||
}
|
||||
|
||||
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')}
|
||||
confirmButtonText={authorizationStatus === AuthorizationStatusEnum.Pending ? t('pluginTrigger.modal.common.authorizing')
|
||||
: authorizationStatus === AuthorizationStatusEnum.Success ? t('pluginTrigger.modal.oauth.authorization.waitingJump') : 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}
|
||||
onCancel={() => handleSave(false)}
|
||||
onConfirm={() => handleSave(true)}
|
||||
footerSlot={
|
||||
oauthConfig?.custom_enabled && oauthConfig?.params && (
|
||||
<div className='grow'>
|
||||
|
||||
@ -103,6 +103,7 @@ const translation = {
|
||||
waitingAuth: 'Waiting for authorization...',
|
||||
authSuccess: 'Authorization successful',
|
||||
authFailed: 'Authorization failed',
|
||||
waitingJump: 'Authorized, waiting for jump',
|
||||
},
|
||||
configuration: {
|
||||
title: 'Configure Subscription',
|
||||
|
||||
@ -96,13 +96,14 @@ const translation = {
|
||||
title: '通过OAuth创建',
|
||||
authorization: {
|
||||
title: 'OAuth授权',
|
||||
description: '授权Dify访问您的账户',
|
||||
redirectUrl: '重定向URL',
|
||||
redirectUrlHelp: '在您的OAuth应用配置中使用此URL',
|
||||
authorizeButton: '使用{{provider}}授权',
|
||||
description: '授权 Dify 访问您的账户',
|
||||
redirectUrl: '重定向 URL',
|
||||
redirectUrlHelp: '在您的 OAuth 应用配置中使用此 URL',
|
||||
authorizeButton: '使用 {{provider}} 授权',
|
||||
waitingAuth: '等待授权中...',
|
||||
authSuccess: '授权成功',
|
||||
authFailed: '授权失败',
|
||||
waitingJump: '已授权,待跳转',
|
||||
},
|
||||
configuration: {
|
||||
title: '配置订阅',
|
||||
@ -112,7 +113,7 @@ const translation = {
|
||||
},
|
||||
manual: {
|
||||
title: '手动设置',
|
||||
description: '手动配置您的Webhook订阅',
|
||||
description: '手动配置您的 Webhook 订阅',
|
||||
instruction: {
|
||||
title: '设置说明',
|
||||
step1: '1. 复制下方的回调URL',
|
||||
|
||||
@ -166,7 +166,7 @@ export const useVerifyTriggerSubscriptionBuilder = () => {
|
||||
credentials?: Record<string, any>
|
||||
}) => {
|
||||
const { provider, subscriptionBuilderId, ...body } = payload
|
||||
return post(
|
||||
return post<{ verified: boolean }>(
|
||||
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
|
||||
{ body },
|
||||
)
|
||||
@ -276,10 +276,11 @@ export const useTriggerPluginDynamicOptions = (payload: {
|
||||
provider: string
|
||||
action: string
|
||||
parameter: string
|
||||
credential_id: string
|
||||
extra?: Record<string, any>
|
||||
}, enabled = true) => {
|
||||
return useQuery<{ options: Array<{ value: string; label: any }> }>({
|
||||
queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.extra],
|
||||
queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.credential_id, payload.extra],
|
||||
queryFn: () => get<{ options: Array<{ value: string; label: any }> }>(
|
||||
'/workspaces/current/plugin/parameters/dynamic-options',
|
||||
{
|
||||
@ -289,7 +290,7 @@ export const useTriggerPluginDynamicOptions = (payload: {
|
||||
},
|
||||
},
|
||||
),
|
||||
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter,
|
||||
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user