Support OAuth Integration for Plugin Tools (#22550)

Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
This commit is contained in:
Maries
2025-07-17 17:18:44 +08:00
committed by GitHub
parent 965e952336
commit a4ef900916
89 changed files with 5516 additions and 875 deletions

View File

@@ -18,7 +18,6 @@ import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import ConfigContext from '@/context/debug-configuration'
import type { AgentTool } from '@/types/app'
import { type Collection, CollectionType } from '@/app/components/tools/types'
@@ -26,8 +25,6 @@ import { MAX_TOOLS_NUM } from '@/config'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import { updateBuiltInToolCredential } from '@/service/tools'
import cn from '@/utils/classnames'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
@@ -57,13 +54,7 @@ const AgentTools: FC = () => {
const formattingChangedDispatcher = useFormattingChangedDispatcher()
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
const currentCollection = useMemo(() => {
if (!currentTool) return null
const collection = collectionList.find(collection => canFindTool(collection.id, currentTool?.provider_id) && collection.type === currentTool?.provider_type)
return collection
}, [currentTool, collectionList])
const [isShowSettingTool, setIsShowSettingTool] = useState(false)
const [isShowSettingAuth, setShowSettingAuth] = useState(false)
const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => {
const collection = collectionList.find(
collection =>
@@ -100,17 +91,6 @@ const AgentTools: FC = () => {
formattingChangedDispatcher()
}
const handleToolAuthSetting = (value: AgentToolWithMoreInfo) => {
const newModelConfig = produce(modelConfig, (draft) => {
const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === value?.collection?.id && item.tool_name === value?.tool_name)
if (tool)
(tool as AgentTool).notAuthor = false
})
setModelConfig(newModelConfig)
setIsShowSettingTool(false)
formattingChangedDispatcher()
}
const [isDeleting, setIsDeleting] = useState<number>(-1)
const getToolValue = (tool: ToolDefaultValue) => {
return {
@@ -144,6 +124,20 @@ const AgentTools: FC = () => {
return item.provider_name
}
const handleAuthorizationItemClick = useCallback((credentialId: string) => {
const newModelConfig = produce(modelConfig, (draft) => {
const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === currentTool?.provider_id)
if (tool)
(tool as AgentTool).credential_id = credentialId
})
setCurrentTool({
...currentTool,
credential_id: credentialId,
} as any)
setModelConfig(newModelConfig)
formattingChangedDispatcher()
}, [currentTool, modelConfig, setModelConfig, formattingChangedDispatcher])
return (
<>
<Panel
@@ -299,7 +293,7 @@ const AgentTools: FC = () => {
{item.notAuthor && (
<Button variant='secondary' size='small' onClick={() => {
setCurrentTool(item)
setShowSettingAuth(true)
setIsShowSettingTool(true)
}}>
{t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' />
@@ -319,21 +313,8 @@ const AgentTools: FC = () => {
isModel={currentTool?.collection?.type === CollectionType.model}
onSave={handleToolSettingChange}
onHide={() => setIsShowSettingTool(false)}
/>
)}
{isShowSettingAuth && (
<ConfigCredential
collection={currentCollection as any}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential((currentCollection as any).name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleToolAuthSetting(currentTool)
setShowSettingAuth(false)
}}
credentialId={currentTool?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
)}
</>

View File

@@ -14,7 +14,6 @@ import Icon from '@/app/components/plugins/card/base/card-icon'
import OrgInfo from '@/app/components/plugins/card/base/org-info'
import Description from '@/app/components/plugins/card/base/description'
import TabSlider from '@/app/components/base/tab-slider-plain'
import Button from '@/app/components/base/button'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
@@ -25,6 +24,10 @@ import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import cn from '@/utils/classnames'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import {
AuthCategory,
PluginAuthInAgent,
} from '@/app/components/plugins/plugin-auth'
type Props = {
showBackButton?: boolean
@@ -36,6 +39,8 @@ type Props = {
readonly?: boolean
onHide: () => void
onSave?: (value: Record<string, any>) => void
credentialId?: string
onAuthorizationItemClick?: (id: string) => void
}
const SettingBuiltInTool: FC<Props> = ({
@@ -48,6 +53,8 @@ const SettingBuiltInTool: FC<Props> = ({
readonly,
onHide,
onSave,
credentialId,
onAuthorizationItemClick,
}) => {
const { locale } = useContext(I18n)
const language = getLanguage(locale)
@@ -197,8 +204,20 @@ const SettingBuiltInTool: FC<Props> = ({
</div>
<div className='system-md-semibold mt-1 text-text-primary'>{currTool?.label[language]}</div>
{!!currTool?.description[language] && (
<Description className='mt-3' text={currTool.description[language]} descriptionLineRows={2}></Description>
<Description className='mb-2 mt-3 h-auto' text={currTool.description[language]} descriptionLineRows={2}></Description>
)}
{
collection.allow_delete && collection.type === CollectionType.builtIn && (
<PluginAuthInAgent
pluginPayload={{
provider: collection.name,
category: AuthCategory.tool,
}}
credentialId={credentialId}
onAuthorizationItemClick={onAuthorizationItemClick}
/>
)
}
</div>
{/* form */}
<div className='h-full'>

View File

@@ -117,7 +117,7 @@ const Question: FC<QuestionProps> = ({
</div>
<div
ref={contentRef}
className='w-full rounded-2xl bg-background-gradient-bg-fill-chat-bubble-bg-3 px-4 py-3 text-sm text-text-primary'
className='bg-background-gradient-bg-fill-chat-bubble-bg-3 w-full rounded-2xl px-4 py-3 text-sm text-text-primary'
style={theme?.chatBubbleColorStyle ? CssTransform(theme.chatBubbleColorStyle) : {}}
>
{

View File

@@ -0,0 +1,177 @@
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 PureSelect from '@/app/components/base/select/pure'
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,
required,
placeholder,
options,
labelClassName: formLabelClassName,
show_on = [],
} = 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 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 values = useStore(field.form.store, (s) => {
return show_on.reduce((acc, condition) => {
acc[condition.variable] = s.values[condition.variable]
return acc
}, {} as Record<string, any>)
})
const show = useMemo(() => {
return show_on.every((condition) => {
const conditionValue = values[condition.variable]
return conditionValue === condition.value
})
}, [values, show_on])
if (!show)
return null
return (
<div className={cn(fieldClassName)}>
<div className={cn(labelClassName, formLabelClassName)}>
{memorizedLabel}
{
required && !isValidElement(label) && (
<span className='ml-1 text-text-destructive-secondary'>*</span>
)
}
</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}
placeholder={memorizedPlaceholder}
/>
)
}
{
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}
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
/>
)
}
{
formSchema.type === FormTypeEnum.radio && (
<div className='flex items-center space-x-2'>
{
memorizedOptions.map(option => (
<div
key={option.value}
className={cn(
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
)}
onClick={() => field.handleChange(option.value)}
>
{option.label}
</div>
))
}
</div>
)
}
</div>
</div>
)
}
export default memo(BaseField)

View File

@@ -0,0 +1,115 @@
import {
memo,
useCallback,
useImperativeHandle,
} from 'react'
import type {
AnyFieldApi,
AnyFormApi,
} 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'
import {
useGetFormValues,
useGetValidators,
} from '@/app/components/base/form/hooks'
export type BaseFormProps = {
formSchemas?: FormSchema[]
defaultValues?: Record<string, any>
formClassName?: string
ref?: FormRef
disabled?: boolean
formFromProps?: AnyFormApi
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
const BaseForm = ({
formSchemas = [],
defaultValues,
formClassName,
fieldClassName,
labelClassName,
inputContainerClassName,
inputClassName,
ref,
disabled,
formFromProps,
}: BaseFormProps) => {
const formFromHook = useForm({
defaultValues,
})
const form: any = formFromProps || formFromHook
const { getFormValues } = useGetFormValues(form, formSchemas)
const { getValidators } = useGetValidators()
useImperativeHandle(ref, () => {
return {
getForm() {
return form
},
getFormValues: (option) => {
return getFormValues(option)
},
}
}, [form, getFormValues])
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])
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
const validators = getValidators(formSchema)
const {
name,
} = formSchema
return (
<form.Field
key={name}
name={name}
validators={validators}
>
{renderField}
</form.Field>
)
}, [renderField, form, getValidators])
if (!formSchemas?.length)
return null
return (
<form
className={cn(formClassName)}
>
{formSchemas.map(renderFieldWrapper)}
</form>
)
}
export default memo(BaseForm)

View File

@@ -0,0 +1,2 @@
export { default as BaseForm, type BaseFormProps } from './base-form'
export { default as BaseField, type BaseFieldProps } from './base-field'

View File

@@ -0,0 +1,23 @@
import { memo } from 'react'
import { BaseForm } from '../../components/base'
import type { BaseFormProps } from '../../components/base'
const AuthForm = ({
formSchemas = [],
defaultValues,
ref,
formFromProps,
}: 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'
formFromProps={formFromProps}
/>
)
}
export default memo(AuthForm)

View File

@@ -0,0 +1,3 @@
export * from './use-check-validated'
export * from './use-get-form-values'
export * from './use-get-validators'

View File

@@ -0,0 +1,48 @@
import { useCallback } from 'react'
import type { AnyFormApi } from '@tanstack/react-form'
import { useToastContext } from '@/app/components/base/toast'
import type { FormSchema } from '@/app/components/base/form/types'
export const useCheckValidated = (form: AnyFormApi, FormSchemas: FormSchema[]) => {
const { notify } = useToastContext()
const checkValidated = useCallback(() => {
const allError = form?.getAllErrors()
const values = form.state.values
if (allError) {
const fields = allError.fields
const errorArray = Object.keys(fields).reduce((acc: string[], key: string) => {
const currentSchema = FormSchemas.find(schema => schema.name === key)
const { show_on = [] } = currentSchema || {}
const showOnValues = show_on.reduce((acc, condition) => {
acc[condition.variable] = values[condition.variable]
return acc
}, {} as Record<string, any>)
const show = show_on?.every((condition) => {
const conditionValue = showOnValues[condition.variable]
return conditionValue === condition.value
})
const errors: any[] = show ? fields[key].errors : []
return [...acc, ...errors]
}, [] as string[])
if (errorArray.length) {
notify({
type: 'error',
message: errorArray[0],
})
return false
}
return true
}
return true
}, [form, notify, FormSchemas])
return {
checkValidated,
}
}

View File

@@ -0,0 +1,44 @@
import { useCallback } from 'react'
import type { AnyFormApi } from '@tanstack/react-form'
import { useCheckValidated } from './use-check-validated'
import type {
FormSchema,
GetValuesOptions,
} from '../types'
import { getTransformedValuesWhenSecretInputPristine } from '../utils'
export const useGetFormValues = (form: AnyFormApi, formSchemas: FormSchema[]) => {
const { checkValidated } = useCheckValidated(form, formSchemas)
const getFormValues = useCallback((
{
needCheckValidatedValues,
needTransformWhenSecretFieldIsPristine,
}: GetValuesOptions,
) => {
const values = form?.store.state.values || {}
if (!needCheckValidatedValues) {
return {
values,
isCheckValidated: false,
}
}
if (checkValidated()) {
return {
values: needTransformWhenSecretFieldIsPristine ? getTransformedValuesWhenSecretInputPristine(formSchemas, form) : values,
isCheckValidated: true,
}
}
else {
return {
values: {},
isCheckValidated: false,
}
}
}, [form, checkValidated, formSchemas])
return {
getFormValues,
}
}

View File

@@ -0,0 +1,36 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import type { FormSchema } from '../types'
export const useGetValidators = () => {
const { t } = useTranslation()
const getValidators = useCallback((formSchema: FormSchema) => {
const {
name,
validators,
required,
} = formSchema
let mergedValidators = validators
if (required && !validators) {
mergedValidators = {
onMount: ({ value }: any) => {
if (!value)
return t('common.errorMsg.fieldRequired', { field: name })
},
onChange: ({ value }: any) => {
if (!value)
return t('common.errorMsg.fieldRequired', { field: name })
},
onBlur: ({ value }: any) => {
if (!value)
return t('common.errorMsg.fieldRequired', { field: name })
},
}
}
return mergedValidators
}, [t])
return {
getValidators,
}
}

View File

@@ -0,0 +1,76 @@
import type {
ForwardedRef,
ReactNode,
} from 'react'
import type {
AnyFormApi,
FieldValidators,
} 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 FormOption = {
label: TypeWithI18N | string
value: string
show_on?: FormShowOnObject[]
icon?: string
}
export type AnyValidators = FieldValidators<any, any, any, any, any, any, any, any, any, any>
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
help?: string | TypeWithI18N
placeholder?: string | TypeWithI18N
options?: FormOption[]
labelClassName?: string
validators?: AnyValidators
}
export type FormValues = Record<string, any>
export type GetValuesOptions = {
needTransformWhenSecretFieldIsPristine?: boolean
needCheckValidatedValues?: boolean
}
export type FormRefObject = {
getForm: () => AnyFormApi
getFormValues: (obj: GetValuesOptions) => {
values: Record<string, any>
isCheckValidated: boolean
}
}
export type FormRef = ForwardedRef<FormRefObject>

View File

@@ -0,0 +1 @@
export * from './secret-input'

View File

@@ -0,0 +1,29 @@
import type { AnyFormApi } from '@tanstack/react-form'
import type { FormSchema } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
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
}
export const getTransformedValuesWhenSecretInputPristine = (formSchemas: FormSchema[], form: AnyFormApi) => {
const values = form?.store.state.values || {}
const isPristineSecretInputNames: string[] = []
for (let i = 0; i < formSchemas.length; i++) {
const schema = formSchemas[i]
if (schema.type === FormTypeEnum.secretInput) {
const fieldMeta = form?.getFieldMeta(schema.name)
if (fieldMeta?.isPristine)
isPristineSecretInputNames.push(schema.name)
}
}
return transformFormSchemasSecretInput(isPristineSecretInputNames, values)
}

View File

@@ -0,0 +1,127 @@
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 justify-between p-6 pt-5'>
<div>
{footerSlot}
</div>
<div className='flex items-center'>
{
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>
</div>
{bottomSlot}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default memo(Modal)

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,181 @@
import {
memo,
useCallback,
useMemo,
useRef,
useState,
} 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 { CredentialTypeEnum } from '../types'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type { FormRefObject } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import type { PluginPayload } from '../types'
import {
useAddPluginCredentialHook,
useGetPluginCredentialSchemaHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
import { useRenderI18nObject } from '@/hooks/use-i18n'
export type ApiKeyModalProps = {
pluginPayload: PluginPayload
onClose?: () => void
editValues?: Record<string, any>
onRemove?: () => void
disabled?: boolean
onUpdate?: () => void
}
const ApiKeyModal = ({
pluginPayload,
onClose,
editValues,
onRemove,
disabled,
onUpdate,
}: ApiKeyModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((value: boolean) => {
doingActionRef.current = value
setDoingAction(value)
}, [])
const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
const formSchemas = useMemo(() => {
return [
{
type: FormTypeEnum.textInput,
name: '__name__',
label: t('plugin.auth.authorizationName'),
required: false,
},
...data,
]
}, [data, t])
const defaultValues = formSchemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
const helpField = formSchemas.find(schema => schema.url && schema.help)
const renderI18nObject = useRenderI18nObject()
const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload)
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const formRef = useRef<FormRefObject>(null)
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
const {
isCheckValidated,
values,
} = formRef.current?.getFormValues({
needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
return
try {
const {
__name__,
__credential_id__,
...restValues
} = values
handleSetDoingAction(true)
if (editValues) {
await updatePluginCredential({
credentials: restValues,
credential_id: __credential_id__,
name: __name__ || '',
})
}
else {
await addPluginCredential({
credentials: restValues,
type: CredentialTypeEnum.API_KEY,
name: __name__ || '',
})
}
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [addPluginCredential, onClose, onUpdate, updatePluginCredential, notify, t, editValues, handleSetDoingAction])
return (
<Modal
size='md'
title={t('plugin.auth.useApiAuth')}
subTitle={t('plugin.auth.useApiAuthDesc')}
onClose={onClose}
onCancel={onClose}
footerSlot={
helpField && (
<a
className='system-xs-regular mr-2 flex items-center py-2 text-text-accent'
href={helpField?.url}
target='_blank'
>
<span className='break-all'>
{renderI18nObject(helpField?.help as any)}
</span>
<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 || isLoading || doingAction}
>
{
isLoading && (
<div className='flex h-40 items-center justify-center'>
<Loading />
</div>
)
}
{
!isLoading && !!data.length && (
<AuthForm
ref={formRef}
formSchemas={formSchemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
)
}
</Modal>
)
}
export default memo(ApiKeyModal)

View File

@@ -0,0 +1,104 @@
import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import AddOAuthButton from './add-oauth-button'
import type { AddOAuthButtonProps } from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
import type { AddApiKeyButtonProps } from './add-api-key-button'
import type { PluginPayload } from '../types'
type AuthorizeProps = {
pluginPayload: PluginPayload
theme?: 'primary' | 'secondary'
showDivider?: boolean
canOAuth?: boolean
canApiKey?: boolean
disabled?: boolean
onUpdate?: () => void
}
const Authorize = ({
pluginPayload,
theme = 'primary',
showDivider = true,
canOAuth,
canApiKey,
disabled,
onUpdate,
}: AuthorizeProps) => {
const { t } = useTranslation()
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
buttonVariant: 'secondary',
className: 'hover:bg-components-button-secondary-bg',
buttonLeftClassName: 'hover:bg-components-button-secondary-bg-hover',
buttonRightClassName: 'hover:bg-components-button-secondary-bg-hover',
dividerClassName: 'bg-divider-regular opacity-100',
pluginPayload,
}
}
return {
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
pluginPayload,
}
}, [canApiKey, theme, pluginPayload, t])
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
pluginPayload,
buttonVariant: 'secondary',
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
}
}
return {
pluginPayload,
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
buttonVariant: !canOAuth ? 'primary' : 'secondary-accent',
}
}, [canOAuth, theme, pluginPayload, t])
return (
<>
<div className='flex items-center space-x-1.5'>
{
canOAuth && (
<div className='min-w-0 flex-[1]'>
<AddOAuthButton
{...oAuthButtonProps}
disabled={disabled}
onUpdate={onUpdate}
/>
</div>
)
}
{
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 && (
<div className='min-w-0 flex-[1]'>
<AddApiKeyButton
{...apiKeyButtonProps}
disabled={disabled}
onUpdate={onUpdate}
/>
</div>
)
}
</div>
</>
)
}
export default memo(Authorize)

View File

@@ -0,0 +1,188 @@
import {
memo,
useCallback,
useRef,
useState,
} from 'react'
import { RiExternalLinkLine } from '@remixicon/react'
import {
useForm,
useStore,
} from '@tanstack/react-form'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal/modal'
import {
useDeletePluginOAuthCustomClientHook,
useInvalidPluginOAuthClientSchemaHook,
useSetPluginOAuthCustomClientHook,
} from '../hooks/use-credential'
import type { PluginPayload } from '../types'
import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type {
FormRefObject,
FormSchema,
} from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import { useRenderI18nObject } from '@/hooks/use-i18n'
type OAuthClientSettingsProps = {
pluginPayload: PluginPayload
onClose?: () => void
editValues?: Record<string, any>
disabled?: boolean
schemas: FormSchema[]
onAuth?: () => Promise<void>
hasOriginalClientParams?: boolean
onUpdate?: () => void
}
const OAuthClientSettings = ({
pluginPayload,
onClose,
editValues,
disabled,
schemas,
onAuth,
hasOriginalClientParams,
onUpdate,
}: OAuthClientSettingsProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const [doingAction, setDoingAction] = useState(false)
const doingActionRef = useRef(doingAction)
const handleSetDoingAction = useCallback((value: boolean) => {
doingActionRef.current = value
setDoingAction(value)
}, [])
const defaultValues = schemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload)
const invalidPluginOAuthClientSchema = useInvalidPluginOAuthClientSchemaHook(pluginPayload)
const formRef = useRef<FormRefObject>(null)
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
try {
const {
isCheckValidated,
values,
} = formRef.current?.getFormValues({
needCheckValidatedValues: true,
needTransformWhenSecretFieldIsPristine: true,
}) || { isCheckValidated: false, values: {} }
if (!isCheckValidated)
throw new Error('error')
const {
__oauth_client__,
...restValues
} = values
handleSetDoingAction(true)
await setPluginOAuthCustomClient({
client_params: restValues,
enable_oauth_custom_client: __oauth_client__ === 'custom',
})
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
onUpdate?.()
invalidPluginOAuthClientSchema()
}
finally {
handleSetDoingAction(false)
}
}, [onClose, onUpdate, invalidPluginOAuthClientSchema, setPluginOAuthCustomClient, notify, t, handleSetDoingAction])
const handleConfirmAndAuthorize = useCallback(async () => {
await handleConfirm()
if (onAuth)
await onAuth()
}, [handleConfirm, onAuth])
const { mutateAsync: deletePluginOAuthCustomClient } = useDeletePluginOAuthCustomClientHook(pluginPayload)
const handleRemove = useCallback(async () => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await deletePluginOAuthCustomClient()
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
onUpdate?.()
invalidPluginOAuthClientSchema()
}
finally {
handleSetDoingAction(false)
}
}, [onUpdate, invalidPluginOAuthClientSchema, deletePluginOAuthCustomClient, notify, t, handleSetDoingAction, onClose])
const form = useForm({
defaultValues: editValues || defaultValues,
})
const __oauth_client__ = useStore(form.store, s => s.values.__oauth_client__)
const helpField = schemas.find(schema => schema.url && schema.help)
const renderI18nObject = useRenderI18nObject()
return (
<Modal
title={t('plugin.auth.oauthClientSettings')}
confirmButtonText={t('plugin.auth.saveAndAuth')}
cancelButtonText={t('plugin.auth.saveOnly')}
extraButtonText={t('common.operation.cancel')}
showExtraButton
extraButtonVariant='secondary'
onExtraButtonClick={onClose}
onClose={onClose}
onCancel={handleConfirm}
onConfirm={handleConfirmAndAuthorize}
disabled={disabled || doingAction}
footerSlot={
__oauth_client__ === 'custom' && hasOriginalClientParams && (
<div className='grow'>
<Button
variant='secondary'
className='text-components-button-destructive-secondary-text'
disabled={disabled || doingAction || !editValues}
onClick={handleRemove}
>
{t('common.operation.remove')}
</Button>
</div>
)
}
>
<>
<AuthForm
formFromProps={form}
ref={formRef}
formSchemas={schemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
{
helpField && __oauth_client__ === 'custom' && (
<a
className='system-xs-regular mt-4 flex items-center text-text-accent'
href={helpField?.url}
target='_blank'
>
<span className='break-all'>
{renderI18nObject(helpField?.help as any)}
</span>
<RiExternalLinkLine className='ml-1 h-3 w-3' />
</a>
)}
</>
</Modal>
)
}
export default memo(OAuthClientSettings)

