tool oauth

pull/22338/head^2
zxhlyh 11 months ago
parent bdf5af7a6f
commit 18699f8671

@ -7,6 +7,7 @@ import type { AnyFieldApi } from '@tanstack/react-form'
import { useStore } from '@tanstack/react-form' import { useStore } from '@tanstack/react-form'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import PureSelect from '@/app/components/base/select/pure'
import type { FormSchema } from '@/app/components/base/form/types' import type { FormSchema } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types'
import { useRenderI18nObject } from '@/hooks/use-i18n' import { useRenderI18nObject } from '@/hooks/use-i18n'
@ -32,6 +33,9 @@ const BaseField = ({
const renderI18nObject = useRenderI18nObject() const renderI18nObject = useRenderI18nObject()
const { const {
label, label,
required,
placeholder,
options,
} = formSchema } = formSchema
const memorizedLabel = useMemo(() => { const memorizedLabel = useMemo(() => {
@ -44,12 +48,32 @@ const BaseField = ({
if (typeof label === 'object' && label !== null) if (typeof label === 'object' && label !== null)
return renderI18nObject(label as Record<string, string>) return renderI18nObject(label as Record<string, string>)
}, [label, renderI18nObject]) }, [label, renderI18nObject])
const memorizedPlaceholder = useMemo(() => {
if (typeof placeholder === 'string')
return placeholder
if (typeof placeholder === 'object' && placeholder !== null)
return renderI18nObject(placeholder as Record<string, string>)
}, [placeholder, renderI18nObject])
const memorizedOptions = useMemo(() => {
return options?.map((option) => {
return {
label: typeof option.label === 'string' ? option.label : renderI18nObject(option.label),
value: option.value,
}
}) || []
}, [options, renderI18nObject])
const value = useStore(field.form.store, s => s.values[field.name]) const value = useStore(field.form.store, s => s.values[field.name])
return ( return (
<div className={cn(fieldClassName)}> <div className={cn(fieldClassName)}>
<div className={cn(labelClassName)}> <div className={cn(labelClassName)}>
{memorizedLabel} {memorizedLabel}
{
required && (
<span className='ml-1 text-text-destructive-secondary'>*</span>
)
}
</div> </div>
<div className={cn(inputContainerClassName)}> <div className={cn(inputContainerClassName)}>
{ {
@ -62,6 +86,7 @@ const BaseField = ({
onChange={e => field.handleChange(e.target.value)} onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur} onBlur={field.handleBlur}
disabled={disabled} disabled={disabled}
placeholder={memorizedPlaceholder}
/> />
) )
} }
@ -76,6 +101,34 @@ const BaseField = ({
onChange={e => field.handleChange(e.target.value)} onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur} onBlur={field.handleBlur}
disabled={disabled} disabled={disabled}
placeholder={memorizedPlaceholder}
/>
)
}
{
formSchema.type === FormTypeEnum.textNumber && (
<Input
id={field.name}
name={field.name}
type='number'
className={cn(inputClassName)}
value={value}
onChange={e => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
disabled={disabled}
placeholder={memorizedPlaceholder}
/>
)
}
{
formSchema.type === FormTypeEnum.select && (
<PureSelect
value={value}
onChange={v => field.handleChange(v)}
disabled={disabled}
placeholder={memorizedPlaceholder}
options={memorizedOptions}
triggerPopupSameWidth
/> />
) )
} }

@ -31,6 +31,13 @@ export enum FormTypeEnum {
dynamicSelect = 'dynamic-select', dynamicSelect = 'dynamic-select',
} }
export type FormOption = {
label: TypeWithI18N | string
value: string
show_on: FormShowOnObject[]
icon?: string
}
export type FormSchema = { export type FormSchema = {
type: FormTypeEnum type: FormTypeEnum
name: string name: string
@ -41,6 +48,9 @@ export type FormSchema = {
show_on?: FormShowOnObject[] show_on?: FormShowOnObject[]
url?: string url?: string
scope?: string scope?: string
help?: string | TypeWithI18N
placeholder?: string | TypeWithI18N
options?: FormOption[]
} }
export type FormValues = Record<string, any> export type FormValues = Record<string, any>

@ -39,6 +39,9 @@ type PureSelectProps = {
itemClassName?: string itemClassName?: string
title?: string title?: string
}, },
placeholder?: string
disabled?: boolean
triggerPopupSameWidth?: boolean
} }
const PureSelect = ({ const PureSelect = ({
options, options,
@ -47,6 +50,9 @@ const PureSelect = ({
containerProps, containerProps,
triggerProps, triggerProps,
popupProps, popupProps,
placeholder,
disabled,
triggerPopupSameWidth,
}: PureSelectProps) => { }: PureSelectProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
@ -74,7 +80,7 @@ const PureSelect = ({
}, [onOpenChange]) }, [onOpenChange])
const selectedOption = options.find(option => option.value === value) const selectedOption = options.find(option => option.value === value)
const triggerText = selectedOption?.label || t('common.placeholder.select') const triggerText = selectedOption?.label || placeholder || t('common.placeholder.select')
return ( return (
<PortalToFollowElem <PortalToFollowElem
@ -82,6 +88,7 @@ const PureSelect = ({
offset={offset || 4} offset={offset || 4}
open={mergedOpen} open={mergedOpen}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
triggerPopupSameWidth={triggerPopupSameWidth}
> >
<PortalToFollowElemTrigger <PortalToFollowElemTrigger
onClick={() => handleOpenChange(!mergedOpen)} onClick={() => handleOpenChange(!mergedOpen)}
@ -135,6 +142,7 @@ const PureSelect = ({
)} )}
title={option.label} title={option.label}
onClick={() => { onClick={() => {
if (disabled) return
onChange?.(option.value) onChange?.(option.value)
handleOpenChange(false) handleOpenChange(false)
}} }}

@ -14,6 +14,7 @@ import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type { FromRefObject } from '@/app/components/base/form/types' import type { FromRefObject } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import type { PluginPayload } from '../types' import type { PluginPayload } from '../types'
import { import {
useAddPluginCredentialHook, useAddPluginCredentialHook,
@ -21,6 +22,7 @@ import {
useInvalidPluginCredentialInfoHook, useInvalidPluginCredentialInfoHook,
useUpdatePluginCredentialHook, useUpdatePluginCredentialHook,
} from '../hooks/use-credential' } from '../hooks/use-credential'
import { useRenderI18nObject } from '@/hooks/use-i18n'
export type ApiKeyModalProps = { export type ApiKeyModalProps = {
pluginPayload: PluginPayload pluginPayload: PluginPayload
@ -38,18 +40,25 @@ const ApiKeyModal = ({
}: ApiKeyModalProps) => { }: ApiKeyModalProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
const { data = [] } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY) const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
const formSchemas = useMemo(() => { const formSchemas = useMemo(() => {
return [ return [
{ {
type: FormTypeEnum.textInput, type: FormTypeEnum.textInput,
name: '__name__', name: '__name__',
label: 'Authorization name', label: t('plugin.auth.authorizationName'),
required: false, required: false,
}, },
...data, ...data,
] ]
}, [data]) }, [data, t])
const defaultValues = formSchemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
const secretInput = formSchemas.find(schema => schema.type === FormTypeEnum.secretInput)
const renderI18nObject = useRenderI18nObject()
const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload) const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload)
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload)
@ -77,7 +86,6 @@ const ApiKeyModal = ({
await updatePluginCredential({ await updatePluginCredential({
credentials: transformedValues, credentials: transformedValues,
credential_id: __credential_id__, credential_id: __credential_id__,
type: CredentialTypeEnum.API_KEY,
name: __name__ || '', name: __name__ || '',
}) })
} }
@ -100,19 +108,21 @@ const ApiKeyModal = ({
return ( return (
<Modal <Modal
size='md' size='md'
title='API Key Authorization Configuration' title={t('plugin.auth.useApiAuth')}
subTitle='After configuring credentials, all members within the workspace can use this tool when orchestrating applications.' subTitle={t('plugin.auth.useApiAuthDesc')}
onClose={onClose} onClose={onClose}
onCancel={onClose} onCancel={onClose}
footerSlot={ footerSlot={
<a secretInput && (
className='system-xs-regular flex h-8 grow items-center text-text-accent' <a
href='' className='system-xs-regular flex h-8 grow items-center text-text-accent'
target='_blank' href={secretInput?.url}
> target='_blank'
Get your API Key from OpenAI >
<RiExternalLinkLine className='ml-1 h-3 w-3' /> {renderI18nObject(secretInput?.help as any)}
</a> <RiExternalLinkLine className='ml-1 h-3 w-3' />
</a>
)
} }
bottomSlot={ bottomSlot={
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'> <div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
@ -133,12 +143,23 @@ const ApiKeyModal = ({
onExtraButtonClick={onRemove} onExtraButtonClick={onRemove}
disabled={disabled} disabled={disabled}
> >
<AuthForm {
ref={formRef} isLoading && (
formSchemas={formSchemas} <div className='flex h-40 items-center justify-center'>
defaultValues={editValues} <Loading />
disabled={disabled} </div>
/> )
}
{
!isLoading && !!data.length && (
<AuthForm
ref={formRef}
formSchemas={formSchemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
)
}
</Modal> </Modal>
) )
} }

@ -2,6 +2,7 @@ import {
memo, memo,
useMemo, useMemo,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next'
import AddOAuthButton from './add-oauth-button' import AddOAuthButton from './add-oauth-button'
import type { AddOAuthButtonProps } from './add-oauth-button' import type { AddOAuthButtonProps } from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button' import AddApiKeyButton from './add-api-key-button'
@ -24,10 +25,11 @@ const Authorize = ({
canApiKey, canApiKey,
disabled, disabled,
}: AuthorizeProps) => { }: AuthorizeProps) => {
const { t } = useTranslation()
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => { const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
if (theme === 'secondary') { if (theme === 'secondary') {
return { return {
buttonText: !canApiKey ? 'Add OAuth Authorization' : 'Add OAuth', buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
buttonVariant: 'secondary', buttonVariant: 'secondary',
className: 'hover:bg-components-button-secondary-bg', className: 'hover:bg-components-button-secondary-bg',
buttonLeftClassName: 'hover:bg-components-button-secondary-bg-hover', buttonLeftClassName: 'hover:bg-components-button-secondary-bg-hover',
@ -38,25 +40,25 @@ const Authorize = ({
} }
return { return {
buttonText: !canApiKey ? 'Use OAuth Authorization' : 'Use OAuth', buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
pluginPayload, pluginPayload,
} }
}, [canApiKey, theme, pluginPayload]) }, [canApiKey, theme, pluginPayload, t])
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => { const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
if (theme === 'secondary') { if (theme === 'secondary') {
return { return {
pluginPayload, pluginPayload,
buttonVariant: 'secondary', buttonVariant: 'secondary',
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Add API Key', buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
} }
} }
return { return {
pluginPayload, pluginPayload,
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Use API Key', buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
buttonVariant: !canOAuth ? 'primary' : 'secondary-accent', buttonVariant: !canOAuth ? 'primary' : 'secondary-accent',
} }
}, [canOAuth, theme, pluginPayload]) }, [canOAuth, theme, pluginPayload, t])
return ( return (
<> <>

@ -3,6 +3,7 @@ import {
useCallback, useCallback,
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react' import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
@ -26,6 +27,7 @@ const AuthorizedInNode = ({
onAuthorizationItemClick, onAuthorizationItemClick,
credentialId, credentialId,
}: AuthorizedInNodeProps) => { }: AuthorizedInNodeProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
const { const {
canApiKey, canApiKey,
@ -37,11 +39,11 @@ const AuthorizedInNode = ({
let label = '' let label = ''
let removed = false let removed = false
if (!credentialId) { if (!credentialId) {
label = 'Workspace default' label = t('plugin.auth.workspaceDefault')
} }
else { else {
const credential = credentials.find(c => c.id === credentialId) const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : 'Auth removed' label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential removed = !credential
} }
return ( return (
@ -65,13 +67,13 @@ const AuthorizedInNode = ({
/> />
</Button> </Button>
) )
}, [credentialId, credentials]) }, [credentialId, credentials, t])
const extraAuthorizationItems: Credential[] = [ const extraAuthorizationItems: Credential[] = [
{ {
id: '__workspace_default__', id: '__workspace_default__',
name: 'Workspace default', name: t('plugin.auth.workspaceDefault'),
provider: '', provider: '',
is_default: false, is_default: !credentialId,
isWorkspaceDefault: true, isWorkspaceDefault: true,
}, },
] ]
@ -100,6 +102,7 @@ const AuthorizedInNode = ({
disableSetDefault disableSetDefault
onItemClick={handleAuthorizationItemClick} onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems} extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
/> />
) )
} }

@ -31,6 +31,7 @@ import {
useDeletePluginCredentialHook, useDeletePluginCredentialHook,
useInvalidPluginCredentialInfoHook, useInvalidPluginCredentialInfoHook,
useSetPluginDefaultCredentialHook, useSetPluginDefaultCredentialHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential' } from '../hooks/use-credential'
type AuthorizedProps = { type AuthorizedProps = {
@ -49,6 +50,7 @@ type AuthorizedProps = {
disableSetDefault?: boolean disableSetDefault?: boolean
onItemClick?: (id: string) => void onItemClick?: (id: string) => void
extraAuthorizationItems?: Credential[] extraAuthorizationItems?: Credential[]
showItemSelectedIcon?: boolean
} }
const Authorized = ({ const Authorized = ({
pluginPayload, pluginPayload,
@ -66,6 +68,7 @@ const Authorized = ({
disableSetDefault, disableSetDefault,
onItemClick, onItemClick,
extraAuthorizationItems, extraAuthorizationItems,
showItemSelectedIcon,
}: AuthorizedProps) => { }: AuthorizedProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { notify } = useToastContext() const { notify } = useToastContext()
@ -125,6 +128,17 @@ const Authorized = ({
}) })
invalidatePluginCredentialInfo() invalidatePluginCredentialInfo()
}, [setPluginDefaultCredential, invalidatePluginCredentialInfo, notify, t]) }, [setPluginDefaultCredential, invalidatePluginCredentialInfo, notify, t])
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const handleRename = useCallback(async (payload: {
credential_id: string
name: string
}) => {
await updatePluginCredential(payload)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}, [updatePluginCredential, notify, t])
return ( return (
<> <>
@ -149,7 +163,12 @@ const Authorized = ({
isOpen && 'bg-components-button-secondary-bg-hover', isOpen && 'bg-components-button-secondary-bg-hover',
)}> )}>
<Indicator className='mr-2' /> <Indicator className='mr-2' />
{credentials.length} Authorizations {credentials.length}&nbsp;
{
credentials.length > 1
? t('plugin.auth.authorizations')
: t('plugin.auth.authorization')
}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' /> <RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button> </Button>
) )
@ -160,31 +179,35 @@ const Authorized = ({
'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg', 'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
popupClassName, 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'> <div className='py-1'>
{
!!extraAuthorizationItems?.length && (
<div className='p-1'>
{
extraAuthorizationItems.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
onItemClick={onItemClick}
disableRename
disableEdit
disableDelete
disableSetDefault
showSelectedIcon={showItemSelectedIcon}
/>
))
}
</div>
)
}
{ {
!!oAuthCredentials.length && ( !!oAuthCredentials.length && (
<div className='p-1'> <div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'> <div className={cn(
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
showItemSelectedIcon && 'pl-7',
)}>
OAuth OAuth
</div> </div>
{ {
@ -192,6 +215,14 @@ const Authorized = ({
<Item <Item
key={credential.id} key={credential.id}
credential={credential} credential={credential}
disabled={disabled}
disableEdit
onDelete={openConfirm}
onSetDefault={handleSetDefault}
onRename={handleRename}
disableSetDefault={disableSetDefault}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon}
/> />
)) ))
} }
@ -201,7 +232,10 @@ const Authorized = ({
{ {
!!apiKeyCredentials.length && ( !!apiKeyCredentials.length && (
<div className='p-1'> <div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'> <div className={cn(
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
showItemSelectedIcon && 'pl-7',
)}>
API Keys API Keys
</div> </div>
{ {
@ -214,7 +248,10 @@ const Authorized = ({
onEdit={handleEdit} onEdit={handleEdit}
onSetDefault={handleSetDefault} onSetDefault={handleSetDefault}
disableSetDefault={disableSetDefault} disableSetDefault={disableSetDefault}
disableRename
onItemClick={onItemClick} onItemClick={onItemClick}
onRename={handleRename}
showSelectedIcon={showItemSelectedIcon}
/> />
)) ))
} }

@ -1,8 +1,11 @@
import { import {
memo, memo,
useMemo, useMemo,
useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next'
import { import {
RiCheckLine,
RiDeleteBinLine, RiDeleteBinLine,
RiEditLine, RiEditLine,
RiEqualizer2Line, RiEqualizer2Line,
@ -12,6 +15,8 @@ import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import cn from '@/utils/classnames'
import type { Credential } from '../types' import type { Credential } from '../types'
import { CredentialTypeEnum } from '../types' import { CredentialTypeEnum } from '../types'
@ -21,11 +26,16 @@ type ItemProps = {
onDelete?: (id: string) => void onDelete?: (id: string) => void
onEdit?: (id: string, values: Record<string, any>) => void onEdit?: (id: string, values: Record<string, any>) => void
onSetDefault?: (id: string) => void onSetDefault?: (id: string) => void
onRename?: (payload: {
credential_id: string
name: string
}) => void
disableRename?: boolean disableRename?: boolean
disableEdit?: boolean disableEdit?: boolean
disableDelete?: boolean disableDelete?: boolean
disableSetDefault?: boolean disableSetDefault?: boolean
onItemClick?: (id: string) => void onItemClick?: (id: string) => void
showSelectedIcon?: boolean
} }
const Item = ({ const Item = ({
credential, credential,
@ -33,12 +43,17 @@ const Item = ({
onDelete, onDelete,
onEdit, onEdit,
onSetDefault, onSetDefault,
onRename,
disableRename, disableRename,
disableEdit, disableEdit,
disableDelete, disableDelete,
disableSetDefault, disableSetDefault,
onItemClick, onItemClick,
showSelectedIcon,
}: ItemProps) => { }: ItemProps) => {
const { t } = useTranslation()
const [renaming, setRenaming] = useState(false)
const [renameValue, setRenameValue] = useState(credential.name)
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2 const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
const showAction = useMemo(() => { const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete && disableSetDefault) return !(disableRename && disableEdit && disableDelete && disableSetDefault)
@ -47,27 +62,77 @@ const Item = ({
return ( return (
<div <div
key={credential.id} key={credential.id}
className='group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover' className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
renaming && 'bg-state-base-hover',
)}
onClick={() => onItemClick?.(credential.id)} 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 && ( renaming && (
<div className='flex w-full items-center space-x-1'>
<Input
wrapperClassName='grow rounded-[6px]'
className='h-6'
value={renameValue}
onChange={e => setRenameValue(e.target.value)}
placeholder={t('common.placeholder.input')}
/>
<Button
size='small'
variant='primary'
onClick={() => {
onRename?.({
credential_id: credential.id,
name: renameValue,
})
setRenaming(false)
}}
>
{t('common.operation.save')}
</Button>
<Button
size='small'
onClick={() => setRenaming(false)}
>
{t('common.operation.cancel')}
</Button>
</div>
)
}
{
!renaming && (
<div className='flex w-0 grow items-center space-x-1.5'>
{
showSelectedIcon && (
<div className='h-4 w-4'>
{
credential.is_default && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}
</div>
)
}
<Indicator className='ml-2 mr-1.5 shrink-0' />
<div
className='system-md-regular truncate text-text-secondary'
title={credential.name}
>
{credential.name}
</div>
{
credential.is_default && (
<Badge className='shrink-0'>
{t('plugin.auth.default')}
</Badge>
)
}
</div>
)
}
{
showAction && !renaming && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'> <div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{ {
!credential.is_default && !disableSetDefault && ( !credential.is_default && !disableSetDefault && (
@ -79,14 +144,21 @@ const Item = ({
onSetDefault?.(credential.id) onSetDefault?.(credential.id)
}} }}
> >
Set as default {t('plugin.auth.setDefault')}
</Button> </Button>
) )
} }
{ {
isOAuth && !disableRename && ( !disableRename && (
<Tooltip popupContent='rename'> <Tooltip popupContent={t('common.operation.rename')}>
<ActionButton> <ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
setRenaming(true)
setRenameValue(credential.name)
}}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' /> <RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
@ -94,7 +166,7 @@ const Item = ({
} }
{ {
!isOAuth && !disableEdit && ( !isOAuth && !disableEdit && (
<Tooltip popupContent='edit'> <Tooltip popupContent={t('common.operation.edit')}>
<ActionButton <ActionButton
disabled={disabled} disabled={disabled}
onClick={(e) => { onClick={(e) => {
@ -116,15 +188,16 @@ const Item = ({
} }
{ {
!disableDelete && ( !disableDelete && (
<Tooltip popupContent='delete'> <Tooltip popupContent={t('common.operation.delete')}>
<ActionButton <ActionButton
className='hover:bg-transparent'
disabled={disabled} disabled={disabled}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
onDelete?.(credential.id) onDelete?.(credential.id)
}} }}
> >
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' /> <RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
) )

@ -1,5 +1,6 @@
export { default as PluginAuth } from './plugin-auth' export { default as PluginAuth } from './plugin-auth'
export { default as Authorized } from './authorized' export { default as Authorized } from './authorized'
export { default as AuthorizedInNode } from './authorized-in-node' export { default as AuthorizedInNode } from './authorized-in-node'
export { default as PluginAuthInAgent } from './plugin-auth-in-agent'
export { usePluginAuth } from './hooks/use-plugin-auth' export { usePluginAuth } from './hooks/use-plugin-auth'
export * from './types' export * from './types'

@ -0,0 +1,119 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { RiArrowDownSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Authorize from './authorize'
import Authorized from './authorized'
import type {
Credential,
PluginPayload,
} from './types'
import { usePluginAuth } from './hooks/use-plugin-auth'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
type PluginAuthInAgentProps = {
pluginPayload: PluginPayload
credentialId?: string
onAuthorizationItemClick?: (id: string) => void
}
const PluginAuthInAgent = ({
pluginPayload,
credentialId,
onAuthorizationItemClick,
}: PluginAuthInAgentProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const {
isAuthorized,
canOAuth,
canApiKey,
credentials,
disabled,
} = usePluginAuth(pluginPayload, true)
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
name: t('plugin.auth.workspaceDefault'),
provider: '',
is_default: !credentialId,
isWorkspaceDefault: true,
},
]
const handleAuthorizationItemClick = useCallback((id: string) => {
onAuthorizationItemClick?.(id)
setIsOpen(false)
}, [
onAuthorizationItemClick,
setIsOpen,
])
const renderTrigger = useCallback((isOpen?: boolean) => {
let label = ''
let removed = false
if (!credentialId) {
label = t('plugin.auth.workspaceDefault')
}
else {
const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential
}
return (
<Button
className={cn(
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
removed && 'text-text-destructive',
)}>
<Indicator
className='mr-2'
color={removed ? 'red' : 'green'}
/>
{label}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
}, [credentialId, credentials, t])
return (
<>
{
!isAuthorized && (
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
/>
)
}
{
isAuthorized && (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
/>
)
}
</>
)
}
export default memo(PluginAuthInAgent)

@ -4,7 +4,6 @@ import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Link from 'next/link' import Link from 'next/link'
import { import {
RiArrowLeftLine,
RiArrowRightUpLine, RiArrowRightUpLine,
} from '@remixicon/react' } from '@remixicon/react'
import { import {
@ -15,24 +14,17 @@ import {
import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger' import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger'
import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item' import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form'
import Toast from '@/app/components/base/toast'
import Textarea from '@/app/components/base/textarea' import Textarea from '@/app/components/base/textarea'
import Divider from '@/app/components/base/divider' import Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain' import TabSlider from '@/app/components/base/tab-slider-plain'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form' import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useAppContext } from '@/context/app-context'
import { import {
useAllBuiltInTools, useAllBuiltInTools,
useAllCustomTools, useAllCustomTools,
useAllWorkflowTools, useAllWorkflowTools,
useInvalidateAllBuiltInTools, useInvalidateAllBuiltInTools,
useUpdateProviderCredentials,
} from '@/service/use-tools' } from '@/service/use-tools'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks' import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
@ -46,6 +38,10 @@ import { MARKETPLACE_API_PREFIX } from '@/config'
import type { Node } from 'reactflow' import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types' import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import {
AuthCategory,
PluginAuthInAgent,
} from '@/app/components/plugins/plugin-auth'
type Props = { type Props = {
disabled?: boolean disabled?: boolean
@ -191,23 +187,6 @@ const ToolSelector: FC<Props> = ({
} as any) } as any)
} }
// authorization
const { isCurrentWorkspaceManager } = useAppContext()
const [isShowSettingAuth, setShowSettingAuth] = useState(false)
const handleCredentialSettingUpdate = () => {
invalidateAllBuiltinTools()
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
setShowSettingAuth(false)
onShowChange(false)
}
const { mutate: updatePermission } = useUpdateProviderCredentials({
onSuccess: handleCredentialSettingUpdate,
})
// install from marketplace // install from marketplace
const currentTool = useMemo(() => { const currentTool = useMemo(() => {
return currentProvider?.tools.find(tool => tool.name === value?.tool_name) return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
@ -221,6 +200,12 @@ const ToolSelector: FC<Props> = ({
invalidateAllBuiltinTools() invalidateAllBuiltinTools()
invalidateInstalledPluginList() invalidateInstalledPluginList()
} }
const handleAuthorizationItemClick = (id: string) => {
onSelect({
...value,
credential_id: id,
} as any)
}
return ( return (
<> <>
@ -257,7 +242,6 @@ const ToolSelector: FC<Props> = ({
onSwitchChange={handleEnabledChange} onSwitchChange={handleEnabledChange}
onDelete={onDelete} onDelete={onDelete}
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization} noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
onAuth={() => setShowSettingAuth(true)}
uninstalled={!currentProvider && inMarketPlace} uninstalled={!currentProvider && inMarketPlace}
versionMismatch={currentProvider && inMarketPlace && !currentTool} versionMismatch={currentProvider && inMarketPlace && !currentTool}
installInfo={manifest?.latest_package_identifier} installInfo={manifest?.latest_package_identifier}
@ -276,181 +260,141 @@ const ToolSelector: FC<Props> = ({
)} )}
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent> <PortalToFollowElemContent>
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', !isShowSettingAuth && 'overflow-y-auto pb-2')}> <div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
{!isShowSettingAuth && ( <>
<> <div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div>
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div> {/* base form */}
{/* base form */} <div className='flex flex-col gap-3 px-4 py-2'>
<div className='flex flex-col gap-3 px-4 py-2'> <div className='flex flex-col gap-1'>
<div className='flex flex-col gap-1'> <div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div> <ToolPicker
<ToolPicker panelClassName='w-[328px]'
panelClassName='w-[328px]' placement='bottom'
placement='bottom' offset={offset}
offset={offset} trigger={
trigger={ <ToolTrigger
<ToolTrigger open={panelShowState || isShowChooseTool}
open={panelShowState || isShowChooseTool} value={value}
value={value} provider={currentProvider}
provider={currentProvider} />
/> }
} isShow={panelShowState || isShowChooseTool}
isShow={panelShowState || isShowChooseTool} onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool}
onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool} disabled={false}
disabled={false} supportAddCustomTool
supportAddCustomTool onSelect={handleSelectTool}
onSelect={handleSelectTool} scope={scope}
scope={scope} selectedTools={selectedTools}
selectedTools={selectedTools} />
</div>
<div className='flex flex-col gap-1'>
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.descriptionLabel')}</div>
<Textarea
className='resize-none'
placeholder={t('plugin.detailPanel.toolSelector.descriptionPlaceholder')}
value={value?.extra?.description || ''}
onChange={handleDescriptionChange}
disabled={!value?.provider_name}
/>
</div>
</div>
{/* authorization */}
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<>
<Divider className='my-1 w-full' />
<div className='px-4 py-2'>
<PluginAuthInAgent
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/> />
</div> </div>
<div className='flex flex-col gap-1'> </>
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.descriptionLabel')}</div> )}
<Textarea {/* tool settings */}
className='resize-none' {(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
placeholder={t('plugin.detailPanel.toolSelector.descriptionPlaceholder')} <>
value={value?.extra?.description || ''} <Divider className='my-1 w-full' />
onChange={handleDescriptionChange} {/* tabs */}
disabled={!value?.provider_name} {nodeId && showTabSlider && (
<TabSlider
className='mt-1 shrink-0 px-4'
itemClassName='py-3'
noBorderBottom
smallItem
value={currType}
onChange={(value) => {
setCurrType(value)
}}
options={[
{ value: 'settings', text: t('plugin.detailPanel.toolSelector.settings')! },
{ value: 'params', text: t('plugin.detailPanel.toolSelector.params')! },
]}
/> />
</div> )}
</div> {nodeId && showTabSlider && currType === 'params' && (
{/* authorization */}
{currentProvider && currentProvider.type === CollectionType.builtIn && currentProvider.allow_delete && (
<>
<Divider className='my-1 w-full' />
<div className='px-4 py-2'> <div className='px-4 py-2'>
{!currentProvider.is_team_authorization && ( <div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<Button <div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
variant='primary'
className={cn('w-full shrink-0')}
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>
{t('tools.auth.unauthorized')}
</Button>
)}
{currentProvider.is_team_authorization && (
<Button
variant='secondary'
className={cn('w-full shrink-0')}
onClick={() => setShowSettingAuth(true)}
disabled={!isCurrentWorkspaceManager}
>
<Indicator className='mr-2' color={'green'} />
{t('tools.auth.authorized')}
</Button>
)}
</div> </div>
</> )}
)} {/* user settings only */}
{/* tool settings */} {userSettingsOnly && (
{(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && ( <div className='p-4 pb-1'>
<> <div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.settings')}</div>
<Divider className='my-1 w-full' /> </div>
{/* tabs */} )}
{nodeId && showTabSlider && ( {/* reasoning config only */}
<TabSlider {nodeId && reasoningConfigOnly && (
className='mt-1 shrink-0 px-4' <div className='mb-1 p-4 pb-1'>
itemClassName='py-3' <div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.params')}</div>
noBorderBottom <div className='pb-1'>
smallItem
value={currType}
onChange={(value) => {
setCurrType(value)
}}
options={[
{ value: 'settings', text: t('plugin.detailPanel.toolSelector.settings')! },
{ value: 'params', text: t('plugin.detailPanel.toolSelector.params')! },
]}
/>
)}
{nodeId && showTabSlider && currType === 'params' && (
<div className='px-4 py-2'>
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div> <div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div>
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div> <div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
</div> </div>
)} </div>
{/* user settings only */} )}
{userSettingsOnly && ( {/* user settings form */}
<div className='p-4 pb-1'> {(currType === 'settings' || userSettingsOnly) && (
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.settings')}</div> <div className='px-4 py-2'>
</div> <Form
)} value={getPlainValue(value?.settings || {})}
{/* reasoning config only */} onChange={handleSettingsFormChange}
{nodeId && reasoningConfigOnly && ( formSchemas={settingsFormSchemas as any}
<div className='mb-1 p-4 pb-1'> isEditMode={true}
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.params')}</div> showOnVariableMap={{}}
<div className='pb-1'> validating={false}
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip1')}</div> inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div> fieldMoreInfo={item => item.url
</div> ? (<a
</div> href={item.url}
)} target='_blank' rel='noopener noreferrer'
{/* user settings form */} className='inline-flex items-center text-xs text-text-accent'
{(currType === 'settings' || userSettingsOnly) && ( >
<div className='px-4 py-2'> {t('tools.howToGet')}
<Form <RiArrowRightUpLine className='ml-1 h-3 w-3' />
value={getPlainValue(value?.settings || {})} </a>)
onChange={handleSettingsFormChange} : null}
formSchemas={settingsFormSchemas as any}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='bg-components-input-bg-normal hover:bg-components-input-bg-hover'
fieldMoreInfo={item => item.url
? (<a
href={item.url}
target='_blank' rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<RiArrowRightUpLine className='ml-1 h-3 w-3' />
</a>)
: null}
/>
</div>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/> />
)} </div>
</> )}
)} {/* reasoning config form */}
</> {nodeId && (currType === 'params' || reasoningConfigOnly) && (
)} <ReasoningConfigForm
{/* authorization panel */} value={value?.parameters || {}}
{isShowSettingAuth && currentProvider && ( onChange={handleParamsFormChange}
<> schemas={paramsFormSchemas as any}
<div className='relative flex flex-col gap-1 pt-3.5'> nodeOutputVars={nodeOutputVars}
<div className='absolute -top-2 left-2 w-[345px] rounded-t-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pt-2 backdrop-blur-sm'></div> availableNodes={availableNodes}
<div nodeId={nodeId}
className='system-xs-semibold-uppercase flex h-6 cursor-pointer items-center gap-1 px-3 text-text-accent-secondary' />
onClick={() => setShowSettingAuth(false)} )}
> </>
<RiArrowLeftLine className='h-4 w-4' /> )}
BACK </>
</div>
<div className='system-xl-semibold px-4 text-text-primary'>{t('tools.auth.setupModalTitle')}</div>
<div className='system-xs-regular px-4 text-text-tertiary'>{t('tools.auth.setupModalTitleDescription')}</div>
</div>
<ToolCredentialForm
collection={currentProvider}
onCancel={() => setShowSettingAuth(false)}
onSaved={async value => updatePermission({
providerName: currentProvider.name,
credentials: value,
})}
/>
</>
)}
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
</PortalToFollowElem> </PortalToFollowElem>

@ -27,7 +27,6 @@ type Props = {
onSwitchChange?: (value: boolean) => void onSwitchChange?: (value: boolean) => void
onDelete?: () => void onDelete?: () => void
noAuth?: boolean noAuth?: boolean
onAuth?: () => void
isError?: boolean isError?: boolean
errorTip?: any errorTip?: any
uninstalled?: boolean uninstalled?: boolean
@ -35,6 +34,7 @@ type Props = {
onInstall?: () => void onInstall?: () => void
versionMismatch?: boolean versionMismatch?: boolean
open: boolean open: boolean
authRemoved?: boolean
} }
const ToolItem = ({ const ToolItem = ({
@ -47,13 +47,13 @@ const ToolItem = ({
onSwitchChange, onSwitchChange,
onDelete, onDelete,
noAuth, noAuth,
onAuth,
uninstalled, uninstalled,
installInfo, installInfo,
onInstall, onInstall,
isError, isError,
errorTip, errorTip,
versionMismatch, versionMismatch,
authRemoved,
}: Props) => { }: Props) => {
const { t } = useTranslation() const { t } = useTranslation()
const providerNameText = providerName?.split('/').pop() const providerNameText = providerName?.split('/').pop()
@ -113,11 +113,17 @@ const ToolItem = ({
</div> </div>
)} )}
{!isError && !uninstalled && !versionMismatch && noAuth && ( {!isError && !uninstalled && !versionMismatch && noAuth && (
<Button variant='secondary' size='small' onClick={onAuth}> <Button variant='secondary' size='small'>
{t('tools.notAuthorized')} {t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' /> <Indicator className='ml-2' color='orange' />
</Button> </Button>
)} )}
{!isError && !uninstalled && !versionMismatch && authRemoved && (
<Button variant='secondary' size='small'>
{t('plugin.auth.authRemoved')}
<Indicator className='ml-2' color='red' />
</Button>
)}
{!isError && !uninstalled && versionMismatch && installInfo && ( {!isError && !uninstalled && versionMismatch && installInfo && (
<div onClick={e => e.stopPropagation()}> <div onClick={e => e.stopPropagation()}>
<SwitchPluginVersion <SwitchPluginVersion

@ -92,7 +92,8 @@ export type CommonNodeType<T = {}> = {
error_strategy?: ErrorHandleTypeEnum error_strategy?: ErrorHandleTypeEnum
retry_config?: WorkflowRetryConfig retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[] default_value?: DefaultValueForm[]
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name' | 'credential_id'>> credential_id?: string
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
export type CommonEdgeType = { export type CommonEdgeType = {
_hovering?: boolean _hovering?: boolean

@ -213,6 +213,26 @@ const translation = {
requestAPlugin: 'Request a plugin', requestAPlugin: 'Request a plugin',
publishPlugins: 'Publish plugins', publishPlugins: 'Publish plugins',
difyVersionNotCompatible: 'The current Dify version is not compatible with this plugin, please upgrade to the minimum version required: {{minimalDifyVersion}}', difyVersionNotCompatible: 'The current Dify version is not compatible with this plugin, please upgrade to the minimum version required: {{minimalDifyVersion}}',
auth: {
default: 'Default',
setDefault: 'Set as default',
useOAuth: 'Use OAuth',
useOAuthAuth: 'Use OAuth Authorization',
addOAuth: 'Add OAuth',
setupOAuth: 'Setup OAuth Client',
useApi: 'Use API Key',
addApi: 'Add API Key',
useApiAuth: 'API Key Authorization Configuration',
useApiAuthDesc: 'After configuring credentials, all members within the workspace can use this tool when orchestrating applications.',
oauthClientSettings: 'OAuth Client Settings',
saveOnly: 'Save only',
saveAndAuth: 'Save and Authorize',
authorization: 'Authorization',
authorizations: 'Authorizations',
authorizationName: 'Authorization Name',
workspaceDefault: 'Workspace Default',
authRemoved: 'Auth removed',
},
} }
export default translation export default translation

@ -213,6 +213,26 @@ const translation = {
requestAPlugin: '申请插件', requestAPlugin: '申请插件',
publishPlugins: '发布插件', publishPlugins: '发布插件',
difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}', difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}',
auth: {
default: '默认',
setDefault: '设为默认',
useOAuth: '使用 OAuth',
useOAuthAuth: '使用 OAuth 授权',
addOAuth: '添加 OAuth',
setupOAuth: '设置 OAuth 客户端',
useApi: '使用 API Key',
addApi: '添加 API Key',
useApiAuth: 'API Key 授权配置',
useApiAuthDesc: '配置凭据后,工作区内的所有成员在编排应用时都可以使用此工具。',
oauthClientSettings: 'OAuth 客户端设置',
saveOnly: '仅保存',
saveAndAuth: '保存并授权',
authorization: '凭据',
authorizations: '凭据',
authorizationName: '凭据名称',
workspaceDefault: '工作区默认',
authRemoved: '凭据已移除',
},
} }
export default translation export default translation

@ -72,8 +72,7 @@ export const useUpdatePluginCredential = (
return useMutation({ return useMutation({
mutationFn: (params: { mutationFn: (params: {
credential_id: string credential_id: string
credentials: Record<string, any> credentials?: Record<string, any>
type: CredentialTypeEnum
name?: string name?: string
}) => { }) => {
return post(url, { body: params }) return post(url, { body: params })

Loading…
Cancel
Save