tool oauth

pull/22338/head^2
zxhlyh 7 months ago
parent c53d5c105b
commit 0f1be60daa

@ -0,0 +1,76 @@
import {
isValidElement,
memo,
useMemo,
} from 'react'
import type { AnyFieldApi } 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
}
const BaseField = ({
fieldClassName,
labelClassName,
inputContainerClassName,
inputClassName,
formSchema,
field,
}: BaseFieldProps) => {
const renderI18nObject = useRenderI18nObject()
const {
label,
} = formSchema
const memorizedLabel = useMemo(() => {
if (isValidElement(label))
return label
if (typeof label === 'object' && label !== null)
return renderI18nObject(label as Record<string, string>)
}, [label, renderI18nObject])
return (
<div className={cn(fieldClassName)}>
<div className={cn(labelClassName)}>
{memorizedLabel}
</div>
<div className={cn(inputContainerClassName)}>
{
formSchema.type === FormTypeEnum.textInput && (
<Input
className={cn(inputClassName)}
id={field.name}
value={field.state.value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)
}
{
formSchema.type === FormTypeEnum.secretInput && (
<Input
type='password'
className={cn(inputClassName)}
id={field.name}
value={field.state.value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
)
}
</div>
</div>
)
}
export default memo(BaseField)

@ -0,0 +1,91 @@
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
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
const BaseForm = ({
formSchemas,
defaultValues,
formClassName,
fieldClassName,
labelClassName,
inputContainerClassName,
inputClassName,
ref,
}: BaseFormProps) => {
const form = useForm({
defaultValues,
})
useImperativeHandle(ref, () => {
return {
getFormStore() {
return form.store
},
}
}, [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}
/>
)
}
return null
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName])
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 = {
getFormStore: () => AnyFormApi['store']
}
export type FormRef = ForwardedRef<FromRefObject>

@ -6,13 +6,17 @@ import Button from '@/app/components/base/button'
import type { ButtonProps } from '@/app/components/base/button'
import ApiKeyModal from './api-key-modal'
type AddApiKeyButtonProps = {
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)
@ -22,12 +26,14 @@ const AddApiKeyButton = ({
className='grow'
variant={buttonVariant}
onClick={() => setIsApiKeyModalOpen(true)}
disabled={disabled}
>
{buttonText}
</Button>
{
isApiKeyModalOpen && (
<ApiKeyModal
provider={provider}
onClose={() => setIsApiKeyModalOpen(false)}
/>
)

@ -8,13 +8,14 @@ import type { ButtonProps } from '@/app/components/base/button'
import OAuthClientSettings from './oauth-client-settings'
import cn from '@/utils/classnames'
type AddOAuthButtonProps = {
export type AddOAuthButtonProps = {
buttonVariant?: ButtonProps['variant']
buttonText?: string
className?: string
buttonLeftClassName?: string
buttonRightClassName?: string
dividerClassName?: string
disabled?: boolean
}
const AddOAuthButton = ({
buttonVariant = 'primary',
@ -23,6 +24,7 @@ const AddOAuthButton = ({
buttonLeftClassName,
buttonRightClassName,
dividerClassName,
disabled,
}: AddOAuthButtonProps) => {
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
@ -34,6 +36,7 @@ const AddOAuthButton = ({
'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',

@ -0,0 +1,109 @@
import {
memo,
useCallback,
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 AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type { FromRefObject } 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
}
const ApiKeyModal = ({
provider,
onClose,
editValues,
onRemove,
}: ApiKeyModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { data } = useGetPluginToolCredentialSchema(provider, CredentialTypeEnum.API_KEY)
const { mutateAsync: addPluginToolCredential } = useAddPluginToolCredential(provider)
const { mutateAsync: updatePluginToolCredential } = useUpdatePluginToolCredential(provider)
const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider)
const formRef = useRef<FromRefObject>(null)
const handleConfirm = useCallback(async () => {
const store = formRef.current?.getFormStore()
const values = store?.state.values
if (editValues) {
await updatePluginToolCredential({
credentials: values,
type: CredentialTypeEnum.API_KEY,
})
}
else {
await addPluginToolCredential({
credentials: values,
type: CredentialTypeEnum.API_KEY,
})
}
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
invalidatePluginToolCredentialInfo()
}, [addPluginToolCredential, onClose, invalidatePluginToolCredentialInfo, updatePluginToolCredential, notify, t, editValues])
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}
>
<AuthForm
ref={formRef}
formSchemas={data}
defaultValues={editValues}
/>
</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' : '',
}
}, [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' : '',
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,205 @@
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 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
}
const Authorized = ({
provider,
credentials,
canOAuth,
canApiKey,
disabled,
}: AuthorizedProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [isOpen, setIsOpen] = useState(false)
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={isOpen}
onOpenChange={setIsOpen}
placement='bottom-start'
offset={8}
triggerPopupSameWidth
>
<PortalToFollowElemTrigger
onClick={() => setIsOpen(!isOpen)}
asChild
>
<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='max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<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}
/>
))
}
</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}
/>
)
}
</>
)
}
export default memo(Authorized)