View File

@@ -0,0 +1,113 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
import type {
Credential,
PluginPayload,
} from './types'
import {
Authorized,
usePluginAuth,
} from '.'
type AuthorizedInNodeProps = {
pluginPayload: PluginPayload
onAuthorizationItemClick: (id: string) => void
credentialId?: string
}
const AuthorizedInNode = ({
pluginPayload,
onAuthorizationItemClick,
credentialId,
}: AuthorizedInNodeProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const {
canApiKey,
canOAuth,
credentials,
disabled,
invalidPluginCredentialInfo,
} = usePluginAuth(pluginPayload, isOpen || !!credentialId)
const renderTrigger = useCallback((open?: 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
size='small'
className={cn(
open && !removed && 'bg-components-button-ghost-bg-hover',
removed && 'bg-transparent text-text-destructive',
)}
>
<Indicator
className='mr-1.5'
color={removed ? 'red' : 'green'}
/>
{label}
<RiArrowDownSLine
className={cn(
'h-3.5 w-3.5 text-components-button-ghost-text',
removed && 'text-text-destructive',
)}
/>
</Button>
)
}, [credentialId, credentials, t])
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,
])
return (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
offset={4}
placement='bottom-end'
triggerPopupSameWidth={false}
popupClassName='w-[360px]'
disabled={disabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
selectedCredentialId={credentialId || '__workspace_default__'}
onUpdate={invalidPluginCredentialInfo}
/>
)
}
export default memo(AuthorizedInNode)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,123 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { RiArrowDownSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Authorize from './authorize'
import Authorized from './authorized'
import type {
Credential,
PluginPayload,
} from './types'
import { usePluginAuth } from './hooks/use-plugin-auth'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
type PluginAuthInAgentProps = {
pluginPayload: PluginPayload
credentialId?: string
onAuthorizationItemClick?: (id: string) => void
}
const PluginAuthInAgent = ({
pluginPayload,
credentialId,
onAuthorizationItemClick,
}: PluginAuthInAgentProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const {
isAuthorized,
canOAuth,
canApiKey,
credentials,
disabled,
invalidPluginCredentialInfo,
} = 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}
onUpdate={invalidPluginCredentialInfo}
/>
)
}
{
isAuthorized && (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
selectedCredentialId={credentialId || '__workspace_default__'}
onUpdate={invalidPluginCredentialInfo}
/>
)
}
</>
)
}
export default memo(PluginAuthInAgent)

