feat: introduce CredentialConfigHeader and EndUserCredentialSection components for enhanced plugin authentication UI

This commit is contained in:
zhsama 2025-11-28 18:12:11 +08:00
parent a31eea8389
commit 5e96e4dda6
6 changed files with 386 additions and 402 deletions

View File

@ -18,7 +18,6 @@ import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import GroupAuthControl from './group-auth-control'
import ConfigContext from '@/context/debug-configuration'
import type { AgentTool } from '@/types/app'
import { type Collection, CollectionType } from '@/app/components/tools/types'
@ -237,24 +236,6 @@ const AgentTools: FC = () => {
? <div className='h-5 w-5 rounded-md bg-cover bg-center' style={{ backgroundImage: `url(${group.icon})` }} />
: <AppIcon className='rounded-md' size='xs' icon={group.icon?.content} background={group.icon?.background} />}
<div className='system-sm-semibold text-text-secondary'>{group.providerName}</div>
<div className='ml-auto'>
<GroupAuthControl
providerId={group.providerId}
providerName={group.providerName}
providerType={group.providerType}
credentialId={group.tools.find(t => !!t.credential_id)?.credential_id}
onChange={(id) => {
const newModelConfig = produce(modelConfig, (draft) => {
draft.agentConfig.tools.forEach((tool: any) => {
if (tool.provider_id === group.providerId)
tool.credential_id = id
})
})
setModelConfig(newModelConfig)
formattingChangedDispatcher()
}}
/>
</div>
{group.tools.every(t => t.notAuthor) && (
<Button
variant='secondary'
@ -279,11 +260,6 @@ const AgentTools: FC = () => {
'group relative flex w-full items-center justify-between rounded-lg pl-[21px] pr-2 hover:bg-state-base-hover',
isDeleting === item.__index && 'border border-state-destructive-border hover:bg-state-destructive-hover',
)}
onClickCapture={() => {
// 调试:查看工具行数据
console.log('tool item', item)
}}
>
<div className='flex w-0 grow items-center'>
<div

View File

@ -0,0 +1,116 @@
import {
memo,
useState,
} from 'react'
import {
RiAddLine,
RiKey2Line,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import AddOAuthButton from './authorize/add-oauth-button'
import AddApiKeyButton from './authorize/add-api-key-button'
import type { PluginPayload } from './types'
import cn from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
export type CredentialConfigHeaderProps = {
pluginPayload: PluginPayload
canOAuth?: boolean
canApiKey?: boolean
hasOAuthClientConfigured?: boolean
disabled?: boolean
onCredentialAdded?: () => void
onAddMenuOpenChange?: (open: boolean) => void
}
const CredentialConfigHeader = ({
pluginPayload,
canOAuth,
canApiKey,
hasOAuthClientConfigured,
disabled,
onCredentialAdded,
onAddMenuOpenChange,
}: CredentialConfigHeaderProps) => {
const { t } = useTranslation()
const [showAddMenu, setShowAddMenu] = useState(false)
const handleAddMenuOpenChange = (open: boolean) => {
setShowAddMenu(open)
onAddMenuOpenChange?.(open)
}
const addButtonDisabled = disabled || (!canOAuth && !canApiKey && !hasOAuthClientConfigured)
return (
<div className='flex items-start justify-between gap-2'>
<div className='flex items-start gap-2'>
<RiKey2Line className='mt-0.5 h-4 w-4 text-text-tertiary' />
<div className='space-y-0.5'>
<div className='system-md-semibold text-text-primary'>
{t('plugin.auth.configuredCredentials.title')}
</div>
<div className='system-xs-regular text-text-tertiary'>
{t('plugin.auth.configuredCredentials.desc')}
</div>
</div>
</div>
<PortalToFollowElem
open={showAddMenu}
onOpenChange={handleAddMenuOpenChange}
placement='bottom-end'
offset={6}
>
<PortalToFollowElemTrigger asChild>
<button
type='button'
className={cn(
'flex h-9 w-9 items-center justify-center rounded-full bg-primary-600 text-white hover:bg-primary-700',
addButtonDisabled && 'pointer-events-none opacity-50',
)}
onClick={() => handleAddMenuOpenChange(!showAddMenu)}
>
<RiAddLine className='h-5 w-5' />
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[120]'>
<div className='w-[220px] rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg'>
<div className='flex flex-col gap-1 p-1'>
{canOAuth && (
<AddOAuthButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
className='w-full justify-between bg-transparent text-text-primary hover:bg-transparent'
buttonText={t('plugin.auth.addOAuth')}
disabled={disabled}
onUpdate={() => {
setShowAddMenu(false)
onCredentialAdded?.()
}}
/>
)}
{canApiKey && (
<AddApiKeyButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
buttonText={t('plugin.auth.addApi')}
disabled={disabled}
onUpdate={() => {
setShowAddMenu(false)
onCredentialAdded?.()
}}
/>
)}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
export default memo(CredentialConfigHeader)

View File

@ -0,0 +1,176 @@
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import type { ReactNode } from 'react'
import {
RiArrowDownSLine,
RiEqualizer2Line,
RiKey2Line,
RiUserStarLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import AddOAuthButton from './authorize/add-oauth-button'
import AddApiKeyButton from './authorize/add-api-key-button'
import type { PluginPayload } from './types'
import cn from '@/utils/classnames'
import Switch from '@/app/components/base/switch'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
export type EndUserCredentialSectionProps = {
pluginPayload: PluginPayload
canOAuth?: boolean
canApiKey?: boolean
disabled?: boolean
useEndUserCredentialEnabled?: boolean
endUserCredentialType?: string
onEndUserCredentialChange?: (enabled: boolean) => void
onEndUserCredentialTypeChange?: (type: string) => void
onCredentialAdded?: () => void
className?: string
}
const EndUserCredentialSection = ({
pluginPayload,
canOAuth,
canApiKey,
disabled,
useEndUserCredentialEnabled,
endUserCredentialType,
onEndUserCredentialChange,
onEndUserCredentialTypeChange,
onCredentialAdded,
className,
}: EndUserCredentialSectionProps) => {
const { t } = useTranslation()
const [showEndUserTypeMenu, setShowEndUserTypeMenu] = useState(false)
const availableEndUserTypes = useMemo(() => {
const list: { value: string; label: string; icon: ReactNode }[] = []
if (canOAuth) {
list.push({
value: 'oauth2',
label: t('plugin.auth.endUserCredentials.optionOAuth'),
icon: <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />,
})
}
if (canApiKey) {
list.push({
value: 'api-key',
label: t('plugin.auth.endUserCredentials.optionApiKey'),
icon: <RiKey2Line className='h-4 w-4 text-text-tertiary' />,
})
}
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 (!useEndUserCredentialEnabled)
return
if (!availableEndUserTypes.length)
return
const isValid = availableEndUserTypes.some(item => item.value === endUserCredentialType)
if (!isValid)
onEndUserCredentialTypeChange?.(availableEndUserTypes[0].value)
}, [useEndUserCredentialEnabled, endUserCredentialType, availableEndUserTypes, onEndUserCredentialTypeChange])
const handleSelectEndUserType = useCallback((value: string) => {
onEndUserCredentialTypeChange?.(value)
setShowEndUserTypeMenu(false)
}, [onEndUserCredentialTypeChange])
return (
<div className={cn('flex items-start gap-3', className)}>
<RiUserStarLine className='mt-0.5 h-4 w-4 shrink-0 text-text-tertiary' />
<div className='flex-1 space-y-3'>
<div className='flex items-center justify-between gap-3'>
<div className='space-y-1'>
<div className='system-sm-semibold text-text-primary'>
{t('plugin.auth.endUserCredentials.title')}
</div>
<div className='system-xs-regular text-text-tertiary'>
{t('plugin.auth.endUserCredentials.desc')}
</div>
</div>
<Switch
size='md'
defaultValue={!!useEndUserCredentialEnabled}
onChange={onEndUserCredentialChange}
disabled={disabled}
/>
</div>
{
useEndUserCredentialEnabled && availableEndUserTypes.length > 0 && (
<div className='flex items-center justify-between gap-3'>
<div className='system-sm-semibold text-text-primary'>
{t('plugin.auth.endUserCredentials.typeLabel')}
</div>
<PortalToFollowElem
open={showEndUserTypeMenu}
onOpenChange={setShowEndUserTypeMenu}
placement='bottom-end'
offset={6}
>
<PortalToFollowElemTrigger asChild>
<button
type='button'
className='border-components-input-border flex h-9 min-w-[190px] items-center justify-between rounded-lg border bg-components-input-bg-normal px-3 text-left text-text-primary shadow-xs hover:bg-components-input-bg-hover'
onClick={() => setShowEndUserTypeMenu(v => !v)}
>
<span className='system-sm-semibold'>{endUserCredentialLabel}</span>
<RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[120]'>
<div className='w-[220px] rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg'>
<div className='flex flex-col gap-1 p-1'>
{canOAuth && (
<AddOAuthButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
className='w-full justify-between bg-transparent text-text-primary hover:bg-transparent'
buttonText={t('plugin.auth.addOAuth')}
disabled={disabled}
onUpdate={() => {
handleSelectEndUserType('oauth2')
onCredentialAdded?.()
}}
/>
)}
{canApiKey && (
<AddApiKeyButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
buttonText={t('plugin.auth.addApi')}
disabled={disabled}
onUpdate={() => {
handleSelectEndUserType('api-key')
onCredentialAdded?.()
}}
/>
)}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
</div>
</div>
)
}
export default memo(EndUserCredentialSection)

View File

@ -2,6 +2,10 @@ export { default as PluginAuth } from './plugin-auth'
export { default as Authorized } from './authorized'
export { default as AuthorizedInNode } from './authorized-in-node'
export { default as PluginAuthInAgent } from './plugin-auth-in-agent'
export { default as CredentialConfigHeader } from './credential-config-header'
export type { CredentialConfigHeaderProps } from './credential-config-header'
export { default as EndUserCredentialSection } from './end-user-credential-section'
export type { EndUserCredentialSectionProps } from './end-user-credential-section'
export { usePluginAuth } from './hooks/use-plugin-auth'
export { default as PluginAuthInDataSourceNode } from './plugin-auth-in-datasource-node'
export { default as AuthorizedInDataSourceNode } from './authorized-in-data-source-node'

View File

@ -1,23 +1,16 @@
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import type { ReactNode } from 'react'
import {
RiAddLine,
RiArrowDownSLine,
RiEqualizer2Line,
RiKey2Line,
RiUserStarLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Authorize from './authorize'
import Authorized from './authorized'
import AddOAuthButton from './authorize/add-oauth-button'
import AddApiKeyButton from './authorize/add-api-key-button'
import CredentialConfigHeader from './credential-config-header'
import EndUserCredentialSection from './end-user-credential-section'
import type {
Credential,
PluginPayload,
@ -26,12 +19,6 @@ import { usePluginAuth } from './hooks/use-plugin-auth'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Switch from '@/app/components/base/switch'
type PluginAuthInAgentProps = {
pluginPayload: PluginPayload
@ -53,8 +40,6 @@ const PluginAuthInAgent = ({
}: PluginAuthInAgentProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const [showAddMenu, setShowAddMenu] = useState(false)
const [showEndUserTypeMenu, setShowEndUserTypeMenu] = useState(false)
const {
isAuthorized,
canOAuth,
@ -66,6 +51,8 @@ const PluginAuthInAgent = ({
hasOAuthClientConfigured,
} = usePluginAuth(pluginPayload, true)
const configuredDisabled = !!useEndUserCredentialEnabled
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
@ -122,251 +109,86 @@ const PluginAuthInAgent = ({
)
}, [credentialId, credentials, t])
const availableEndUserTypes = useMemo(() => {
const list: { value: string; label: string; icon: ReactNode }[] = []
if (canOAuth) {
list.push({
value: 'oauth2',
label: t('plugin.auth.endUserCredentials.optionOAuth'),
icon: <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />,
})
}
if (canApiKey) {
list.push({
value: 'api-key',
label: t('plugin.auth.endUserCredentials.optionApiKey'),
icon: <RiKey2Line className='h-4 w-4 text-text-tertiary' />,
})
}
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 (!useEndUserCredentialEnabled)
return
if (!availableEndUserTypes.length)
return
const isValid = availableEndUserTypes.some(item => item.value === endUserCredentialType)
if (!isValid)
onEndUserCredentialTypeChange?.(availableEndUserTypes[0].value)
}, [useEndUserCredentialEnabled, endUserCredentialType, availableEndUserTypes, onEndUserCredentialTypeChange])
const handleSelectEndUserType = useCallback((value: string) => {
onEndUserCredentialTypeChange?.(value)
setShowEndUserTypeMenu(false)
}, [onEndUserCredentialTypeChange])
const shouldShowAuthorizeCard = !credentials.length && (canOAuth || canApiKey || hasOAuthClientConfigured)
const endUserSection = (
<div className='flex items-start rounded-lg border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-3 py-3'>
<RiUserStarLine className='mt-0.5 h-4 w-4 text-text-tertiary' />
<div className='flex-1 space-y-3'>
<div className='flex items-center justify-between gap-3'>
<div className='space-y-1'>
<div className='system-sm-semibold text-text-primary'>
{t('plugin.auth.endUserCredentials.title')}
</div>
<div className='system-xs-regular text-text-tertiary'>
{t('plugin.auth.endUserCredentials.desc')}
</div>
</div>
<Switch
size='md'
defaultValue={!!useEndUserCredentialEnabled}
onChange={onEndUserCredentialChange}
disabled={disabled}
/>
</div>
{
useEndUserCredentialEnabled && availableEndUserTypes.length > 0 && (
<div className='flex items-center justify-between gap-3'>
<div className='system-sm-semibold text-text-primary'>
{t('plugin.auth.endUserCredentials.typeLabel')}
</div>
<PortalToFollowElem
open={showEndUserTypeMenu}
onOpenChange={setShowEndUserTypeMenu}
placement='bottom-end'
offset={6}
>
<PortalToFollowElemTrigger asChild>
<button
type='button'
className='border-components-input-border flex h-9 min-w-[190px] items-center justify-between rounded-lg border bg-components-input-bg-normal px-3 text-left text-text-primary shadow-xs hover:bg-components-input-bg-hover'
onClick={() => setShowEndUserTypeMenu(v => !v)}
>
<span className='system-sm-semibold'>{endUserCredentialLabel}</span>
<RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[120]'>
<div className='w-[220px] rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg'>
<div className='flex flex-col gap-1 p-1'>
{canOAuth && (
<AddOAuthButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
className='w-full justify-between bg-transparent text-text-primary hover:bg-transparent'
buttonText={t('plugin.auth.addOAuth')}
disabled={disabled}
onUpdate={() => {
handleSelectEndUserType('oauth2')
invalidPluginCredentialInfo()
}}
/>
)}
{canApiKey && (
<AddApiKeyButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
buttonText={t('plugin.auth.addApi')}
disabled={disabled}
onUpdate={() => {
handleSelectEndUserType('api-key')
invalidPluginCredentialInfo()
}}
/>
)}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
</div>
</div>
)
return (
<div className='border-components-panel-border bg-components-panel-bg'>
<div className='flex items-start justify-between gap-2'>
<div className='flex items-start gap-2'>
<RiKey2Line className='mt-0.5 h-4 w-4 text-text-tertiary' />
<div className='space-y-0.5'>
<div className='system-md-semibold text-text-primary'>
{t('plugin.auth.configuredCredentials.title')}
</div>
<div className='system-xs-regular text-text-tertiary'>
{t('plugin.auth.configuredCredentials.desc')}
</div>
</div>
</div>
<PortalToFollowElem
open={showAddMenu}
onOpenChange={setShowAddMenu}
placement='bottom-end'
offset={6}
>
<PortalToFollowElemTrigger asChild>
<button
type='button'
className={cn(
'flex h-9 w-9 items-center justify-center rounded-full bg-primary-600 text-white hover:bg-primary-700',
(disabled || (!canOAuth && !canApiKey && !hasOAuthClientConfigured)) && 'pointer-events-none opacity-50',
)}
onClick={() => setShowAddMenu(v => !v)}
>
<RiAddLine className='h-5 w-5' />
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[120]'>
<div className='w-[220px] rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg'>
<div className='flex flex-col gap-1 p-1'>
{
canOAuth && (
<AddOAuthButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
className='w-full justify-between bg-transparent text-text-primary hover:bg-transparent'
buttonText={t('plugin.auth.addOAuth')}
disabled={disabled}
onUpdate={() => {
setShowAddMenu(false)
invalidPluginCredentialInfo()
}}
/>
)
}
{
canApiKey && (
<AddApiKeyButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
buttonText={t('plugin.auth.addApi')}
disabled={disabled}
onUpdate={() => {
setShowAddMenu(false)
invalidPluginCredentialInfo()
}}
/>
)
}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
<div className={cn(configuredDisabled && 'pointer-events-none opacity-50')}>
<CredentialConfigHeader
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
hasOAuthClientConfigured={hasOAuthClientConfigured}
disabled={disabled || configuredDisabled}
onCredentialAdded={invalidPluginCredentialInfo}
/>
</div>
{
!isAuthorized && shouldShowAuthorizeCard && (
<div className='rounded-xl bg-background-section px-4 py-4'>
<div className='flex w-full justify-center'>
<div className='w-full max-w-[520px]'>
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
theme='secondary'
showDivider={!!(canOAuth && canApiKey)}
/>
<div className={cn(configuredDisabled && 'pointer-events-none opacity-50')}>
{
!isAuthorized && shouldShowAuthorizeCard && (
<div className='rounded-xl bg-background-section px-4 py-4'>
<div className='flex w-full justify-center'>
<div className='w-full max-w-[520px]'>
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled || configuredDisabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
theme='secondary'
showDivider={!!(canOAuth && canApiKey)}
/>
</div>
</div>
</div>
</div>
)
}
{
!isAuthorized && !shouldShowAuthorizeCard && (
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
{
isAuthorized && (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
selectedCredentialId={credentialId || '__workspace_default__'}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
{endUserSection}
)
}
{
!isAuthorized && !shouldShowAuthorizeCard && (
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled || configuredDisabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
{
isAuthorized && (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled || configuredDisabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
selectedCredentialId={credentialId || '__workspace_default__'}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
</div>
<EndUserCredentialSection
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
useEndUserCredentialEnabled={useEndUserCredentialEnabled}
endUserCredentialType={endUserCredentialType}
onEndUserCredentialChange={onEndUserCredentialChange}
onEndUserCredentialTypeChange={onEndUserCredentialTypeChange}
onCredentialAdded={invalidPluginCredentialInfo}
/>
</div>
)
}

View File

@ -1,17 +1,13 @@
import {
memo,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import type { ReactNode } from 'react'
import {
RiAddLine,
RiArrowDownSLine,
RiEqualizer2Line,
RiKey2Line,
RiUserStarLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
@ -23,10 +19,10 @@ import Authorize from './authorize'
import Authorized from './authorized'
import AddApiKeyButton from './authorize/add-api-key-button'
import AddOAuthButton from './authorize/add-oauth-button'
import EndUserCredentialSection from './end-user-credential-section'
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 = {
@ -63,50 +59,12 @@ const PluginAuth = ({
const shouldShowGuide = !!showConnectGuide
const [showCredentialPanel, setShowCredentialPanel] = useState(false)
const [showAddMenu, setShowAddMenu] = useState(false)
const [showEndUserTypeMenu, setShowEndUserTypeMenu] = useState(false)
const configuredDisabled = !!endUserCredentialEnabled
const shouldShowAuthorizeCard = useMemo(() => {
const hasCredential = credentials.length > 0
const canAdd = canOAuth || canApiKey || hasOAuthClientConfigured
return !hasCredential && canAdd
}, [credentials.length, canOAuth, canApiKey, hasOAuthClientConfigured])
const availableEndUserTypes = useMemo(() => {
const list: { value: string; label: string; icon: ReactNode }[] = []
if (canOAuth) {
list.push({
value: 'oauth2',
label: t('plugin.auth.endUserCredentials.optionOAuth'),
icon: <RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />,
})
}
if (canApiKey) {
list.push({
value: 'api-key',
label: t('plugin.auth.endUserCredentials.optionApiKey'),
icon: <RiKey2Line className='h-4 w-4 text-text-tertiary' />,
})
}
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)
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
@ -145,86 +103,18 @@ const PluginAuth = ({
}, [credentials, t])
const endUserSwitch = (
<div className='px-4 py-3'>
<div className='flex items-start gap-3'>
<RiUserStarLine className='mt-0.5 h-5 w-5 text-text-tertiary' />
<div className='flex-1 space-y-3'>
<div className='flex items-center justify-between gap-3'>
<div className='space-y-1'>
<div className='system-sm-semibold text-text-primary'>
{t('plugin.auth.endUserCredentials.title')}
</div>
<div className='system-xs-regular text-text-tertiary'>
{t('plugin.auth.endUserCredentials.desc')}
</div>
</div>
<Switch
size='md'
defaultValue={!!endUserCredentialEnabled}
onChange={onEndUserCredentialChange}
disabled={disabled}
/>
</div>
{
endUserCredentialEnabled && availableEndUserTypes.length > 0 && (
<div className='flex items-center justify-between gap-3'>
<div className='system-sm-semibold text-text-primary'>
{t('plugin.auth.endUserCredentials.typeLabel')}
</div>
<PortalToFollowElem
open={showEndUserTypeMenu}
onOpenChange={setShowEndUserTypeMenu}
placement='bottom-end'
offset={6}
>
<PortalToFollowElemTrigger asChild>
<button
type='button'
className='border-components-input-border flex h-9 min-w-[190px] items-center justify-between rounded-lg border bg-components-input-bg-normal px-3 text-left text-text-primary shadow-xs hover:bg-components-input-bg-hover'
onClick={() => setShowEndUserTypeMenu(v => !v)}
>
<span className='system-sm-semibold'>{endUserCredentialLabel}</span>
<RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[120]'>
<div className='w-[220px] rounded-xl border border-components-panel-border bg-components-panel-bg shadow-lg'>
<div className='flex flex-col gap-1 p-1'>
{canOAuth && (
<AddOAuthButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
className='w-full justify-between bg-transparent text-text-primary hover:bg-transparent'
buttonText={t('plugin.auth.addOAuth')}
disabled={disabled}
onUpdate={() => {
handleSelectEndUserType('oauth2')
invalidPluginCredentialInfo()
}}
/>
)}
{canApiKey && (
<AddApiKeyButton
pluginPayload={pluginPayload}
buttonVariant='ghost'
buttonText={t('plugin.auth.addApi')}
disabled={disabled}
onUpdate={() => {
handleSelectEndUserType('api-key')
invalidPluginCredentialInfo()
}}
/>
)}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
</div>
)
}
</div>
</div>
</div>
<EndUserCredentialSection
className='px-4 py-3'
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
useEndUserCredentialEnabled={endUserCredentialEnabled}
endUserCredentialType={endUserCredentialType}
onEndUserCredentialChange={onEndUserCredentialChange}
onEndUserCredentialTypeChange={onEndUserCredentialTypeChange}
onCredentialAdded={invalidPluginCredentialInfo}
/>
)
return (