This commit is contained in:
2025-12-01 17:21:38 +08:00
parent 32fee2b8ab
commit fab8c13cb3
7511 changed files with 996300 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import {
memo,
useState,
} from 'react'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import ApiKeyModal from './api-key-modal'
import type { PluginPayload } from '../types'
import type { FormSchema } from '@/app/components/base/form/types'
export type AddApiKeyButtonProps = {
pluginPayload: PluginPayload
buttonVariant?: ButtonProps['variant']
buttonText?: string
disabled?: boolean
onUpdate?: () => void
formSchemas?: FormSchema[]
}
const AddApiKeyButton = ({
pluginPayload,
buttonVariant = 'secondary-accent',
buttonText = 'Use Api Key',
disabled,
onUpdate,
formSchemas = [],
}: AddApiKeyButtonProps) => {
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
return (
<>
<Button
className='w-full'
variant={buttonVariant}
onClick={() => setIsApiKeyModalOpen(true)}
disabled={disabled}
>
{buttonText}
</Button>
{
isApiKeyModalOpen && (
<ApiKeyModal
pluginPayload={pluginPayload}
onClose={() => setIsApiKeyModalOpen(false)}
onUpdate={onUpdate}
formSchemas={formSchemas}
/>
)
}
</>
)
}
export default memo(AddApiKeyButton)

View File

@@ -0,0 +1,273 @@
import {
memo,
useCallback,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiClipboardLine,
RiEqualizer2Line,
RiInformation2Fill,
} from '@remixicon/react'
import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import OAuthClientSettings from './oauth-client-settings'
import cn from '@/utils/classnames'
import type { PluginPayload } from '../types'
import { openOAuthPopup } from '@/hooks/use-oauth'
import Badge from '@/app/components/base/badge'
import {
useGetPluginOAuthClientSchemaHook,
useGetPluginOAuthUrlHook,
} from '../hooks/use-credential'
import type { FormSchema } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import ActionButton from '@/app/components/base/action-button'
import { useRenderI18nObject } from '@/hooks/use-i18n'
export type AddOAuthButtonProps = {
pluginPayload: PluginPayload
buttonVariant?: ButtonProps['variant']
buttonText?: string
className?: string
buttonLeftClassName?: string
buttonRightClassName?: string
dividerClassName?: string
disabled?: boolean
onUpdate?: () => void
oAuthData?: {
schema?: FormSchema[]
is_oauth_custom_client_enabled?: boolean
is_system_oauth_params_exists?: boolean
client_params?: Record<string, any>
redirect_uri?: string
}
}
const AddOAuthButton = ({
pluginPayload,
buttonVariant = 'primary',
buttonText = 'use oauth',
className,
buttonLeftClassName,
buttonRightClassName,
dividerClassName,
disabled,
onUpdate,
oAuthData,
}: AddOAuthButtonProps) => {
const { t } = useTranslation()
const renderI18nObject = useRenderI18nObject()
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload)
const { data, isLoading } = useGetPluginOAuthClientSchemaHook(pluginPayload)
const mergedOAuthData = useMemo(() => {
if (oAuthData)
return oAuthData
return data
}, [oAuthData, data])
const {
schema = [],
is_oauth_custom_client_enabled,
is_system_oauth_params_exists,
client_params,
redirect_uri,
} = mergedOAuthData as any || {}
const isConfigured = is_system_oauth_params_exists || is_oauth_custom_client_enabled
const handleOAuth = useCallback(async () => {
const { authorization_url } = await getPluginOAuthUrl()
if (authorization_url) {
openOAuthPopup(
authorization_url,
() => onUpdate?.(),
)
}
}, [getPluginOAuthUrl, onUpdate])
const renderCustomLabel = useCallback((item: FormSchema) => {
return (
<div className='w-full'>
<div className='mb-4 flex rounded-xl bg-background-section-burn p-4'>
<div className='mr-3 flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg'>
<RiInformation2Fill className='h-5 w-5 text-text-accent' />
</div>
<div className='w-0 grow'>
<div className='system-sm-regular mb-1.5'>
{t('plugin.auth.clientInfo')}
</div>
{
redirect_uri && (
<div className='system-sm-medium flex w-full py-0.5'>
<div className='w-0 grow break-words break-all'>{redirect_uri}</div>
<ActionButton
className='shrink-0'
onClick={() => {
navigator.clipboard.writeText(redirect_uri || '')
}}
>
<RiClipboardLine className='h-4 w-4' />
</ActionButton>
</div>
)
}
</div>
</div>
<div className='system-sm-medium flex h-6 items-center text-text-secondary'>
{renderI18nObject(item.label as Record<string, string>)}
{
item.required && (
<span className='ml-1 text-text-destructive-secondary'>*</span>
)
}
</div>
</div>
)
}, [t, redirect_uri, renderI18nObject])
const memorizedSchemas = useMemo(() => {
const result: FormSchema[] = (schema as FormSchema[]).map((item, index) => {
return {
...item,
label: index === 0 ? renderCustomLabel(item) : item.label,
labelClassName: index === 0 ? 'h-auto' : undefined,
}
})
if (is_system_oauth_params_exists) {
result.unshift({
name: '__oauth_client__',
label: t('plugin.auth.oauthClient'),
type: FormTypeEnum.radio,
options: [
{
label: t('plugin.auth.default'),
value: 'default',
},
{
label: t('plugin.auth.custom'),
value: 'custom',
},
],
required: false,
default: is_oauth_custom_client_enabled ? 'custom' : 'default',
} as FormSchema)
result.forEach((item, index) => {
if (index > 0) {
item.show_on = [
{
variable: '__oauth_client__',
value: 'custom',
},
]
if (client_params)
item.default = client_params[item.name] || item.default
}
})
}
return result
}, [schema, renderCustomLabel, t, is_system_oauth_params_exists, is_oauth_custom_client_enabled, client_params])
const __auth_client__ = useMemo(() => {
if (isConfigured) {
if (is_oauth_custom_client_enabled)
return 'custom'
return 'default'
}
else {
if (is_system_oauth_params_exists)
return 'default'
return 'custom'
}
}, [isConfigured, is_oauth_custom_client_enabled, is_system_oauth_params_exists])
return (
<>
{
isConfigured && (
<Button
variant={buttonVariant}
className={cn(
'w-full px-0 py-0 hover:bg-components-button-primary-bg',
className,
)}
disabled={disabled}
onClick={handleOAuth}
>
<div className={cn(
'flex h-full w-0 grow items-center justify-center rounded-l-lg pl-0.5 hover:bg-components-button-primary-bg-hover',
buttonLeftClassName,
)}>
<div
className='truncate'
title={buttonText}
>
{buttonText}
</div>
{
is_oauth_custom_client_enabled && (
<Badge
className={cn(
'ml-1 mr-0.5',
buttonVariant === 'primary' && 'border-text-primary-on-surface bg-components-badge-bg-dimm text-text-primary-on-surface',
)}
>
{t('plugin.auth.custom')}
</Badge>
)
}
</div>
<div className={cn(
'h-4 w-[1px] shrink-0 bg-text-primary-on-surface opacity-[0.15]',
dividerClassName,
)}></div>
<div
className={cn(
'flex h-full w-8 shrink-0 items-center justify-center rounded-r-lg hover:bg-components-button-primary-bg-hover',
buttonRightClassName,
)}
onClick={(e) => {
e.stopPropagation()
setIsOAuthSettingsOpen(true)
}}
>
<RiEqualizer2Line className='h-4 w-4' />
</div>
</Button>
)
}
{
!isConfigured && (
<Button
variant={buttonVariant}
onClick={() => setIsOAuthSettingsOpen(true)}
disabled={disabled}
className='w-full'
>
<RiEqualizer2Line className='mr-0.5 h-4 w-4' />
{t('plugin.auth.setupOAuth')}
</Button>
)
}
{
isOAuthSettingsOpen && (
<OAuthClientSettings
pluginPayload={pluginPayload}
onClose={() => setIsOAuthSettingsOpen(false)}
disabled={disabled || isLoading}
schemas={memorizedSchemas}
onAuth={handleOAuth}
editValues={{
...client_params,
__oauth_client__: __auth_client__,
}}
hasOriginalClientParams={Object.keys(client_params || {}).length > 0}
onUpdate={onUpdate}
/>
)
}
</>
)
}
export default memo(AddOAuthButton)

