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)