dify/web/app/components/plugins/plugin-auth/plugin-auth.tsx

287 lines
11 KiB
TypeScript

import {
memo,
useEffect,
useMemo,
useState,
} from 'react'
import {
RiAddLine,
RiArrowDownSLine,
RiKey2Line,
} 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 EndUserCredentialSection from './end-user-credential-section'
import Item from './authorized/item'
import type { PluginPayload } from './types'
import { usePluginAuth } from './hooks/use-plugin-auth'
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,
canApiKey,
credentials,
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
hasOAuthClientConfigured,
} = usePluginAuth(pluginPayload, !!pluginPayload.provider)
const shouldShowGuide = !!showConnectGuide
const [showCredentialPanel, setShowCredentialPanel] = useState(false)
const [showAddMenu, setShowAddMenu] = 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 containerClassName = useMemo(() => {
if (showConnectGuide)
return className
return !isAuthorized ? className : undefined
}, [isAuthorized, className, showConnectGuide])
useEffect(() => {
if (isAuthorized)
setShowCredentialPanel(false)
}, [isAuthorized])
const credentialList = useMemo(() => {
return (
<div className={cn(!credentials.length ? 'mt-0' : 'mt-3')}>
{
credentials.length > 0
? (
<div className='space-y-1'>
{credentials.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled
disableRename
disableEdit
disableDelete
disableSetDefault
/>
))}
</div>
)
: null
}
</div>
)
}, [credentials, t])
const endUserSwitch = (
<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 (
<div className={cn(containerClassName)}>
{
shouldShowGuide && (
<PortalToFollowElem
open={showCredentialPanel}
onOpenChange={setShowCredentialPanel}
placement='bottom-start'
offset={8}
triggerPopupSameWidth
>
<PortalToFollowElemTrigger asChild>
<button
type='button'
className='flex w-full items-center justify-center gap-2 rounded-xl bg-primary-600 px-4 py-3 text-left text-white shadow-xs hover:bg-primary-700'
onClick={() => setShowCredentialPanel(v => !v)}
>
<div className='system-sm-semibold text-white'>
{t('plugin.auth.connectCredentials')}
</div>
<RiArrowDownSLine
className={cn(
'h-4 w-4 text-white transition-transform',
showCredentialPanel && 'rotate-180',
)}
/>
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'>
<div className='w-[420px] max-w-[calc(100vw-48px)] rounded-2xl border border-divider-subtle bg-components-panel-bg shadow-lg'>
<div className='border-b border-divider-subtle px-3 py-3'>
<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',
configuredDisabled && 'pointer-events-none opacity-50',
)}
onClick={() => {
setShowCredentialPanel(true)
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>
<div className={cn(configuredDisabled && 'pointer-events-none opacity-50')}>
{credentialList}
</div>
{
shouldShowAuthorizeCard && (
<div className={cn(
'mt-4 flex items-start gap-1.5 rounded-xl bg-background-section px-4 py-8',
configuredDisabled && 'pointer-events-none opacity-50',
)}>
<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>
{endUserSwitch}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
{
!shouldShowGuide && !isAuthorized && (
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
{
isAuthorized && !children && (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
{
isAuthorized && children
}
</div>
)
}
export default memo(PluginAuth)