View File

@@ -0,0 +1,171 @@
import {
memo,
useCallback,
useMemo,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal/modal'
import { CredentialTypeEnum } from '../types'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type {
FormRefObject,
FormSchema,
} from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import type { PluginPayload } from '../types'
import {
useAddPluginCredentialHook,
useGetPluginCredentialSchemaHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
import { ReadmeEntrance } from '../../readme-panel/entrance'
import { ReadmeShowType } from '../../readme-panel/store'
import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
export type ApiKeyModalProps = {
pluginPayload: PluginPayload
onClose?: () => void
editValues?: Record<string, any>
onRemove?: () => void
disabled?: boolean
onUpdate?: () => void
formSchemas?: FormSchema[]
}
const ApiKeyModal = ({
pluginPayload,
onClose,
editValues,
onRemove,
disabled,
onUpdate,
formSchemas: formSchemasFromProps = [],
}: ApiKeyModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((value: boolean) => {
doingActionRef.current = value
setDoingAction(value)
}, [])
const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
const mergedData = useMemo(() => {
if (formSchemasFromProps?.length)
return formSchemasFromProps
return data
}, [formSchemasFromProps, data])
const formSchemas = useMemo(() => {
return [
{
type: FormTypeEnum.textInput,
name: '__name__',
label: t('plugin.auth.authorizationName'),
required: false,
},
...mergedData,
]
}, [mergedData, t])
const defaultValues = formSchemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload)
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const formRef = useRef<FormRefObject>(null)
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
const {
isCheckValidated,
values,
} = formRef.current?.getFormValues({
needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
return
try {
const {
__name__,
__credential_id__,
...restValues
} = values
handleSetDoingAction(true)
if (editValues) {
await updatePluginCredential({
credentials: restValues,
credential_id: __credential_id__,
name: __name__ || '',
})
}
else {
await addPluginCredential({
credentials: restValues,
type: CredentialTypeEnum.API_KEY,
name: __name__ || '',
})
}
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [addPluginCredential, onClose, onUpdate, updatePluginCredential, notify, t, editValues, handleSetDoingAction])
return (
<Modal
size='md'
title={t('plugin.auth.useApiAuth')}
subTitle={t('plugin.auth.useApiAuthDesc')}
onClose={onClose}
onCancel={onClose}
footerSlot={
(<div></div>)
}
bottomSlot={<EncryptedBottom />}
onConfirm={handleConfirm}
showExtraButton={!!editValues}
onExtraButtonClick={onRemove}
disabled={disabled || isLoading || doingAction}
clickOutsideNotClose={true}
wrapperClassName='!z-[101]'
>
{pluginPayload.detail && (
<ReadmeEntrance pluginDetail={pluginPayload.detail} showType={ReadmeShowType.modal} />
)}
{
isLoading && (
<div className='flex h-40 items-center justify-center'>
<Loading />
</div>
)
}
{
!isLoading && !!mergedData.length && (
<AuthForm
ref={formRef}
formSchemas={formSchemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
)
}
</Modal>
)
}
export default memo(ApiKeyModal)

View File

@@ -0,0 +1,138 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import AddOAuthButton from './add-oauth-button'
import type { AddOAuthButtonProps } from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
import type { AddApiKeyButtonProps } from './add-api-key-button'
import type { PluginPayload } from '../types'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
type AuthorizeProps = {
pluginPayload: PluginPayload
theme?: 'primary' | 'secondary'
showDivider?: boolean
canOAuth?: boolean
canApiKey?: boolean
disabled?: boolean
onUpdate?: () => void
notAllowCustomCredential?: boolean
}
const Authorize = ({
pluginPayload,
theme = 'primary',
showDivider = true,
canOAuth,
canApiKey,
disabled,
onUpdate,
notAllowCustomCredential,
}: AuthorizeProps) => {
const { t } = useTranslation()
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
buttonVariant: 'secondary',
className: 'hover:bg-components-button-secondary-bg',
buttonLeftClassName: 'hover:bg-components-button-secondary-bg-hover',
buttonRightClassName: 'hover:bg-components-button-secondary-bg-hover',
dividerClassName: 'bg-divider-regular opacity-100',
pluginPayload,
}
}
return {
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
pluginPayload,
}
}, [canApiKey, theme, pluginPayload, t])
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
pluginPayload,
buttonVariant: 'secondary',
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
}
}
return {
pluginPayload,
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
buttonVariant: !canOAuth ? 'primary' : 'secondary-accent',
}
}, [canOAuth, theme, pluginPayload, t])
const OAuthButton = useMemo(() => {
const Item = (
<div className={cn('min-w-0 flex-[1]', notAllowCustomCredential && 'opacity-50')}>
<AddOAuthButton
{...oAuthButtonProps}
disabled={disabled || notAllowCustomCredential}
onUpdate={onUpdate}
/>
</div>
)
if (notAllowCustomCredential) {
return (
<Tooltip popupContent={t('plugin.auth.credentialUnavailable')}>
{Item}
</Tooltip>
)
}
return Item
}, [notAllowCustomCredential, oAuthButtonProps, disabled, onUpdate, t])
const ApiKeyButton = useMemo(() => {
const Item = (
<div className={cn('min-w-0 flex-[1]', notAllowCustomCredential && 'opacity-50')}>
<AddApiKeyButton
{...apiKeyButtonProps}
disabled={disabled || notAllowCustomCredential}
onUpdate={onUpdate}
/>
</div>
)
if (notAllowCustomCredential) {
return (
<Tooltip popupContent={t('plugin.auth.credentialUnavailable')}>
{Item}
</Tooltip>
)
}
return Item
}, [notAllowCustomCredential, apiKeyButtonProps, disabled, onUpdate, t])
return (
<>
<div className='flex items-center space-x-1.5'>
{
canOAuth && (
OAuthButton
)
}
{
showDivider && canOAuth && canApiKey && (
<div className='system-2xs-medium-uppercase flex shrink-0 flex-col items-center justify-between text-text-tertiary'>
<div className='h-2 w-[1px] bg-divider-subtle'></div>
or
<div className='h-2 w-[1px] bg-divider-subtle'></div>
</div>
)
}
{
canApiKey && (
ApiKeyButton
)
}
</div>
</>
)
}
export default memo(Authorize)