View File

@@ -0,0 +1,59 @@
import { memo } from 'react'
import Authorize from './authorize'
import Authorized from './authorized'
import type { PluginPayload } from './types'
import { usePluginAuth } from './hooks/use-plugin-auth'
import cn from '@/utils/classnames'
type PluginAuthProps = {
pluginPayload: PluginPayload
children?: React.ReactNode
className?: string
}
const PluginAuth = ({
pluginPayload,
children,
className,
}: PluginAuthProps) => {
const {
isAuthorized,
canOAuth,
canApiKey,
credentials,
disabled,
invalidPluginCredentialInfo,
} = usePluginAuth(pluginPayload, !!pluginPayload.provider)
return (
<div className={cn(!isAuthorized && className)}>
{
!isAuthorized && (
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
/>
)
}
{
isAuthorized && !children && (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
/>
)
}
{
isAuthorized && children
}
</div>
)
}
export default memo(PluginAuth)

View File

@@ -0,0 +1,25 @@
export enum AuthCategory {
tool = 'tool',
datasource = 'datasource',
model = 'model',
}
export type PluginPayload = {
category: AuthCategory
provider: string
}
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
}

View File

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

View File

@@ -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>
)
}

View File

@@ -36,6 +36,9 @@ 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 { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { useAllToolProviders } from '@/service/use-tools'
const i18nPrefix = 'plugin.action'
@@ -68,7 +71,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
@@ -262,7 +272,17 @@ const DetailHeader = ({
</ActionButton>
</div>
</div>
<Description className='mt-3' text={description[locale]} descriptionLineRows={2}></Description>
<Description className='mb-2 mt-3 h-auto' text={description[locale]} descriptionLineRows={2}></Description>
{
category === PluginType.tool && (
<PluginAuth
pluginPayload={{
provider: provider?.name || '',
category: AuthCategory.tool,
}}
/>
)
}
{isShowPluginInfo && (
<PluginInfo
repository={isFromGitHub ? meta?.repo : ''}

View File

@@ -3,9 +3,6 @@ import type { FC } from 'react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Link from 'next/link'
import {
RiArrowLeftLine,
} from '@remixicon/react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@@ -15,24 +12,17 @@ import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selec
import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
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 Divider from '@/app/components/base/divider'
import TabSlider from '@/app/components/base/tab-slider-plain'
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useAppContext } from '@/context/app-context'
import {
useAllBuiltInTools,
useAllCustomTools,
useAllMCPTools,
useAllWorkflowTools,
useInvalidateAllBuiltInTools,
useUpdateProviderCredentials,
} from '@/service/use-tools'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
@@ -46,6 +36,10 @@ import { MARKETPLACE_API_PREFIX } from '@/config'
import type { Node } from 'reactflow'
import type { NodeOutPutVar } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import {
AuthCategory,
PluginAuthInAgent,
} from '@/app/components/plugins/plugin-auth'
type Props = {
disabled?: boolean
@@ -196,23 +190,6 @@ const ToolSelector: FC<Props> = ({
} 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
const currentTool = useMemo(() => {
return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
@@ -226,6 +203,12 @@ const ToolSelector: FC<Props> = ({
invalidateAllBuiltinTools()
invalidateInstalledPluginList()
}
const handleAuthorizationItemClick = (id: string) => {
onSelect({
...value,
credential_id: id,
} as any)
}
return (
<>
@@ -264,7 +247,6 @@ const ToolSelector: FC<Props> = ({
onSwitchChange={handleEnabledChange}
onDelete={onDelete}
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
onAuth={() => setShowSettingAuth(true)}
uninstalled={!currentProvider && inMarketPlace}
versionMismatch={currentProvider && inMarketPlace && !currentTool}
installInfo={manifest?.latest_package_identifier}
@@ -284,171 +266,131 @@ const ToolSelector: FC<Props> = ({
)}
</PortalToFollowElemTrigger>
<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')}>
{!isShowSettingAuth && (
<>
<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 */}
<div className='flex flex-col gap-3 px-4 py-2'>
<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>
<ToolPicker
placement='bottom'
offset={offset}
trigger={
<ToolTrigger
open={panelShowState || isShowChooseTool}
value={value}
provider={currentProvider}
/>
}
isShow={panelShowState || isShowChooseTool}
onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool}
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
onSelectMultiple={handleSelectMultipleTool}
scope={scope}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</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'>
{!currentProvider.is_team_authorization && (
<Button
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>
</>
)}
{/* tool settings */}
{(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
<>
<Divider className='my-1 w-full' />
{/* tabs */}
{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 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')}>
<>
<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 */}
<div className='flex flex-col gap-3 px-4 py-2'>
<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>
<ToolPicker
placement='bottom'
offset={offset}
trigger={
<ToolTrigger
open={panelShowState || isShowChooseTool}
value={value}
provider={currentProvider}
/>
)}
{nodeId && showTabSlider && currType === 'params' && (
<div className='px-4 py-2'>
}
isShow={panelShowState || isShowChooseTool}
onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool}
disabled={false}
supportAddCustomTool
onSelect={handleSelectTool}
onSelectMultiple={handleSelectMultipleTool}
scope={scope}
selectedTools={selectedTools}
canChooseMCPTool={canChooseMCPTool}
/>
</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>
</>
)}
{/* tool settings */}
{(currentToolSettings.length > 0 || currentToolParams.length > 0) && currentProvider?.is_team_authorization && (
<>
<Divider className='my-1 w-full' />
{/* tabs */}
{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')! },
]}
/>
)}
{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.paramsTip2')}</div>
</div>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className='p-4 pb-1'>
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.settings')}</div>
</div>
)}
{/* reasoning config only */}
{nodeId && reasoningConfigOnly && (
<div className='mb-1 p-4 pb-1'>
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.params')}</div>
<div className='pb-1'>
<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>
)}
{/* user settings only */}
{userSettingsOnly && (
<div className='p-4 pb-1'>
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.settings')}</div>
</div>
)}
{/* reasoning config only */}
{nodeId && reasoningConfigOnly && (
<div className='mb-1 p-4 pb-1'>
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.params')}</div>
<div className='pb-1'>
<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>
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<ToolForm
inPanel
readOnly={false}
nodeId={nodeId}
schema={settingsFormSchemas as any}
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
/>
</div>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
</div>
)}
{/* user settings form */}
{(currType === 'settings' || userSettingsOnly) && (
<div className='px-4 py-2'>
<ToolForm
inPanel
readOnly={false}
nodeId={nodeId}
schema={settingsFormSchemas as any}
value={getPlainValue(value?.settings || {})}
onChange={handleSettingsFormChange}
/>
)}
</>
)}
</>
)}
{/* authorization panel */}
{isShowSettingAuth && currentProvider && (
<>
<div className='relative flex flex-col gap-1 pt-3.5'>
<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>
<div
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>
)}
{/* reasoning config form */}
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
<ReasoningConfigForm
value={value?.parameters || {}}
onChange={handleParamsFormChange}
schemas={paramsFormSchemas as any}
nodeOutputVars={nodeOutputVars}
availableNodes={availableNodes}
nodeId={nodeId}
/>
)}
</>
)}
</>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@@ -30,7 +30,6 @@ type Props = {
onSwitchChange?: (value: boolean) => void
onDelete?: () => void
noAuth?: boolean
onAuth?: () => void
isError?: boolean
errorTip?: any
uninstalled?: boolean
@@ -38,6 +37,7 @@ type Props = {
onInstall?: () => void
versionMismatch?: boolean
open: boolean
authRemoved?: boolean
canChooseMCPTool?: boolean,
}
@@ -53,13 +53,13 @@ const ToolItem = ({
onSwitchChange,
onDelete,
noAuth,
onAuth,
uninstalled,
installInfo,
onInstall,
isError,
errorTip,
versionMismatch,
authRemoved,
canChooseMCPTool,
}: Props) => {
const { t } = useTranslation()
@@ -125,11 +125,17 @@ const ToolItem = ({
<McpToolNotSupportTooltip />
)}
{!isError && !uninstalled && !versionMismatch && noAuth && (
<Button variant='secondary' size='small' onClick={onAuth}>
<Button variant='secondary' size='small'>
{t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' />
</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 && (
<div onClick={e => e.stopPropagation()}>
<SwitchPluginVersion

View File

@@ -33,6 +33,7 @@ export type ToolDefaultValue = {
params: Record<string, any>
paramSchemas: Record<string, any>[]
output_schema: Record<string, any>
credential_id?: string
meta?: PluginMeta
}
@@ -46,4 +47,5 @@ export type ToolValue = {
parameters?: Record<string, any>
enabled?: boolean
extra?: Record<string, any>
credential_id?: string
}

View File

@@ -59,6 +59,12 @@ import { useLogs } from '@/app/components/workflow/run/hooks'
import PanelWrap from '../before-run-form/panel-wrap'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import {
AuthorizedInNode,
PluginAuth,
} from '@/app/components/plugins/plugin-auth'
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { canFindTool } from '@/utils'
type BasePanelProps = {
children: ReactNode
@@ -221,6 +227,22 @@ const BasePanel: FC<BasePanelProps> = ({
return {}
})()
const buildInTools = useStore(s => s.buildInTools)
const currCollection = useMemo(() => {
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
const showPluginAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currCollection?.allow_delete
}, [currCollection, data.type])
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
handleNodeDataUpdateWithSyncDraft({
id,
data: {
credential_id,
},
})
}, [handleNodeDataUpdateWithSyncDraft, id])
if(logParams.showSpecialResultPanel) {
return (
<div className={cn(
@@ -353,12 +375,42 @@ const BasePanel: FC<BasePanelProps> = ({
onChange={handleDescriptionChange}
/>
</div>
<div className='pl-4'>
<Tab
value={tabType}
onChange={setTabType}
/>
</div>
{
showPluginAuth && (
<PluginAuth
className='px-4 pb-2'
pluginPayload={{
provider: currCollection?.name || '',
category: AuthCategory.tool,
}}
>
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
onChange={setTabType}
/>
<AuthorizedInNode
pluginPayload={{
provider: currCollection?.name || '',
category: AuthCategory.tool,
}}
onAuthorizationItemClick={handleAuthorizationItemClick}
credentialId={data.credential_id}
/>
</div>
</PluginAuth>
)
}
{
!showPluginAuth && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
onChange={setTabType}
/>
</div>
)
}
<Split />
</div>

View File

@@ -5,10 +5,8 @@ import Split from '../_base/components/split'
import type { ToolNodeType } from './types'
import useConfig from './use-config'
import ToolForm from './components/tool-form'
import Button from '@/app/components/base/button'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import type { NodePanelProps } from '@/app/components/workflow/types'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
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'
@@ -32,10 +30,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
setToolSettingValue,
currCollection,
isShowAuthBtn,
showSetAuth,
showSetAuthModal,
hideSetAuthModal,
handleSaveAuth,
isLoading,
outputSchema,
hasObjectOutput,
@@ -52,19 +46,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
return (
<div className='pt-2'>
{!readOnly && isShowAuthBtn && (
<>
<div className='px-4'>
<Button
variant='primary'
className='w-full'
onClick={showSetAuthModal}
>
{t(`${i18nPrefix}.authorize`)}
</Button>
</div>
</>
)}
{!isShowAuthBtn && (
<div className='relative'>
{toolInputVarSchema.length > 0 && (
@@ -109,15 +90,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
</div>
)}
{showSetAuth && (
<ConfigCredential
collection={currCollection!}
onCancel={hideSetAuthModal}
onSaved={handleSaveAuth}
isHideRemoveBtn
/>
)}
<div>
<OutputVars>
<>

View File

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