diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx
index a9bb287cdf..0d34fcefc9 100644
--- a/web/app/components/plugins/plugin-auth/plugin-auth.tsx
+++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx
@@ -1,20 +1,56 @@
-import { memo } from 'react'
+import {
+ memo,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react'
+import type { ReactNode } from 'react'
+import {
+ RiAddLine,
+ RiArrowDownSLine,
+ RiCheckLine,
+ RiEqualizer2Line,
+ RiKey2Line,
+ RiUserStarLine,
+} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import {
+ PortalToFollowElem,
+ PortalToFollowElemContent,
+ PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
import Authorize from './authorize'
import Authorized from './authorized'
+import AddApiKeyButton from './authorize/add-api-key-button'
+import AddOAuthButton from './authorize/add-oauth-button'
+import Item from './authorized/item'
import type { PluginPayload } from './types'
import { usePluginAuth } from './hooks/use-plugin-auth'
+import Switch from '@/app/components/base/switch'
import cn from '@/utils/classnames'
type PluginAuthProps = {
pluginPayload: PluginPayload
children?: React.ReactNode
className?: string
+ showConnectGuide?: boolean
+ endUserCredentialEnabled?: boolean
+ endUserCredentialType?: string
+ onEndUserCredentialTypeChange?: (type: string) => void
+ onEndUserCredentialChange?: (enabled: boolean) => void
}
const PluginAuth = ({
pluginPayload,
children,
className,
+ showConnectGuide,
+ endUserCredentialEnabled,
+ endUserCredentialType,
+ onEndUserCredentialTypeChange,
+ onEndUserCredentialChange,
}: PluginAuthProps) => {
+ const { t } = useTranslation()
const {
isAuthorized,
canOAuth,
@@ -24,11 +60,294 @@ const PluginAuth = ({
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, !!pluginPayload.provider)
+ const shouldShowGuide = !!showConnectGuide
+ const [showCredentialPanel, setShowCredentialPanel] = useState(false)
+ const [showAddMenu, setShowAddMenu] = useState(false)
+ const [showEndUserTypeMenu, setShowEndUserTypeMenu] = useState(false)
+ const configuredDisabled = !!endUserCredentialEnabled
+ const availableEndUserTypes = useMemo(() => {
+ const list: { value: string; label: string; icon: ReactNode }[] = []
+ if (canOAuth) {
+ list.push({
+ value: 'oauth2',
+ label: t('plugin.auth.endUserCredentials.optionOAuth'),
+ icon: ,
+ })
+ }
+ if (canApiKey) {
+ list.push({
+ value: 'api-key',
+ label: t('plugin.auth.endUserCredentials.optionApiKey'),
+ icon: ,
+ })
+ }
+ return list
+ }, [canOAuth, canApiKey, t])
+ const endUserCredentialLabel = useMemo(() => {
+ const found = availableEndUserTypes.find(item => item.value === endUserCredentialType)
+ return found?.label || availableEndUserTypes[0]?.label || '-'
+ }, [availableEndUserTypes, endUserCredentialType])
+
+ useEffect(() => {
+ if (!endUserCredentialEnabled) {
+ setShowEndUserTypeMenu(false)
+ return
+ }
+ if (!availableEndUserTypes.length)
+ return
+ const isValid = availableEndUserTypes.some(item => item.value === endUserCredentialType)
+ if (!isValid)
+ onEndUserCredentialTypeChange?.(availableEndUserTypes[0].value)
+ }, [endUserCredentialEnabled, endUserCredentialType, availableEndUserTypes, onEndUserCredentialTypeChange])
+
+ const handleSelectEndUserType = useCallback((value: string) => {
+ onEndUserCredentialTypeChange?.(value)
+ setShowEndUserTypeMenu(false)
+ }, [onEndUserCredentialTypeChange])
+ const containerClassName = useMemo(() => {
+ if (showConnectGuide)
+ return className
+ return !isAuthorized ? className : undefined
+ }, [isAuthorized, className, showConnectGuide])
+
+ useEffect(() => {
+ if (isAuthorized)
+ setShowCredentialPanel(false)
+ }, [isAuthorized])
+
+ const credentialList = useMemo(() => {
+ return (
+
+ {
+ credentials.length > 0
+ ? (
+
+ {credentials.map(credential => (
+
+ ))}
+
+ )
+ : null
+ }
+
+ )
+ }, [credentials, t])
+
+ const endUserSwitch = (
+
+
+
+
+
+
+
+
+ {t('plugin.auth.endUserCredentials.title')}
+
+
+
+
+ {t('plugin.auth.endUserCredentials.desc')}
+
+ {
+ endUserCredentialEnabled && availableEndUserTypes.length > 0 && (
+
+
+ {t('plugin.auth.endUserCredentials.typeLabel')}
+
+
+
+
+
+
+
+
+ {availableEndUserTypes.map(item => (
+
+ ))}
+
+
+
+
+
+ )
+ }
+
+
+
+ )
return (
-
+
{
- !isAuthorized && (
+ shouldShowGuide && (
+
+
+
+
+
+
+
+
+
+
+
+
+ {t('plugin.auth.configuredCredentials.title')}
+
+
+ {t('plugin.auth.configuredCredentials.desc')}
+
+
+
+
+
+
+
+
+
+
+ {
+ canOAuth && (
+
{
+ setShowAddMenu(false)
+ invalidPluginCredentialInfo()
+ }}
+ />
+ )
+ }
+ {
+ canApiKey && (
+ {
+ setShowAddMenu(false)
+ invalidPluginCredentialInfo()
+ }}
+ />
+ )
+ }
+
+
+
+
+
+
+ {credentialList}
+
+ {
+ credentials.length === 0 && (
+
+ )
+ }
+
+ {endUserSwitch}
+
+
+
+ )
+ }
+ {
+ !shouldShowGuide && !isAuthorized && (
[]
output_schema?: Record
credential_id?: string
+ use_end_user_credentials?: boolean
+ end_user_credential_type?: string
meta?: PluginMeta
plugin_id?: string
provider_icon?: Collection['icon']
@@ -86,6 +88,8 @@ export type ToolValue = {
enabled?: boolean
extra?: Record
credential_id?: string
+ use_end_user_credentials?: boolean
+ end_user_credential_type?: string
}
export type DataSourceItem = {
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
index 0d3aebd06d..9e7da37910 100644
--- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
+++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx
@@ -325,6 +325,22 @@ const BasePanel: FC = ({
},
})
}, [handleNodeDataUpdateWithSyncDraft, id])
+ const handleEndUserCredentialChange = useCallback((enabled: boolean) => {
+ handleNodeDataUpdateWithSyncDraft({
+ id,
+ data: {
+ use_end_user_credentials: enabled,
+ },
+ })
+ }, [handleNodeDataUpdateWithSyncDraft, id])
+ const handleEndUserCredentialTypeChange = useCallback((type: string) => {
+ handleNodeDataUpdateWithSyncDraft({
+ id,
+ data: {
+ end_user_credential_type: type,
+ },
+ })
+ }, [handleNodeDataUpdateWithSyncDraft, id])
const { setShowAccountSettingModal } = useModalContext()
@@ -516,6 +532,11 @@ const BasePanel: FC = ({
needsToolAuth && (
= {
retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[]
credential_id?: string
+ use_end_user_credentials?: boolean
+ end_user_credential_type?: string
subscription_id?: string
provider_id?: string
_dimmed?: boolean
diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts
index 62a5f35c0b..425b6f0d7e 100644
--- a/web/i18n/en-US/plugin.ts
+++ b/web/i18n/en-US/plugin.ts
@@ -308,6 +308,19 @@ const translation = {
unavailable: 'Unavailable',
connectedWorkspace: 'Connected Workspace',
emptyAuth: 'Please configure authentication',
+ connectCredentials: 'Connect credentials to continue',
+ configuredCredentials: {
+ title: 'Configured Credentials',
+ desc: 'Set up by you or your team in advance',
+ empty: 'No workspace credentials configured yet',
+ },
+ endUserCredentials: {
+ title: 'End-user Credentials',
+ desc: 'Credentials are provided by the end user at runtime',
+ typeLabel: 'Credential Type',
+ optionOAuth: 'OAuth',
+ optionApiKey: 'API Key',
+ },
},
readmeInfo: {
title: 'README',