View File

@@ -0,0 +1,177 @@
import {
memo,
useCallback,
useRef,
useState,
} from 'react'
import {
useForm,
useStore,
} from '@tanstack/react-form'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal/modal'
import {
useDeletePluginOAuthCustomClientHook,
useInvalidPluginOAuthClientSchemaHook,
useSetPluginOAuthCustomClientHook,
} from '../hooks/use-credential'
import type { PluginPayload } from '../types'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type {
FormRefObject,
FormSchema,
} from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import { ReadmeEntrance } from '../../readme-panel/entrance'
import { ReadmeShowType } from '../../readme-panel/store'
type OAuthClientSettingsProps = {
pluginPayload: PluginPayload
onClose?: () => void
editValues?: Record<string, any>
disabled?: boolean
schemas: FormSchema[]
onAuth?: () => Promise<void>
hasOriginalClientParams?: boolean
onUpdate?: () => void
}
const OAuthClientSettings = ({
pluginPayload,
onClose,
editValues,
disabled,
schemas,
onAuth,
hasOriginalClientParams,
onUpdate,
}: OAuthClientSettingsProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((value: boolean) => {
doingActionRef.current = value
setDoingAction(value)
}, [])
const defaultValues = schemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload)
const invalidPluginOAuthClientSchema = useInvalidPluginOAuthClientSchemaHook(pluginPayload)
const formRef = useRef<FormRefObject>(null)
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
try {
const {
isCheckValidated,
values,
} = formRef.current?.getFormValues({
needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
throw new Error('error')
const {
__oauth_client__,
...restValues
} = values
handleSetDoingAction(true)
await setPluginOAuthCustomClient({
client_params: restValues,
enable_oauth_custom_client: __oauth_client__ === 'custom',
})
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
onUpdate?.()
invalidPluginOAuthClientSchema()
}
finally {
handleSetDoingAction(false)
}
}, [onClose, onUpdate, invalidPluginOAuthClientSchema, setPluginOAuthCustomClient, notify, t, handleSetDoingAction])
const handleConfirmAndAuthorize = useCallback(async () => {
await handleConfirm()
if (onAuth)
await onAuth()
}, [handleConfirm, onAuth])
const { mutateAsync: deletePluginOAuthCustomClient } = useDeletePluginOAuthCustomClientHook(pluginPayload)
const handleRemove = useCallback(async () => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await deletePluginOAuthCustomClient()
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
onUpdate?.()
invalidPluginOAuthClientSchema()
}
finally {
handleSetDoingAction(false)
}
}, [onUpdate, invalidPluginOAuthClientSchema, deletePluginOAuthCustomClient, notify, t, handleSetDoingAction, onClose])
const form = useForm({
defaultValues: editValues || defaultValues,
})
const __oauth_client__ = useStore(form.store, s => s.values.__oauth_client__)
return (
<Modal
title={t('plugin.auth.oauthClientSettings')}
confirmButtonText={t('plugin.auth.saveAndAuth')}
cancelButtonText={t('plugin.auth.saveOnly')}
extraButtonText={t('common.operation.cancel')}
showExtraButton
extraButtonVariant='secondary'
onExtraButtonClick={onClose}
onClose={onClose}
onCancel={handleConfirm}
onConfirm={handleConfirmAndAuthorize}
disabled={disabled || doingAction}
footerSlot={
__oauth_client__ === 'custom' && hasOriginalClientParams && (
<div className='grow'>
<Button
variant='secondary'
className='text-components-button-destructive-secondary-text'
disabled={disabled || doingAction || !editValues}
onClick={handleRemove}
>
{t('common.operation.remove')}
</Button>
</div>
)
}
containerClassName='pt-0'
wrapperClassName='!z-[101]'
clickOutsideNotClose={true}
>
{pluginPayload.detail && (
<ReadmeEntrance pluginDetail={pluginPayload.detail} showType={ReadmeShowType.modal} />
)}
<AuthForm
formFromProps={form}
ref={formRef}
formSchemas={schemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
</Modal>
)
}
export default memo(OAuthClientSettings)

