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:
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@@ -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'>
|
||||
|
@@ -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) : {}}
|
||||
>
|
||||
{
|
||||
|
177
web/app/components/base/form/components/base/base-field.tsx
Normal file
177
web/app/components/base/form/components/base/base-field.tsx
Normal 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)
|
115
web/app/components/base/form/components/base/base-form.tsx
Normal file
115
web/app/components/base/form/components/base/base-form.tsx
Normal 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)
|
2
web/app/components/base/form/components/base/index.tsx
Normal file
2
web/app/components/base/form/components/base/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as BaseForm, type BaseFormProps } from './base-form'
|
||||
export { default as BaseField, type BaseFieldProps } from './base-field'
|
23
web/app/components/base/form/form-scenarios/auth/index.tsx
Normal file
23
web/app/components/base/form/form-scenarios/auth/index.tsx
Normal 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)
|
3
web/app/components/base/form/hooks/index.ts
Normal file
3
web/app/components/base/form/hooks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './use-check-validated'
|
||||
export * from './use-get-form-values'
|
||||
export * from './use-get-validators'
|
48
web/app/components/base/form/hooks/use-check-validated.ts
Normal file
48
web/app/components/base/form/hooks/use-check-validated.ts
Normal 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,
|
||||
}
|
||||
}
|
44
web/app/components/base/form/hooks/use-get-form-values.ts
Normal file
44
web/app/components/base/form/hooks/use-get-form-values.ts
Normal 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,
|
||||
}
|
||||
}
|
36
web/app/components/base/form/hooks/use-get-validators.ts
Normal file
36
web/app/components/base/form/hooks/use-get-validators.ts
Normal 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,
|
||||
}
|
||||
}
|
76
web/app/components/base/form/types.ts
Normal file
76
web/app/components/base/form/types.ts
Normal 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>
|
1
web/app/components/base/form/utils/index.ts
Normal file
1
web/app/components/base/form/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './secret-input'
|
29
web/app/components/base/form/utils/secret-input/index.ts
Normal file
29
web/app/components/base/form/utils/secret-input/index.ts
Normal 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)
|
||||
}
|
127
web/app/components/base/modal/modal.tsx
Normal file
127
web/app/components/base/modal/modal.tsx
Normal 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)
|
@@ -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)
|
||||
}}
|
||||
|
@@ -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)
|
@@ -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)
|
@@ -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)
|
104
web/app/components/plugins/plugin-auth/authorize/index.tsx
Normal file
104
web/app/components/plugins/plugin-auth/authorize/index.tsx
Normal 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)
|
@@ -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)
|
113
web/app/components/plugins/plugin-auth/authorized-in-node.tsx
Normal file
113
web/app/components/plugins/plugin-auth/authorized-in-node.tsx
Normal 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)
|
342
web/app/components/plugins/plugin-auth/authorized/index.tsx
Normal file
342
web/app/components/plugins/plugin-auth/authorized/index.tsx
Normal 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}
|
||||
{
|
||||
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)
|
219
web/app/components/plugins/plugin-auth/authorized/item.tsx
Normal file
219
web/app/components/plugins/plugin-auth/authorized/item.tsx
Normal 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)
|
@@ -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)
|
||||
}
|
41
web/app/components/plugins/plugin-auth/hooks/use-get-api.ts
Normal file
41
web/app/components/plugins/plugin-auth/hooks/use-get-api.ts
Normal 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: '',
|
||||
}
|
||||
}
|
@@ -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,
|
||||
}
|
||||
}
|
6
web/app/components/plugins/plugin-auth/index.tsx
Normal file
6
web/app/components/plugins/plugin-auth/index.tsx
Normal 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'
|
123
web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx
Normal file
123
web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx
Normal 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)
|
59
web/app/components/plugins/plugin-auth/plugin-auth.tsx
Normal file
59
web/app/components/plugins/plugin-auth/plugin-auth.tsx
Normal 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)
|
25
web/app/components/plugins/plugin-auth/types.ts
Normal file
25
web/app/components/plugins/plugin-auth/types.ts
Normal 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
|
||||
}
|
10
web/app/components/plugins/plugin-auth/utils.ts
Normal file
10
web/app/components/plugins/plugin-auth/utils.ts
Normal 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
|
||||
}
|
@@ -1,17 +1,9 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import Indicator from '@/app/components/header/indicator'
|
||||
import ToolItem from '@/app/components/tools/provider/tool-item'
|
||||
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
||||
import {
|
||||
useAllToolProviders,
|
||||
useBuiltinTools,
|
||||
useInvalidateAllToolProviders,
|
||||
useRemoveProviderCredentials,
|
||||
useUpdateProviderCredentials,
|
||||
} from '@/service/use-tools'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
|
||||
@@ -23,35 +15,14 @@ const ActionList = ({
|
||||
detail,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const providerBriefInfo = detail.declaration.tool.identity
|
||||
const providerKey = `${detail.plugin_id}/${providerBriefInfo.name}`
|
||||
const { data: collectionList = [] } = useAllToolProviders()
|
||||
const invalidateAllToolProviders = useInvalidateAllToolProviders()
|
||||
const provider = useMemo(() => {
|
||||
return collectionList.find(collection => collection.name === providerKey)
|
||||
}, [collectionList, providerKey])
|
||||
const { data } = useBuiltinTools(providerKey)
|
||||
|
||||
const [showSettingAuth, setShowSettingAuth] = useState(false)
|
||||
|
||||
const handleCredentialSettingUpdate = () => {
|
||||
invalidateAllToolProviders()
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
})
|
||||
setShowSettingAuth(false)
|
||||
}
|
||||
|
||||
const { mutate: updatePermission, isPending } = useUpdateProviderCredentials({
|
||||
onSuccess: handleCredentialSettingUpdate,
|
||||
})
|
||||
|
||||
const { mutate: removePermission } = useRemoveProviderCredentials({
|
||||
onSuccess: handleCredentialSettingUpdate,
|
||||
})
|
||||
|
||||
if (!data || !provider)
|
||||
return null
|
||||
|
||||
@@ -60,26 +31,7 @@ const ActionList = ({
|
||||
<div className='mb-1 py-1'>
|
||||
<div className='system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary'>
|
||||
{t('plugin.detailPanel.actionNum', { num: data.length, action: data.length > 1 ? 'actions' : 'action' })}
|
||||
{provider.is_team_authorization && provider.allow_delete && (
|
||||
<Button
|
||||
variant='secondary'
|
||||
size='small'
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>
|
||||
<Indicator className='mr-2' color={'green'} />
|
||||
{t('tools.auth.authorized')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{!provider.is_team_authorization && provider.allow_delete && (
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-full'
|
||||
onClick={() => setShowSettingAuth(true)}
|
||||
disabled={!isCurrentWorkspaceManager}
|
||||
>{t('workflow.nodes.tool.authorize')}</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-col gap-2'>
|
||||
{data.map(tool => (
|
||||
@@ -93,18 +45,6 @@ const ActionList = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{showSettingAuth && (
|
||||
<ConfigCredential
|
||||
collection={provider}
|
||||
onCancel={() => setShowSettingAuth(false)}
|
||||
onSaved={async value => updatePermission({
|
||||
providerName: provider.name,
|
||||
credentials: value,
|
||||
})}
|
||||
onRemove={async () => removePermission(provider.name)}
|
||||
isSaving={isPending}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -36,6 +36,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 : ''}
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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>
|
||||
<>
|
||||
|
@@ -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 = {
|
||||
|
Reference in New Issue
Block a user