merge feat/tool-oauth
commit
73e209505a
@ -0,0 +1,65 @@
|
||||
import pytest
|
||||
|
||||
from libs.helper import extract_tenant_id
|
||||
from models.account import Account
|
||||
from models.model import EndUser
|
||||
|
||||
|
||||
class TestExtractTenantId:
|
||||
"""Test cases for the extract_tenant_id utility function."""
|
||||
|
||||
def test_extract_tenant_id_from_account_with_tenant(self):
|
||||
"""Test extracting tenant_id from Account with current_tenant_id."""
|
||||
# Create a mock Account object
|
||||
account = Account()
|
||||
# Mock the current_tenant_id property
|
||||
account._current_tenant = type("MockTenant", (), {"id": "account-tenant-123"})()
|
||||
|
||||
tenant_id = extract_tenant_id(account)
|
||||
assert tenant_id == "account-tenant-123"
|
||||
|
||||
def test_extract_tenant_id_from_account_without_tenant(self):
|
||||
"""Test extracting tenant_id from Account without current_tenant_id."""
|
||||
# Create a mock Account object
|
||||
account = Account()
|
||||
account._current_tenant = None
|
||||
|
||||
tenant_id = extract_tenant_id(account)
|
||||
assert tenant_id is None
|
||||
|
||||
def test_extract_tenant_id_from_enduser_with_tenant(self):
|
||||
"""Test extracting tenant_id from EndUser with tenant_id."""
|
||||
# Create a mock EndUser object
|
||||
end_user = EndUser()
|
||||
end_user.tenant_id = "enduser-tenant-456"
|
||||
|
||||
tenant_id = extract_tenant_id(end_user)
|
||||
assert tenant_id == "enduser-tenant-456"
|
||||
|
||||
def test_extract_tenant_id_from_enduser_without_tenant(self):
|
||||
"""Test extracting tenant_id from EndUser without tenant_id."""
|
||||
# Create a mock EndUser object
|
||||
end_user = EndUser()
|
||||
end_user.tenant_id = None
|
||||
|
||||
tenant_id = extract_tenant_id(end_user)
|
||||
assert tenant_id is None
|
||||
|
||||
def test_extract_tenant_id_with_invalid_user_type(self):
|
||||
"""Test extracting tenant_id with invalid user type raises ValueError."""
|
||||
invalid_user = "not_a_user_object"
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"):
|
||||
extract_tenant_id(invalid_user)
|
||||
|
||||
def test_extract_tenant_id_with_none_user(self):
|
||||
"""Test extracting tenant_id with None user raises ValueError."""
|
||||
with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"):
|
||||
extract_tenant_id(None)
|
||||
|
||||
def test_extract_tenant_id_with_dict_user(self):
|
||||
"""Test extracting tenant_id with dict user raises ValueError."""
|
||||
dict_user = {"id": "123", "tenant_id": "456"}
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid user type.*Expected Account or EndUser"):
|
||||
extract_tenant_id(dict_user)
|
||||
@ -0,0 +1,87 @@
|
||||
import {
|
||||
isValidElement,
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import type { AnyFieldApi } from '@tanstack/react-form'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
|
||||
export type BaseFieldProps = {
|
||||
fieldClassName?: string
|
||||
labelClassName?: string
|
||||
inputContainerClassName?: string
|
||||
inputClassName?: string
|
||||
formSchema: FormSchema
|
||||
field: AnyFieldApi
|
||||
disabled?: boolean
|
||||
}
|
||||
const BaseField = ({
|
||||
fieldClassName,
|
||||
labelClassName,
|
||||
inputContainerClassName,
|
||||
inputClassName,
|
||||
formSchema,
|
||||
field,
|
||||
disabled,
|
||||
}: BaseFieldProps) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const {
|
||||
label,
|
||||
} = formSchema
|
||||
|
||||
const memorizedLabel = useMemo(() => {
|
||||
if (isValidElement(label))
|
||||
return label
|
||||
|
||||
if (typeof label === 'string')
|
||||
return label
|
||||
|
||||
if (typeof label === 'object' && label !== null)
|
||||
return renderI18nObject(label as Record<string, string>)
|
||||
}, [label, renderI18nObject])
|
||||
const value = useStore(field.form.store, s => s.values[field.name])
|
||||
|
||||
return (
|
||||
<div className={cn(fieldClassName)}>
|
||||
<div className={cn(labelClassName)}>
|
||||
{memorizedLabel}
|
||||
</div>
|
||||
<div className={cn(inputContainerClassName)}>
|
||||
{
|
||||
formSchema.type === FormTypeEnum.textInput && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
className={cn(inputClassName)}
|
||||
value={value}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.secretInput && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='password'
|
||||
className={cn(inputClassName)}
|
||||
value={value}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BaseField)
|
||||
@ -0,0 +1,96 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
} from 'react'
|
||||
import type {
|
||||
AnyFieldApi,
|
||||
} from '@tanstack/react-form'
|
||||
import { useForm } from '@tanstack/react-form'
|
||||
import type {
|
||||
FormRef,
|
||||
FormSchema,
|
||||
} from '@/app/components/base/form/types'
|
||||
import {
|
||||
BaseField,
|
||||
} from '.'
|
||||
import type {
|
||||
BaseFieldProps,
|
||||
} from '.'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
export type BaseFormProps = {
|
||||
formSchemas?: FormSchema[]
|
||||
defaultValues?: Record<string, any>
|
||||
formClassName?: string
|
||||
ref?: FormRef
|
||||
disabled?: boolean
|
||||
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
|
||||
|
||||
const BaseForm = ({
|
||||
formSchemas,
|
||||
defaultValues,
|
||||
formClassName,
|
||||
fieldClassName,
|
||||
labelClassName,
|
||||
inputContainerClassName,
|
||||
inputClassName,
|
||||
ref,
|
||||
disabled,
|
||||
}: BaseFormProps) => {
|
||||
const form = useForm({
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getForm() {
|
||||
return form
|
||||
},
|
||||
}
|
||||
}, [form])
|
||||
|
||||
const renderField = useCallback((field: AnyFieldApi) => {
|
||||
const formSchema = formSchemas?.find(schema => schema.name === field.name)
|
||||
|
||||
if (formSchema) {
|
||||
return (
|
||||
<BaseField
|
||||
field={field}
|
||||
formSchema={formSchema}
|
||||
fieldClassName={fieldClassName}
|
||||
labelClassName={labelClassName}
|
||||
inputContainerClassName={inputContainerClassName}
|
||||
inputClassName={inputClassName}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled])
|
||||
|
||||
if (!formSchemas?.length)
|
||||
return null
|
||||
|
||||
return (
|
||||
<form
|
||||
className={cn(formClassName)}
|
||||
>
|
||||
{
|
||||
formSchemas.map((formSchema) => {
|
||||
return (
|
||||
<form.Field
|
||||
key={formSchema.name}
|
||||
name={formSchema.name}
|
||||
>
|
||||
{renderField}
|
||||
</form.Field>
|
||||
)
|
||||
})
|
||||
}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(BaseForm)
|
||||
@ -0,0 +1,2 @@
|
||||
export { default as BaseForm, type BaseFormProps } from './base-form'
|
||||
export { default as BaseField, type BaseFieldProps } from './base-field'
|
||||
@ -0,0 +1,21 @@
|
||||
import { memo } from 'react'
|
||||
import { BaseForm } from '../../components/base'
|
||||
import type { BaseFormProps } from '../../components/base'
|
||||
|
||||
const AuthForm = ({
|
||||
formSchemas = [],
|
||||
defaultValues,
|
||||
ref,
|
||||
}: BaseFormProps) => {
|
||||
return (
|
||||
<BaseForm
|
||||
ref={ref}
|
||||
formSchemas={formSchemas}
|
||||
defaultValues={defaultValues}
|
||||
formClassName='space-y-4'
|
||||
labelClassName='h-6 flex items-center mb-1 system-sm-medium text-text-secondary'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AuthForm)
|
||||
@ -0,0 +1,51 @@
|
||||
import type {
|
||||
ForwardedRef,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import type { AnyFormApi } from '@tanstack/react-form'
|
||||
|
||||
export type TypeWithI18N<T = string> = {
|
||||
en_US: T
|
||||
zh_Hans: T
|
||||
[key: string]: T
|
||||
}
|
||||
|
||||
export type FormShowOnObject = {
|
||||
variable: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export enum FormTypeEnum {
|
||||
textInput = 'text-input',
|
||||
textNumber = 'number-input',
|
||||
secretInput = 'secret-input',
|
||||
select = 'select',
|
||||
radio = 'radio',
|
||||
boolean = 'boolean',
|
||||
files = 'files',
|
||||
file = 'file',
|
||||
modelSelector = 'model-selector',
|
||||
toolSelector = 'tool-selector',
|
||||
multiToolSelector = 'array[tools]',
|
||||
appSelector = 'app-selector',
|
||||
dynamicSelect = 'dynamic-select',
|
||||
}
|
||||
|
||||
export type FormSchema = {
|
||||
type: FormTypeEnum
|
||||
name: string
|
||||
label: string | ReactNode | TypeWithI18N
|
||||
required: boolean
|
||||
default?: any
|
||||
tooltip?: string | TypeWithI18N
|
||||
show_on?: FormShowOnObject[]
|
||||
url?: string
|
||||
scope?: string
|
||||
}
|
||||
|
||||
export type FormValues = Record<string, any>
|
||||
|
||||
export type FromRefObject = {
|
||||
getForm: () => AnyFormApi
|
||||
}
|
||||
export type FormRef = ForwardedRef<FromRefObject>
|
||||
@ -1,3 +1,7 @@
|
||||
import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config'
|
||||
|
||||
export const isValidUrl = (url: string): boolean => {
|
||||
return ['http:', 'https:', '//', 'mailto:'].some(prefix => url.startsWith(prefix))
|
||||
const validPrefixes = ['http:', 'https:', '//', 'mailto:']
|
||||
if (ALLOW_UNSAFE_DATA_SCHEME) validPrefixes.push('data:')
|
||||
return validPrefixes.some(prefix => url.startsWith(prefix))
|
||||
}
|
||||
|
||||
@ -0,0 +1,123 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { ButtonProps } from '@/app/components/base/button'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type ModalProps = {
|
||||
onClose?: () => void
|
||||
size?: 'sm' | 'md'
|
||||
title: string
|
||||
subTitle?: string
|
||||
children?: React.ReactNode
|
||||
confirmButtonText?: string
|
||||
onConfirm?: () => void
|
||||
cancelButtonText?: string
|
||||
onCancel?: () => void
|
||||
showExtraButton?: boolean
|
||||
extraButtonText?: string
|
||||
extraButtonVariant?: ButtonProps['variant']
|
||||
onExtraButtonClick?: () => void
|
||||
footerSlot?: React.ReactNode
|
||||
bottomSlot?: React.ReactNode
|
||||
disabled?: boolean
|
||||
}
|
||||
const Modal = ({
|
||||
onClose,
|
||||
size = 'sm',
|
||||
title,
|
||||
subTitle,
|
||||
children,
|
||||
confirmButtonText,
|
||||
onConfirm,
|
||||
cancelButtonText,
|
||||
onCancel,
|
||||
showExtraButton,
|
||||
extraButtonVariant = 'warning',
|
||||
extraButtonText,
|
||||
onExtraButtonClick,
|
||||
footerSlot,
|
||||
bottomSlot,
|
||||
disabled,
|
||||
}: ModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<PortalToFollowElem open>
|
||||
<PortalToFollowElemContent
|
||||
className='z-[9998] flex h-full w-full items-center justify-center bg-background-overlay'
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'max-h-[80%] w-[480px] overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xs',
|
||||
size === 'sm' && 'w-[480px',
|
||||
size === 'md' && 'w-[640px]',
|
||||
)}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className='title-2xl-semi-bold relative p-6 pb-3 pr-14 text-text-primary'>
|
||||
{title}
|
||||
{
|
||||
subTitle && (
|
||||
<div className='system-xs-regular mt-1 text-text-tertiary'>
|
||||
{subTitle}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
className='absolute right-5 top-5 flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg'
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
children && (
|
||||
<div className='px-6 py-3'>{children}</div>
|
||||
)
|
||||
}
|
||||
<div className='flex items-center justify-end p-6 pt-5'>
|
||||
{footerSlot}
|
||||
{
|
||||
showExtraButton && (
|
||||
<>
|
||||
<Button
|
||||
variant={extraButtonVariant}
|
||||
onClick={onExtraButtonClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{extraButtonText || t('common.operation.remove')}
|
||||
</Button>
|
||||
<div className='mx-3 h-4 w-[1px] bg-divider-regular'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
disabled={disabled}
|
||||
>
|
||||
{cancelButtonText || t('common.operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className='ml-2'
|
||||
variant='primary'
|
||||
onClick={onConfirm}
|
||||
disabled={disabled}
|
||||
>
|
||||
{confirmButtonText || t('common.operation.save')}
|
||||
</Button>
|
||||
</div>
|
||||
{bottomSlot}
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Modal)
|
||||
@ -0,0 +1,46 @@
|
||||
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'
|
||||
|
||||
export type AddApiKeyButtonProps = {
|
||||
provider?: string
|
||||
buttonVariant?: ButtonProps['variant']
|
||||
buttonText?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
const AddApiKeyButton = ({
|
||||
provider = '',
|
||||
buttonVariant = 'secondary-accent',
|
||||
buttonText = 'use api key',
|
||||
disabled,
|
||||
}: AddApiKeyButtonProps) => {
|
||||
const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
className='grow'
|
||||
variant={buttonVariant}
|
||||
onClick={() => setIsApiKeyModalOpen(true)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
{
|
||||
isApiKeyModalOpen && (
|
||||
<ApiKeyModal
|
||||
provider={provider}
|
||||
onClose={() => setIsApiKeyModalOpen(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddApiKeyButton)
|
||||
@ -0,0 +1,72 @@
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { RiEqualizer2Line } 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'
|
||||
|
||||
export type AddOAuthButtonProps = {
|
||||
buttonVariant?: ButtonProps['variant']
|
||||
buttonText?: string
|
||||
className?: string
|
||||
buttonLeftClassName?: string
|
||||
buttonRightClassName?: string
|
||||
dividerClassName?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
const AddOAuthButton = ({
|
||||
buttonVariant = 'primary',
|
||||
buttonText = 'use oauth',
|
||||
className,
|
||||
buttonLeftClassName,
|
||||
buttonRightClassName,
|
||||
dividerClassName,
|
||||
disabled,
|
||||
}: AddOAuthButtonProps) => {
|
||||
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant={buttonVariant}
|
||||
className={cn(
|
||||
'grow px-0 py-0 hover:bg-components-button-primary-bg',
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className={cn(
|
||||
'flex h-full grow items-center justify-center rounded-l-lg hover:bg-components-button-primary-bg-hover',
|
||||
buttonLeftClassName,
|
||||
)}>
|
||||
{buttonText}
|
||||
</div>
|
||||
<div className={cn(
|
||||
'h-4 w-[1px] 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={() => setIsOAuthSettingsOpen(true)}
|
||||
>
|
||||
<RiEqualizer2Line className='h-4 w-4' />
|
||||
</div>
|
||||
</Button>
|
||||
{
|
||||
isOAuthSettingsOpen && (
|
||||
<OAuthClientSettings
|
||||
onClose={() => setIsOAuthSettingsOpen(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddOAuthButton)
|
||||
@ -0,0 +1,145 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
import {
|
||||
useAddPluginToolCredential,
|
||||
useGetPluginToolCredentialSchema,
|
||||
useInvalidPluginToolCredentialInfo,
|
||||
useUpdatePluginToolCredential,
|
||||
} from '@/service/use-plugins-auth'
|
||||
import { CredentialTypeEnum } from '../types'
|
||||
import { transformFormSchemasSecretInput } from '../utils'
|
||||
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
|
||||
import type { FromRefObject } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
|
||||
export type ApiKeyModalProps = {
|
||||
provider: string
|
||||
onClose?: () => void
|
||||
editValues?: Record<string, any>
|
||||
onRemove?: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
const ApiKeyModal = ({
|
||||
provider,
|
||||
onClose,
|
||||
editValues,
|
||||
onRemove,
|
||||
disabled,
|
||||
}: ApiKeyModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const { data = [] } = useGetPluginToolCredentialSchema(provider, CredentialTypeEnum.API_KEY)
|
||||
const formSchemas = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
type: FormTypeEnum.textInput,
|
||||
name: '__name__',
|
||||
label: 'Authorization name',
|
||||
required: false,
|
||||
},
|
||||
...data,
|
||||
]
|
||||
}, [data])
|
||||
const { mutateAsync: addPluginToolCredential } = useAddPluginToolCredential(provider)
|
||||
const { mutateAsync: updatePluginToolCredential } = useUpdatePluginToolCredential(provider)
|
||||
const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider)
|
||||
const formRef = useRef<FromRefObject>(null)
|
||||
const handleConfirm = useCallback(async () => {
|
||||
const form = formRef.current?.getForm()
|
||||
const store = form?.store
|
||||
const {
|
||||
__name__,
|
||||
__credential_id__,
|
||||
...values
|
||||
} = store?.state.values
|
||||
const isPristineSecretInputNames: string[] = []
|
||||
formSchemas.forEach((schema) => {
|
||||
if (schema.type === FormTypeEnum.secretInput) {
|
||||
const fieldMeta = form?.getFieldMeta(schema.name)
|
||||
if (fieldMeta?.isPristine)
|
||||
isPristineSecretInputNames.push(schema.name)
|
||||
}
|
||||
})
|
||||
|
||||
const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values)
|
||||
|
||||
if (editValues) {
|
||||
await updatePluginToolCredential({
|
||||
credentials: transformedValues,
|
||||
credential_id: __credential_id__,
|
||||
type: CredentialTypeEnum.API_KEY,
|
||||
name: __name__ || '',
|
||||
})
|
||||
}
|
||||
else {
|
||||
await addPluginToolCredential({
|
||||
credentials: transformedValues,
|
||||
type: CredentialTypeEnum.API_KEY,
|
||||
name: __name__ || '',
|
||||
})
|
||||
}
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
|
||||
onClose?.()
|
||||
invalidatePluginToolCredentialInfo()
|
||||
}, [addPluginToolCredential, onClose, invalidatePluginToolCredentialInfo, updatePluginToolCredential, notify, t, editValues, formSchemas])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size='md'
|
||||
title='API Key Authorization Configuration'
|
||||
subTitle='After configuring credentials, all members within the workspace can use this tool when orchestrating applications.'
|
||||
onClose={onClose}
|
||||
onCancel={onClose}
|
||||
footerSlot={
|
||||
<a
|
||||
className='system-xs-regular flex h-8 grow items-center text-text-accent'
|
||||
href=''
|
||||
target='_blank'
|
||||
>
|
||||
Get your API Key from OpenAI
|
||||
<RiExternalLinkLine className='ml-1 h-3 w-3' />
|
||||
</a>
|
||||
}
|
||||
bottomSlot={
|
||||
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
|
||||
<Lock01 className='mr-1 h-3 w-3 text-text-tertiary' />
|
||||
{t('common.modelProvider.encrypted.front')}
|
||||
<a
|
||||
className='mx-1 text-text-accent'
|
||||
target='_blank' rel='noopener noreferrer'
|
||||
href='https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html'
|
||||
>
|
||||
PKCS1_OAEP
|
||||
</a>
|
||||
{t('common.modelProvider.encrypted.back')}
|
||||
</div>
|
||||
}
|
||||
onConfirm={handleConfirm}
|
||||
showExtraButton={!!editValues}
|
||||
onExtraButtonClick={onRemove}
|
||||
disabled={disabled}
|
||||
>
|
||||
<AuthForm
|
||||
ref={formRef}
|
||||
formSchemas={formSchemas}
|
||||
defaultValues={editValues}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ApiKeyModal)
|
||||
@ -0,0 +1,91 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
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'
|
||||
|
||||
type AuthorizeProps = {
|
||||
provider?: string
|
||||
theme?: 'primary' | 'secondary'
|
||||
showDivider?: boolean
|
||||
canOAuth?: boolean
|
||||
canApiKey?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
const Authorize = ({
|
||||
provider = '',
|
||||
theme = 'primary',
|
||||
showDivider = true,
|
||||
canOAuth,
|
||||
canApiKey,
|
||||
disabled,
|
||||
}: AuthorizeProps) => {
|
||||
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
|
||||
if (theme === 'secondary') {
|
||||
return {
|
||||
buttonText: !canApiKey ? 'Add OAuth Authorization' : 'Add OAuth',
|
||||
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',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
buttonText: !canApiKey ? 'Use OAuth Authorization' : 'Use OAuth',
|
||||
}
|
||||
}, [canApiKey, theme])
|
||||
|
||||
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
|
||||
if (theme === 'secondary') {
|
||||
return {
|
||||
provider,
|
||||
buttonVariant: 'secondary',
|
||||
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Add API Key',
|
||||
}
|
||||
}
|
||||
return {
|
||||
provider,
|
||||
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Use API Key',
|
||||
buttonVariant: !canOAuth ? 'primary' : 'secondary-accent',
|
||||
}
|
||||
}, [canOAuth, theme, provider])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex items-center space-x-1.5'>
|
||||
{
|
||||
canOAuth && (
|
||||
<AddOAuthButton
|
||||
{...oAuthButtonProps}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
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 && (
|
||||
<AddApiKeyButton
|
||||
{...apiKeyButtonProps}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Authorize)
|
||||
@ -0,0 +1,26 @@
|
||||
import { memo } from 'react'
|
||||
import Modal from '@/app/components/base/modal/modal'
|
||||
|
||||
type OAuthClientSettingsProps = {
|
||||
onClose?: () => void
|
||||
}
|
||||
const OAuthClientSettings = ({
|
||||
onClose,
|
||||
}: OAuthClientSettingsProps) => {
|
||||
return (
|
||||
<Modal
|
||||
title='Oauth client settings'
|
||||
confirmButtonText='Save & Authorize'
|
||||
cancelButtonText='Save only'
|
||||
extraButtonText='Cancel'
|
||||
showExtraButton
|
||||
extraButtonVariant='secondary'
|
||||
onExtraButtonClick={onClose}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div>oauth</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(OAuthClientSettings)
|
||||
@ -0,0 +1,94 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
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 } from './types'
|
||||
import {
|
||||
Authorized,
|
||||
usePluginAuth,
|
||||
} from '.'
|
||||
|
||||
type AuthorizedInNodeProps = {
|
||||
provider: string
|
||||
onAuthorizationItemClick: (id: string) => void
|
||||
credentialId?: string
|
||||
}
|
||||
const AuthorizedInNode = ({
|
||||
provider = '',
|
||||
onAuthorizationItemClick,
|
||||
credentialId,
|
||||
}: AuthorizedInNodeProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const {
|
||||
canApiKey,
|
||||
canOAuth,
|
||||
credentials,
|
||||
disabled,
|
||||
} = usePluginAuth(provider, isOpen)
|
||||
const label = useMemo(() => {
|
||||
if (!credentialId)
|
||||
return 'Workspace default'
|
||||
const credential = credentials.find(c => c.id === credentialId)
|
||||
|
||||
if (!credential)
|
||||
return 'Auth removed'
|
||||
|
||||
return credential.name
|
||||
}, [credentials, credentialId])
|
||||
const renderTrigger = useCallback((open?: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
size='small'
|
||||
className={cn(open && 'bg-components-button-ghost-bg-hover')}
|
||||
>
|
||||
<Indicator className='mr-1.5' />
|
||||
{label}
|
||||
<RiArrowDownSLine className='h-3.5 w-3.5 text-components-button-ghost-text' />
|
||||
</Button>
|
||||
)
|
||||
}, [label])
|
||||
const extraAuthorizationItems: Credential[] = [
|
||||
{
|
||||
id: '__workspace_default__',
|
||||
name: 'Workspace default',
|
||||
provider: '',
|
||||
is_default: false,
|
||||
isWorkspaceDefault: true,
|
||||
},
|
||||
]
|
||||
const handleAuthorizationItemClick = useCallback((id: string) => {
|
||||
onAuthorizationItemClick(id)
|
||||
setIsOpen(false)
|
||||
}, [
|
||||
onAuthorizationItemClick,
|
||||
setIsOpen,
|
||||
])
|
||||
|
||||
return (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AuthorizedInNode)
|
||||
@ -0,0 +1,267 @@
|
||||
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 {
|
||||
useDeletePluginToolCredential,
|
||||
useInvalidPluginToolCredentialInfo,
|
||||
useSetPluginToolDefaultCredential,
|
||||
} from '@/service/use-plugins-auth'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
|
||||
type AuthorizedProps = {
|
||||
provider: string
|
||||
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[]
|
||||
}
|
||||
const Authorized = ({
|
||||
provider,
|
||||
credentials,
|
||||
canOAuth,
|
||||
canApiKey,
|
||||
disabled,
|
||||
renderTrigger,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
offset = 8,
|
||||
placement = 'bottom-start',
|
||||
triggerPopupSameWidth = true,
|
||||
popupClassName,
|
||||
disableSetDefault,
|
||||
onItemClick,
|
||||
extraAuthorizationItems,
|
||||
}: 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: deletePluginToolCredential } = useDeletePluginToolCredential(provider)
|
||||
const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider)
|
||||
const openConfirm = useCallback((credentialId?: string) => {
|
||||
if (credentialId)
|
||||
pendingOperationCredentialId.current = credentialId
|
||||
|
||||
setDeleteCredentialId(pendingOperationCredentialId.current)
|
||||
}, [])
|
||||
const closeConfirm = useCallback(() => {
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
}, [])
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (!pendingOperationCredentialId.current) {
|
||||
setDeleteCredentialId(null)
|
||||
return
|
||||
}
|
||||
|
||||
await deletePluginToolCredential({ credential_id: pendingOperationCredentialId.current })
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
invalidatePluginToolCredentialInfo()
|
||||
setDeleteCredentialId(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
}, [deletePluginToolCredential, invalidatePluginToolCredentialInfo, notify, t])
|
||||
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: setPluginToolDefaultCredential } = useSetPluginToolDefaultCredential(provider)
|
||||
const handleSetDefault = useCallback(async (id: string) => {
|
||||
await setPluginToolDefaultCredential(id)
|
||||
notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
invalidatePluginToolCredentialInfo()
|
||||
}, [setPluginToolDefaultCredential, invalidatePluginToolCredentialInfo, notify, t])
|
||||
|
||||
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' />
|
||||
{credentials.length} Authorizations
|
||||
<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,
|
||||
)}>
|
||||
{
|
||||
!!extraAuthorizationItems?.length && (
|
||||
<div className='p-1'>
|
||||
{
|
||||
extraAuthorizationItems.map(credential => (
|
||||
<Item
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
disabled={disabled}
|
||||
onItemClick={onItemClick}
|
||||
disableRename
|
||||
disableEdit
|
||||
disableDelete
|
||||
disableSetDefault
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='py-1'>
|
||||
{
|
||||
!!oAuthCredentials.length && (
|
||||
<div className='p-1'>
|
||||
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
|
||||
OAuth
|
||||
</div>
|
||||
{
|
||||
oAuthCredentials.map(credential => (
|
||||
<Item
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!apiKeyCredentials.length && (
|
||||
<div className='p-1'>
|
||||
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
|
||||
API Keys
|
||||
</div>
|
||||
{
|
||||
apiKeyCredentials.map(credential => (
|
||||
<Item
|
||||
key={credential.id}
|
||||
credential={credential}
|
||||
disabled={disabled}
|
||||
onDelete={openConfirm}
|
||||
onEdit={handleEdit}
|
||||
onSetDefault={handleSetDefault}
|
||||
disableSetDefault={disableSetDefault}
|
||||
onItemClick={onItemClick}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='h-[1px] bg-divider-subtle'></div>
|
||||
<div className='p-2'>
|
||||
<Authorize
|
||||
provider={provider}
|
||||
theme='secondary'
|
||||
showDivider={false}
|
||||
canOAuth={canOAuth}
|
||||
canApiKey={canApiKey}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
{
|
||||
deleteCredentialId && (
|
||||
<Confirm
|
||||
isShow
|
||||
title='Are you sure?'
|
||||
content='content'
|
||||
onCancel={closeConfirm}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!editValues && (
|
||||
<ApiKeyModal
|
||||
provider={provider}
|
||||
editValues={editValues}
|
||||
onClose={() => {
|
||||
setEditValues(null)
|
||||
pendingOperationCredentialId.current = null
|
||||
}}
|
||||
onRemove={handleRemove}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Authorized)
|
||||
@ -0,0 +1,139 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import {
|
||||
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 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
|
||||
disableRename?: boolean
|
||||
disableEdit?: boolean
|
||||
disableDelete?: boolean
|
||||
disableSetDefault?: boolean
|
||||
onItemClick?: (id: string) => void
|
||||
}
|
||||
const Item = ({
|
||||
credential,
|
||||
disabled,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onSetDefault,
|
||||
disableRename,
|
||||
disableEdit,
|
||||
disableDelete,
|
||||
disableSetDefault,
|
||||
onItemClick,
|
||||
}: ItemProps) => {
|
||||
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
|
||||
const showAction = useMemo(() => {
|
||||
return !(disableRename && disableEdit && disableDelete && disableSetDefault)
|
||||
}, [disableRename, disableEdit, disableDelete, disableSetDefault])
|
||||
|
||||
return (
|
||||
<div
|
||||
key={credential.id}
|
||||
className='group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover'
|
||||
onClick={() => onItemClick?.(credential.id)}
|
||||
>
|
||||
<div className='flex w-0 grow items-center space-x-1.5 pl-2'>
|
||||
<Indicator className='mr-1.5 shrink-0' />
|
||||
<div
|
||||
className='system-md-regular truncate text-text-secondary'
|
||||
title={credential.name}
|
||||
>
|
||||
{credential.name}
|
||||
</div>
|
||||
{
|
||||
credential.is_default && (
|
||||
<Badge>
|
||||
Default
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
showAction && (
|
||||
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
|
||||
{
|
||||
!credential.is_default && !disableSetDefault && (
|
||||
<Button
|
||||
size='small'
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onSetDefault?.(credential.id)
|
||||
}}
|
||||
>
|
||||
Set as default
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
{
|
||||
isOAuth && !disableRename && (
|
||||
<Tooltip popupContent='rename'>
|
||||
<ActionButton>
|
||||
<RiEditLine className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isOAuth && !disableEdit && (
|
||||
<Tooltip popupContent='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 && (
|
||||
<Tooltip popupContent='delete'>
|
||||
<ActionButton
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete?.(credential.id)
|
||||
}}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Item)
|
||||
@ -0,0 +1,20 @@
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth'
|
||||
import { CredentialTypeEnum } from './types'
|
||||
|
||||
export const usePluginAuth = (provider: string, enable?: boolean) => {
|
||||
const { data } = useGetPluginToolCredentialInfo(enable ? provider : '')
|
||||
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)
|
||||
|
||||
return {
|
||||
isAuthorized,
|
||||
canOAuth,
|
||||
canApiKey,
|
||||
credentials: data?.credentials || [],
|
||||
provider,
|
||||
disabled: !isCurrentWorkspaceManager,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export { default as PluginAuth } from './plugin-auth'
|
||||
export { default as Authorized } from './authorized'
|
||||
export { default as AuthorizedInNode } from './authorized-in-node'
|
||||
export { usePluginAuth } from './hooks'
|
||||
@ -0,0 +1,52 @@
|
||||
import { memo } from 'react'
|
||||
import Authorize from './authorize'
|
||||
import Authorized from './authorized'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth'
|
||||
import { CredentialTypeEnum } from './types'
|
||||
|
||||
type PluginAuthProps = {
|
||||
provider?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
const PluginAuth = ({
|
||||
provider = '',
|
||||
children,
|
||||
}: PluginAuthProps) => {
|
||||
const { data } = useGetPluginToolCredentialInfo(provider)
|
||||
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)
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
!isAuthorized && (
|
||||
<Authorize
|
||||
provider={provider}
|
||||
canOAuth={canOAuth}
|
||||
canApiKey={canApiKey}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isAuthorized && !children && (
|
||||
<Authorized
|
||||
provider={provider}
|
||||
credentials={data?.credentials}
|
||||
canOAuth={canOAuth}
|
||||
canApiKey={canApiKey}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isAuthorized && children
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PluginAuth)
|
||||
@ -0,0 +1,14 @@
|
||||
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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from '@tanstack/react-query'
|
||||
import { get, post } from './base'
|
||||
import { useInvalid } from './use-base'
|
||||
import type {
|
||||
Credential,
|
||||
CredentialTypeEnum,
|
||||
} from '@/app/components/plugins/plugin-auth/types'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
|
||||
const NAME_SPACE = 'plugins-auth'
|
||||
|
||||
export const useGetPluginToolCredentialInfo = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useQuery({
|
||||
enabled: !!provider,
|
||||
queryKey: [NAME_SPACE, 'credential-info', provider],
|
||||
queryFn: () => get<{
|
||||
supported_credential_types: string[]
|
||||
credentials: Credential[]
|
||||
is_oauth_custom_client_enabled: boolean
|
||||
}>(`/workspaces/current/tool-provider/builtin/${provider}/credential/info`),
|
||||
staleTime: 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useInvalidPluginToolCredentialInfo = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useInvalid([NAME_SPACE, 'credential-info', provider])
|
||||
}
|
||||
|
||||
export const useSetPluginToolDefaultCredential = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
return post(`/workspaces/current/tool-provider/builtin/${provider}/default-credential`, { body: { id } })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetPluginToolCredentialList = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'credential-list', provider],
|
||||
queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/credentials`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useAddPluginToolCredential = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (params: {
|
||||
credentials: Record<string, any>
|
||||
type: CredentialTypeEnum
|
||||
name?: string
|
||||
}) => {
|
||||
return post(`/workspaces/current/tool-provider/builtin/${provider}/add`, { body: params })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdatePluginToolCredential = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (params: {
|
||||
credential_id: string
|
||||
credentials: Record<string, any>
|
||||
type: CredentialTypeEnum
|
||||
name?: string
|
||||
}) => {
|
||||
return post(`/workspaces/current/tool-provider/builtin/${provider}/update`, { body: params })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useDeletePluginToolCredential = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (params: { credential_id: string }) => {
|
||||
return post(`/workspaces/current/tool-provider/builtin/${provider}/delete`, { body: params })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetPluginToolCredentialSchema = (
|
||||
provider: string,
|
||||
credential_type: CredentialTypeEnum,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'credential-schema', provider, credential_type],
|
||||
queryFn: () => get<FormSchema[]>(`/workspaces/current/tool-provider/builtin/${provider}/credential/schema/${credential_type}`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetPluginToolOAuthUrl = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'oauth-url', provider],
|
||||
queryFn: () => get(`oauth/plugin/${provider}/tool/authorization-url`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetPluginToolOAuthClientSchema = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'oauth-client-schema', provider],
|
||||
queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`),
|
||||
})
|
||||
}
|
||||
|
||||
export const useSetPluginToolOAuthCustomClient = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (params) => {
|
||||
return post(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, { body: params })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetPluginToolOAuthCustomClientSchema = (
|
||||
provider: string,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'oauth-custom-client-schema', provider],
|
||||
queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`),
|
||||
})
|
||||
}
|
||||
Loading…
Reference in New Issue