View File

@@ -0,0 +1,43 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
type AuthorizedInDataSourceNodeProps = {
authorizationsNum: number
onJumpToDataSourcePage: () => void
}
const AuthorizedInDataSourceNode = ({
authorizationsNum,
onJumpToDataSourcePage,
}: AuthorizedInDataSourceNodeProps) => {
const { t } = useTranslation()
return (
<Button
size='small'
onClick={onJumpToDataSourcePage}
>
<Indicator
className='mr-1.5'
color='green'
/>
{
authorizationsNum > 1
? t('plugin.auth.authorizations')
: t('plugin.auth.authorization')
}
<RiEqualizer2Line
className={cn(
'h-3.5 w-3.5 text-components-button-ghost-text',
)}
/>
</Button>
)
}
export default memo(AuthorizedInDataSourceNode)

View File

@@ -0,0 +1,142 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import type {
Credential,
PluginPayload,
} from './types'
import {
Authorized,
usePluginAuth,
} from '.'
type AuthorizedInNodeProps = {
pluginPayload: PluginPayload
onAuthorizationItemClick: (id: string) => void
credentialId?: string
}
const AuthorizedInNode = ({
pluginPayload,
onAuthorizationItemClick,
credentialId,
}: AuthorizedInNodeProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const {
canApiKey,
canOAuth,
credentials,
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, true)
const renderTrigger = useCallback((open?: boolean) => {
let label = ''
let removed = false
let unavailable = false
let color = 'green'
let defaultUnavailable = false
if (!credentialId) {
label = t('plugin.auth.workspaceDefault')
const defaultCredential = credentials.find(c => c.is_default)
if (defaultCredential?.not_allowed_to_use) {
color = 'gray'
defaultUnavailable = true
}
}
else {
const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential
unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise
if (removed)
color = 'red'
else if (unavailable)
color = 'gray'
}
return (
<Button
size='small'
className={cn(
open && !removed && 'bg-components-button-ghost-bg-hover',
removed && 'bg-transparent text-text-destructive',
)}
variant={(defaultUnavailable || unavailable) ? 'ghost' : 'secondary'}
>
<Indicator
className='mr-1.5'
color={color as any}
/>
{label}
{
(unavailable || defaultUnavailable) && (
<>
&nbsp;
{t('plugin.auth.unavailable')}
</>
)
}
<RiArrowDownSLine
className={cn(
'h-3.5 w-3.5 text-components-button-ghost-text',
removed && 'text-text-destructive',
)}
/>
</Button>
)
}, [credentialId, credentials, t])
const defaultUnavailable = credentials.find(c => c.is_default)?.not_allowed_to_use
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
name: t('plugin.auth.workspaceDefault'),
provider: '',
is_default: !credentialId,
isWorkspaceDefault: true,
not_allowed_to_use: defaultUnavailable,
},
]
const handleAuthorizationItemClick = useCallback((id: string) => {
onAuthorizationItemClick(id)
setIsOpen(false)
}, [
onAuthorizationItemClick,
setIsOpen,
])
return (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
offset={4}
placement='bottom-end'
triggerPopupSameWidth={false}
popupClassName='w-[360px]'
disabled={disabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
selectedCredentialId={credentialId || '__workspace_default__'}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
export default memo(AuthorizedInNode)

View File

@@ -0,0 +1,357 @@
import {
memo,
useCallback,
useRef,
useState,
} from 'react'
import {
RiArrowDownSLine,
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type {
PortalToFollowElemOptions,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
import Authorize from '../authorize'
import type { Credential } from '../types'
import { CredentialTypeEnum } from '../types'
import ApiKeyModal from '../authorize/api-key-modal'
import Item from './item'
import { useToastContext } from '@/app/components/base/toast'
import type { PluginPayload } from '../types'
import {
useDeletePluginCredentialHook,
useSetPluginDefaultCredentialHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
type AuthorizedProps = {
pluginPayload: PluginPayload
credentials: Credential[]
canOAuth?: boolean
canApiKey?: boolean
disabled?: boolean
renderTrigger?: (open?: boolean) => React.ReactNode
isOpen?: boolean
onOpenChange?: (open: boolean) => void
offset?: PortalToFollowElemOptions['offset']
placement?: PortalToFollowElemOptions['placement']
triggerPopupSameWidth?: boolean
popupClassName?: string
disableSetDefault?: boolean
onItemClick?: (id: string) => void
extraAuthorizationItems?: Credential[]
showItemSelectedIcon?: boolean
selectedCredentialId?: string
onUpdate?: () => void
notAllowCustomCredential?: boolean
}
const Authorized = ({
pluginPayload,
credentials,
canOAuth,
canApiKey,
disabled,
renderTrigger,
isOpen,
onOpenChange,
offset = 8,
placement = 'bottom-start',
triggerPopupSameWidth = true,
popupClassName,
disableSetDefault,
onItemClick,
extraAuthorizationItems,
showItemSelectedIcon,
selectedCredentialId,
onUpdate,
notAllowCustomCredential,
}: AuthorizedProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [isLocalOpen, setIsLocalOpen] = useState(false)
const mergedIsOpen = isOpen ?? isLocalOpen
const setMergedIsOpen = useCallback((open: boolean) => {
if (onOpenChange)
onOpenChange(open)
setIsLocalOpen(open)
}, [onOpenChange])
const oAuthCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.OAUTH2)
const apiKeyCredentials = credentials.filter(credential => credential.credential_type === CredentialTypeEnum.API_KEY)
const pendingOperationCredentialId = useRef<string | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
const openConfirm = useCallback((credentialId?: string) => {
if (credentialId)
pendingOperationCredentialId.current = credentialId
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const closeConfirm = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}
finally {
handleSetDoingAction(false)
}
}, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
const handleEdit = useCallback((id: string, values: Record<string, any>) => {
pendingOperationCredentialId.current = id
setEditValues(values)
}, [])
const handleRemove = useCallback(() => {
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
const handleSetDefault = useCallback(async (id: string) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await setPluginDefaultCredential(id)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const handleRename = useCallback(async (payload: {
credential_id: string
name: string
}) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await updatePluginCredential(payload)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
const unavailableCredentials = credentials.filter(credential => credential.not_allowed_to_use)
const unavailableCredential = credentials.find(credential => credential.not_allowed_to_use && credential.is_default)
return (
<>
<PortalToFollowElem
open={mergedIsOpen}
onOpenChange={setMergedIsOpen}
placement={placement}
offset={offset}
triggerPopupSameWidth={triggerPopupSameWidth}
>
<PortalToFollowElemTrigger
onClick={() => setMergedIsOpen(!mergedIsOpen)}
asChild
>
{
renderTrigger
? renderTrigger(mergedIsOpen)
: (
<Button
className={cn(
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
)}>
<Indicator className='mr-2' color={unavailableCredential ? 'gray' : 'green'} />
{credentials.length}&nbsp;
{
credentials.length > 1
? t('plugin.auth.authorizations')
: t('plugin.auth.authorization')
}
{
!!unavailableCredentials.length && (
` (${unavailableCredentials.length} ${t('plugin.auth.unavailable')})`
)
}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
}
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[100]'>
<div className={cn(
'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
popupClassName,
)}>
<div className='py-1'>
{
!!extraAuthorizationItems?.length && (
<div className='p-1'>
{
extraAuthorizationItems.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
onItemClick={onItemClick}
disableRename
disableEdit
disableDelete
disableSetDefault
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}
</div>
)
}
{
!!oAuthCredentials.length && (
<div className='p-1'>
<div className={cn(
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
showItemSelectedIcon && 'pl-7',
)}>
OAuth
</div>
{
oAuthCredentials.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
disableEdit
onDelete={openConfirm}
onSetDefault={handleSetDefault}
onRename={handleRename}
disableSetDefault={disableSetDefault}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}
</div>
)
}
{
!!apiKeyCredentials.length && (
<div className='p-1'>
<div className={cn(
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
showItemSelectedIcon && 'pl-7',
)}>
API Keys
</div>
{
apiKeyCredentials.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
onDelete={openConfirm}
onEdit={handleEdit}
onSetDefault={handleSetDefault}
disableSetDefault={disableSetDefault}
disableRename
onItemClick={onItemClick}
onRename={handleRename}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}
</div>
)
}
</div>
{
!notAllowCustomCredential && (
<>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='p-2'>
<Authorize
pluginPayload={pluginPayload}
theme='secondary'
showDivider={false}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={onUpdate}
/>
</div>
</>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
{
deleteCredentialId && (
<Confirm
isShow
title={t('datasetDocuments.list.delete.title')}
isDisabled={doingAction}
onCancel={closeConfirm}
onConfirm={handleConfirm}
/>
)
}
{
!!editValues && (
<ApiKeyModal
pluginPayload={pluginPayload}
editValues={editValues}
onClose={() => {
setEditValues(null)
pendingOperationCredentialId.current = null
}}
onRemove={handleRemove}
disabled={disabled || doingAction}
onUpdate={onUpdate}
/>
)
}
</>
)
}
export default memo(Authorized)

View File

@@ -0,0 +1,246 @@
import {
memo,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCheckLine,
RiDeleteBinLine,
RiEditLine,
RiEqualizer2Line,
} from '@remixicon/react'
import Indicator from '@/app/components/header/indicator'
import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import cn from '@/utils/classnames'
import type { Credential } from '../types'
import { CredentialTypeEnum } from '../types'
type ItemProps = {
credential: Credential
disabled?: boolean
onDelete?: (id: string) => void
onEdit?: (id: string, values: Record<string, any>) => void
onSetDefault?: (id: string) => void
onRename?: (payload: {
credential_id: string
name: string
}) => void
disableRename?: boolean
disableEdit?: boolean
disableDelete?: boolean
disableSetDefault?: boolean
onItemClick?: (id: string) => void
showSelectedIcon?: boolean
selectedCredentialId?: string
}
const Item = ({
credential,
disabled,
onDelete,
onEdit,
onSetDefault,
onRename,
disableRename,
disableEdit,
disableDelete,
disableSetDefault,
onItemClick,
showSelectedIcon,
selectedCredentialId,
}: ItemProps) => {
const { t } = useTranslation()
const [renaming, setRenaming] = useState(false)
const [renameValue, setRenameValue] = useState(credential.name)
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete && disableSetDefault)
}, [disableRename, disableEdit, disableDelete, disableSetDefault])
const CredentialItem = (
<div
key={credential.id}
className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
renaming && 'bg-state-base-hover',
(disabled || credential.not_allowed_to_use) && 'cursor-not-allowed opacity-50',
)}
onClick={() => {
if (credential.not_allowed_to_use || disabled)
return
onItemClick?.(credential.id === '__workspace_default__' ? '' : credential.id)
}}
>
{
renaming && (
<div className='flex w-full items-center space-x-1'>
<Input
wrapperClassName='grow rounded-[6px]'
className='h-6'
value={renameValue}
onChange={e => setRenameValue(e.target.value)}
placeholder={t('common.placeholder.input')}
onClick={e => e.stopPropagation()}
/>
<Button
size='small'
variant='primary'
onClick={(e) => {
e.stopPropagation()
onRename?.({
credential_id: credential.id,
name: renameValue,
})
setRenaming(false)
}}
>
{t('common.operation.save')}
</Button>
<Button
size='small'
onClick={(e) => {
e.stopPropagation()
setRenaming(false)
}}
>
{t('common.operation.cancel')}
</Button>
</div>
)
}
{
!renaming && (
<div className='flex w-0 grow items-center space-x-1.5'>
{
showSelectedIcon && (
<div className='h-4 w-4'>
{
selectedCredentialId === credential.id && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}
</div>
)
}
<Indicator
className='ml-2 mr-1.5 shrink-0'
color={credential.not_allowed_to_use ? 'gray' : 'green'}
/>
<div
className='system-md-regular truncate text-text-secondary'
title={credential.name}
>
{credential.name}
</div>
{
credential.is_default && (
<Badge className='shrink-0'>
{t('plugin.auth.default')}
</Badge>
)
}
</div>
)
}
{
credential.from_enterprise && (
<Badge className='shrink-0'>
Enterprise
</Badge>
)
}
{
showAction && !renaming && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{
!credential.is_default && !disableSetDefault && !credential.not_allowed_to_use && (
<Button
size='small'
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onSetDefault?.(credential.id)
}}
>
{t('plugin.auth.setDefault')}
</Button>
)
}
{
!disableRename && !credential.from_enterprise && !credential.not_allowed_to_use && (
<Tooltip popupContent={t('common.operation.rename')}>
<ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
setRenaming(true)
setRenameValue(credential.name)
}}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
{
!isOAuth && !disableEdit && !credential.from_enterprise && !credential.not_allowed_to_use && (
<Tooltip popupContent={t('common.operation.edit')}>
<ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onEdit?.(
credential.id,
{
...credential.credentials,
__name__: credential.name,
__credential_id__: credential.id,
},
)
}}
>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
{
!disableDelete && !credential.from_enterprise && (
<Tooltip popupContent={t('common.operation.delete')}>
<ActionButton
className='hover:bg-transparent'
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onDelete?.(credential.id)
}}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
</ActionButton>
</Tooltip>
)
}
</div>
)
}
</div>
)
if (credential.not_allowed_to_use) {
return (
<Tooltip popupContent={t('plugin.auth.customCredentialUnavailable')}>
{CredentialItem}
</Tooltip>
)
}
return (
CredentialItem
)
}
export default memo(Item)

View File

@@ -0,0 +1,95 @@
import {
useAddPluginCredential,
useDeletePluginCredential,
useDeletePluginOAuthCustomClient,
useGetPluginCredentialInfo,
useGetPluginCredentialSchema,
useGetPluginOAuthClientSchema,
useGetPluginOAuthUrl,
useInvalidPluginCredentialInfo,
useInvalidPluginOAuthClientSchema,
useSetPluginDefaultCredential,
useSetPluginOAuthCustomClient,
useUpdatePluginCredential,
} from '@/service/use-plugins-auth'
import { useGetApi } from './use-get-api'
import type { PluginPayload } from '../types'
import type { CredentialTypeEnum } from '../types'
import { useInvalidToolsByType } from '@/service/use-tools'
export const useGetPluginCredentialInfoHook = (pluginPayload: PluginPayload, enable?: boolean) => {
const apiMap = useGetApi(pluginPayload)
return useGetPluginCredentialInfo(enable ? apiMap.getCredentialInfo : '')
}
export const useDeletePluginCredentialHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useDeletePluginCredential(apiMap.deleteCredential)
}
export const useInvalidPluginCredentialInfoHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
const invalidPluginCredentialInfo = useInvalidPluginCredentialInfo(apiMap.getCredentialInfo)
const providerType = pluginPayload.providerType
const invalidToolsByType = useInvalidToolsByType(providerType)
return () => {
invalidPluginCredentialInfo()
invalidToolsByType()
}
}
export const useSetPluginDefaultCredentialHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useSetPluginDefaultCredential(apiMap.setDefaultCredential)
}
export const useGetPluginCredentialSchemaHook = (pluginPayload: PluginPayload, credentialType: CredentialTypeEnum) => {
const apiMap = useGetApi(pluginPayload)
return useGetPluginCredentialSchema(apiMap.getCredentialSchema(credentialType))
}
export const useAddPluginCredentialHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useAddPluginCredential(apiMap.addCredential)
}
export const useUpdatePluginCredentialHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useUpdatePluginCredential(apiMap.updateCredential)
}
export const useGetPluginOAuthUrlHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useGetPluginOAuthUrl(apiMap.getOauthUrl)
}
export const useGetPluginOAuthClientSchemaHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useGetPluginOAuthClientSchema(apiMap.getOauthClientSchema)
}
export const useInvalidPluginOAuthClientSchemaHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useInvalidPluginOAuthClientSchema(apiMap.getOauthClientSchema)
}
export const useSetPluginOAuthCustomClientHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useSetPluginOAuthCustomClient(apiMap.setCustomOauthClient)
}
export const useDeletePluginOAuthCustomClientHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useDeletePluginOAuthCustomClient(apiMap.deleteCustomOAuthClient)
}