@ -0,0 +1,96 @@
import {
memo,
} 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
}
const Item = ({
credential,
disabled,
onDelete,
onEdit,
onSetDefault,
}: ItemProps) => {
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
return (
<div
key={credential.id}
className='group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover'
>
<div className='flex grow items-center space-x-1.5 pl-2'>
<Indicator className='mr-1.5' />
<div
className='system-md-regular truncate text-text-secondary'
title={credential.name}
>
{credential.name}
</div>
{
credential.is_default && (
<Badge>
Default
</Badge>
)
}
</div>
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
<Button
size='small'
disabled={disabled}
onClick={() => onSetDefault?.(credential.id)}
>
Set as default
</Button>
{
isOAuth && (
<Tooltip popupContent='rename'>
<ActionButton>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
{
!isOAuth && (
<Tooltip popupContent='edit'>
<ActionButton
disabled={disabled}
onClick={() => onEdit?.(credential.id, credential.credentials)}
>
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
)
}
<Tooltip popupContent='delete'>
<ActionButton
disabled={disabled}
onClick={() => onDelete?.(credential.id)}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
</div>
</div>
)
}
export default memo(Item)

@ -0,0 +1 @@
export { default as PluginAuth } from './plugin-auth'

@ -0,0 +1,47 @@
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
}
const PluginAuth = ({
provider = '',
}: 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 && (
<Authorized
provider={provider}
credentials={data?.credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={!isCurrentWorkspaceManager}
/>
)
}
</>
)
}
export default memo(PluginAuth)

@ -0,0 +1,13 @@
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>
}

@ -1,17 +1,9 @@
import React, { useMemo, useState } from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import Button from '@/app/components/base/button'
import Toast from '@/app/components/base/toast'
import Indicator from '@/app/components/header/indicator'
import ToolItem from '@/app/components/tools/provider/tool-item'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import {
useAllToolProviders,
useBuiltinTools,
useInvalidateAllToolProviders,
useRemoveProviderCredentials,
useUpdateProviderCredentials,
} from '@/service/use-tools'
import type { PluginDetail } from '@/app/components/plugins/types'
@ -23,35 +15,14 @@ const ActionList = ({
detail,
}: Props) => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const providerBriefInfo = detail.declaration.tool.identity
const providerKey = `${detail.plugin_id}/${providerBriefInfo.name}`
const { data: collectionList = [] } = useAllToolProviders()
const invalidateAllToolProviders = useInvalidateAllToolProviders()
const provider = useMemo(() => {
return collectionList.find(collection => collection.name === providerKey)
}, [collectionList, providerKey])
const { data } = useBuiltinTools(providerKey)
const [showSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => {
invalidateAllToolProviders()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setShowSettingAuth(false)
}
const { mutate: updatePermission, isPending } = useUpdateProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
const { mutate: removePermission } = useRemoveProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
if (!data || !provider)
return null
@ -60,26 +31,7 @@ const ActionList = ({
<div className='mb-1 py-1'>
<div className='system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary'>
{t('plugin.detailPanel.actionNum', { num: data.length, action: data.length > 1 ? 'actions' : 'action' })}
{provider.is_team_authorization && provider.allow_delete && (
<Button
variant='secondary'
size='small'
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>
<Indicator className='mr-2' color={'green'} />
{t('tools.auth.authorized')}
</Button>
)}
</div>
{!provider.is_team_authorization && provider.allow_delete && (
<Button
variant='primary'
className='w-full'
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>{t('workflow.nodes.tool.authorize')}</Button>
)}
</div>
<div className='flex flex-col gap-2'>
{data.map(tool => (
@ -93,18 +45,6 @@ const ActionList = ({
/>
))}
</div>
{showSettingAuth && (
<ConfigCredential
collection={provider}
onCancel={() => setShowSettingAuth(false)}
onSaved={async value => updatePermission({
providerName: provider.name,
credentials: value,
})}
onRemove={async () => removePermission(provider.name)}
isSaving={isPending}
/>
)}
</div>
)
}

@ -36,6 +36,8 @@ import { useInvalidateAllToolProviders } from '@/service/use-tools'
import { API_PREFIX } from '@/config'
import cn from '@/utils/classnames'
import { getMarketplaceUrl } from '@/utils/var'
import { PluginAuth } from '@/app/components/plugins/plugin-auth'
import { useAllToolProviders } from '@/service/use-tools'
const i18nPrefix = 'plugin.action'
@ -68,7 +70,14 @@ const DetailHeader = ({
meta,
plugin_id,
} = detail
const { author, category, name, label, description, icon, verified } = detail.declaration
const { author, category, name, label, description, icon, verified, tool } = detail.declaration
const isTool = category === PluginType.tool
const providerBriefInfo = tool?.identity
const providerKey = `${plugin_id}/${providerBriefInfo?.name}`
const { data: collectionList = [] } = useAllToolProviders(isTool)
const provider = useMemo(() => {
return collectionList.find(collection => collection.name === providerKey)
}, [collectionList, providerKey])
const isFromGitHub = source === PluginSource.github
const isFromMarketplace = source === PluginSource.marketplace
@ -263,6 +272,13 @@ const DetailHeader = ({
</div>
</div>
<Description className='mt-3' text={description[locale]} descriptionLineRows={2}></Description>
{
category === PluginType.tool && (
<PluginAuth
provider={provider?.name}
/>
)
}
{isShowPluginInfo && (
<PluginInfo
repository={isFromGitHub ? meta?.repo : ''}

@ -1,52 +0,0 @@
import { memo } 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'
export type ApiKeyModalProps = {
onClose?: () => void
}
const ApiKeyModal = ({
onClose,
}: ApiKeyModalProps) => {
const { t } = useTranslation()
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>
}
>
<div>oauth</div>
</Modal>
)
}
export default memo(ApiKeyModal)

@ -1,124 +0,0 @@
import {
memo,
useState,
} from 'react'
import {
RiArrowDownSLine,
RiDeleteBinLine,
RiEditLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button'
import cn from '@/utils/classnames'
import AddOauthButton from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
const Authorization = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<PortalToFollowElem
open={isOpen}
onOpenChange={setIsOpen}
placement='bottom-start'
offset={8}
triggerPopupSameWidth
>
<PortalToFollowElemTrigger
onClick={() => setIsOpen(!isOpen)}
asChild
>
<Button
className={cn(
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
)}>
<Indicator className='mr-2' />
4 Authorizations
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<div className='max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg'>
<div className='py-1'>
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
OAuth
</div>
<div className='flex items-center rounded-lg p-1 hover:bg-state-base-hover'>
<div className='flex grow items-center space-x-1.5 pl-2'>
<Indicator className='mr-1.5' />
<div
className='system-md-regular truncate text-text-secondary'
title='Auth 1'
>
Auth 1
</div>
<Badge>
Default
</Badge>
</div>
<div className='ml-2 flex shrink-0 items-center'>
<ActionButton>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
<ActionButton>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</div>
</div>
</div>
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
API Keys
</div>
<div className='flex items-center rounded-lg p-1 hover:bg-state-base-hover'>
<div className='flex grow items-center space-x-1.5 pl-2'>
<Indicator className='mr-1.5' />
<div
className='system-md-regular truncate text-text-secondary'
title='Production'
>
Production
</div>
</div>
<div className='ml-2 flex shrink-0 items-center space-x-1'>
<Badge>
0.2
</Badge>
<Badge>
ENTERPRISE
</Badge>
</div>
</div>
</div>
</div>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='flex items-center space-x-1.5 p-2'>
<AddOauthButton
buttonVariant='secondary'
buttonText='Add OAuth'
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'
/>
<AddApiKeyButton
buttonVariant='secondary'
buttonText='Add API Key'
/>
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(Authorization)

@ -1,23 +0,0 @@
import {
memo,
} from 'react'
import AddOAuthButton from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
const ToolAuth = () => {
return (
<>
<div className='flex items-center space-x-1.5'>
<AddOAuthButton />
<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>
<AddApiKeyButton />
</div>
</>
)
}
export default memo(ToolAuth)

@ -14,8 +14,9 @@ import Loading from '@/app/components/base/loading'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
import ToolAuth from '@/app/components/tools/tool-auth'
import Authorization from '@/app/components/tools/tool-auth/authorization'
import {
PluginAuth,
} from '@/app/components/plugins/plugin-auth'
const i18nPrefix = 'workflow.nodes.tool'
@ -56,8 +57,9 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
return (
<div className='pt-2'>
<div className='px-4 py-2'>
<ToolAuth />
<Authorization />
<PluginAuth
provider={currCollection?.name}
/>
</div>
{!readOnly && isShowAuthBtn && (
<>

@ -0,0 +1,138 @@
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: {
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`),
})
}

@ -14,10 +14,11 @@ import {
const NAME_SPACE = 'tools'
const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders']
export const useAllToolProviders = () => {
export const useAllToolProviders = (enabled = true) => {
return useQuery<Collection[]>({
queryKey: useAllToolProvidersKey,
queryFn: () => get<Collection[]>('/workspaces/current/tool-providers'),
enabled,
})
}

Loading…
Cancel
Save