View File

@@ -0,0 +1,57 @@
import {
AuthCategory,
} from '../types'
import type {
CredentialTypeEnum,
PluginPayload,
} from '../types'
export const useGetApi = ({ category = AuthCategory.tool, provider }: PluginPayload) => {
if (category === AuthCategory.tool) {
return {
getCredentialInfo: `/workspaces/current/tool-provider/builtin/${provider}/credential/info`,
setDefaultCredential: `/workspaces/current/tool-provider/builtin/${provider}/default-credential`,
getCredentials: `/workspaces/current/tool-provider/builtin/${provider}/credentials`,
addCredential: `/workspaces/current/tool-provider/builtin/${provider}/add`,
updateCredential: `/workspaces/current/tool-provider/builtin/${provider}/update`,
deleteCredential: `/workspaces/current/tool-provider/builtin/${provider}/delete`,
getCredentialSchema: (credential_type: CredentialTypeEnum) => `/workspaces/current/tool-provider/builtin/${provider}/credential/schema/${credential_type}`,
getOauthUrl: `/oauth/plugin/${provider}/tool/authorization-url`,
getOauthClientSchema: `/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`,
setCustomOauthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`,
getCustomOAuthClientValues: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`,
deleteCustomOAuthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`,
}
}
if (category === AuthCategory.datasource) {
return {
getCredentialInfo: '',
setDefaultCredential: `/auth/plugin/datasource/${provider}/default`,
getCredentials: `/auth/plugin/datasource/${provider}`,
addCredential: `/auth/plugin/datasource/${provider}`,
updateCredential: `/auth/plugin/datasource/${provider}/update`,
deleteCredential: `/auth/plugin/datasource/${provider}/delete`,
getCredentialSchema: () => '',
getOauthUrl: `/oauth/plugin/${provider}/datasource/get-authorization-url`,
getOauthClientSchema: '',
setCustomOauthClient: `/auth/plugin/datasource/${provider}/custom-client`,
deleteCustomOAuthClient: `/auth/plugin/datasource/${provider}/custom-client`,
}
}
return {
getCredentialInfo: '',
setDefaultCredential: '',
getCredentials: '',
addCredential: '',
updateCredential: '',
deleteCredential: '',
getCredentialSchema: () => '',
getOauthUrl: '',
getOauthClientSchema: '',
setCustomOauthClient: '',
getCustomOAuthClientValues: '',
deleteCustomOAuthClient: '',
}
}

View File

@@ -0,0 +1,125 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useToastContext } from '@/app/components/base/toast'
import type { PluginPayload } from '../types'
import {
useDeletePluginCredentialHook,
useSetPluginDefaultCredentialHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
export const usePluginAuthAction = (
pluginPayload: PluginPayload,
onUpdate?: () => void,
) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const pendingOperationCredentialId = useRef<string | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
const openConfirm = useCallback((credentialId?: string) => {
if (credentialId)
pendingOperationCredentialId.current = credentialId
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const closeConfirm = useCallback(() => {
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
}, [])
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((doing: boolean) => {
doingActionRef.current = doing
setDoingAction(doing)
}, [])
const [editValues, setEditValues] = useState<Record<string, any> | null>(null)
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
setEditValues(null)
}
finally {
handleSetDoingAction(false)
}
}, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
const handleEdit = useCallback((id: string, values: Record<string, any>) => {
pendingOperationCredentialId.current = id
setEditValues(values)
}, [])
const handleRemove = useCallback(() => {
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
const handleSetDefault = useCallback(async (id: string) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await setPluginDefaultCredential(id)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const handleRename = useCallback(async (payload: {
credential_id: string
name: string
}) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await updatePluginCredential(payload)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
return {
doingAction,
handleSetDoingAction,
openConfirm,
closeConfirm,
deleteCredentialId,
setDeleteCredentialId,
handleConfirm,
editValues,
setEditValues,
handleEdit,
handleRemove,
handleSetDefault,
handleRename,
pendingOperationCredentialId,
}
}

View File

@@ -0,0 +1,26 @@
import { useAppContext } from '@/context/app-context'
import {
useGetPluginCredentialInfoHook,
useInvalidPluginCredentialInfoHook,
} from './use-credential'
import { CredentialTypeEnum } from '../types'
import type { PluginPayload } from '../types'
export const usePluginAuth = (pluginPayload: PluginPayload, enable?: boolean) => {
const { data } = useGetPluginCredentialInfoHook(pluginPayload, enable)
const { isCurrentWorkspaceManager } = useAppContext()
const isAuthorized = !!data?.credentials.length
const canOAuth = data?.supported_credential_types.includes(CredentialTypeEnum.OAUTH2)
const canApiKey = data?.supported_credential_types.includes(CredentialTypeEnum.API_KEY)
const invalidPluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload)
return {
isAuthorized,
canOAuth,
canApiKey,
credentials: data?.credentials || [],
disabled: !isCurrentWorkspaceManager,
notAllowCustomCredential: data?.allow_custom_token === false,
invalidPluginCredentialInfo,
}
}

View File

@@ -0,0 +1,12 @@
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 { 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'
export { default as AddOAuthButton } from './authorize/add-oauth-button'
export { default as AddApiKeyButton } from './authorize/add-api-key-button'
export { default as ApiKeyModal } from './authorize/api-key-modal'
export * from './hooks/use-plugin-auth-action'
export * from './types'

View File

@@ -0,0 +1,136 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { RiArrowDownSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Authorize from './authorize'
import Authorized from './authorized'
import type {
Credential,
PluginPayload,
} from './types'
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'
type PluginAuthInAgentProps = {
pluginPayload: PluginPayload
credentialId?: string
onAuthorizationItemClick?: (id: string) => void
}
const PluginAuthInAgent = ({
pluginPayload,
credentialId,
onAuthorizationItemClick,
}: PluginAuthInAgentProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const {
isAuthorized,
canOAuth,
canApiKey,
credentials,
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, true)
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
name: t('plugin.auth.workspaceDefault'),
provider: '',
is_default: !credentialId,
isWorkspaceDefault: true,
},
]
const handleAuthorizationItemClick = useCallback((id: string) => {
onAuthorizationItemClick?.(id)
setIsOpen(false)
}, [
onAuthorizationItemClick,
setIsOpen,
])
const renderTrigger = useCallback((isOpen?: boolean) => {
let label = ''
let removed = false
let unavailable = false
let color = 'green'
if (!credentialId) {
label = t('plugin.auth.workspaceDefault')
}
else {
const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential
unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise
if (removed)
color = 'red'
else if (unavailable)
color = 'gray'
}
return (
<Button
className={cn(
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
removed && 'text-text-destructive',
)}>
<Indicator
className='mr-2'
color={color as any}
/>
{label}
{
unavailable && t('plugin.auth.unavailable')
}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
}, [credentialId, credentials, t])
return (
<>
{
!isAuthorized && (
<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}
/>
)
}
</>
)
}
export default memo(PluginAuthInAgent)

View File

@@ -0,0 +1,39 @@
import { memo } from 'react'
import type { ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { RiAddLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
type PluginAuthInDataSourceNodeProps = {
children?: ReactNode
isAuthorized?: boolean
onJumpToDataSourcePage: () => void
}
const PluginAuthInDataSourceNode = ({
children,
isAuthorized,
onJumpToDataSourcePage,
}: PluginAuthInDataSourceNodeProps) => {
const { t } = useTranslation()
return (
<>
{
!isAuthorized && (
<div className='px-4 pb-2'>
<Button
className='w-full'
variant='primary'
onClick={onJumpToDataSourcePage}
>
<RiAddLine className='mr-1 h-4 w-4' />
{t('common.integrations.connect')}
</Button>
</div>
)
}
{isAuthorized && children}
</>
)
}
export default memo(PluginAuthInDataSourceNode)

View File

@@ -0,0 +1,62 @@
import { memo } from 'react'
import Authorize from './authorize'
import Authorized from './authorized'
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
}
const PluginAuth = ({
pluginPayload,
children,
className,
}: PluginAuthProps) => {
const {
isAuthorized,
canOAuth,
canApiKey,
credentials,
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, !!pluginPayload.provider)
return (
<div className={cn(!isAuthorized && className)}>
{
!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)

View File

@@ -0,0 +1,36 @@
import type { CollectionType } from '../../tools/types'
import type { PluginDetail } from '../types'
export type { AddApiKeyButtonProps } from './authorize/add-api-key-button'
export type { AddOAuthButtonProps } from './authorize/add-oauth-button'
export enum AuthCategory {
tool = 'tool',
datasource = 'datasource',
model = 'model',
trigger = 'trigger',
}
export type PluginPayload = {
category: AuthCategory
provider: string
providerType?: CollectionType | string
detail?: PluginDetail
}
export enum CredentialTypeEnum {
OAUTH2 = 'oauth2',
API_KEY = 'api-key',
}
export type Credential = {
id: string
name: string
provider: string
credential_type?: CredentialTypeEnum
is_default: boolean
credentials?: Record<string, any>
isWorkspaceDefault?: boolean
from_enterprise?: boolean
not_allowed_to_use?: boolean
}

View File

@@ -0,0 +1,10 @@
export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record<string, any>) => {
const transformedValues: Record<string, any> = { ...values }
isPristineSecretInputNames.forEach((name) => {
if (transformedValues[name])
transformedValues[name] = '[__HIDDEN__]'
})
return transformedValues
}