mirror of
https://github.com/langgenius/dify.git
synced 2025-12-20 14:42:37 +00:00
Compare commits
32 Commits
feat/conta
...
feat/tool-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364e0f1450 | ||
|
|
25e0013db9 | ||
|
|
75d8cb4978 | ||
|
|
52e7bb1848 | ||
|
|
686b4b8e0e | ||
|
|
3f65117007 | ||
|
|
0dcdfd6de3 | ||
|
|
e1bb6c00df | ||
|
|
67b5d73399 | ||
|
|
e4cf6a497b | ||
|
|
498d8ab33c | ||
|
|
a923087b57 | ||
|
|
3f273cae73 | ||
|
|
964e29b27f | ||
|
|
d0ba7adf33 | ||
|
|
2572e99a4b | ||
|
|
fd0a8d5834 | ||
|
|
29035d333d | ||
|
|
3b7df2f9b6 | ||
|
|
580c9a668f | ||
|
|
83ab69d2eb | ||
|
|
119d41099d | ||
|
|
cb0082c0b8 | ||
|
|
90f800408d | ||
|
|
5869d6aacc | ||
|
|
18699f8671 | ||
|
|
bdf5af7a6f | ||
|
|
bda76080a9 | ||
|
|
8968a3e254 | ||
|
|
ce8bf7b5a2 | ||
|
|
0f1be60daa | ||
|
|
c53d5c105b |
@@ -18,7 +18,6 @@ import AppIcon from '@/app/components/base/app-icon'
|
|||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Indicator from '@/app/components/header/indicator'
|
import Indicator from '@/app/components/header/indicator'
|
||||||
import Switch from '@/app/components/base/switch'
|
import Switch from '@/app/components/base/switch'
|
||||||
import Toast from '@/app/components/base/toast'
|
|
||||||
import ConfigContext from '@/context/debug-configuration'
|
import ConfigContext from '@/context/debug-configuration'
|
||||||
import type { AgentTool } from '@/types/app'
|
import type { AgentTool } from '@/types/app'
|
||||||
import { type Collection, CollectionType } from '@/app/components/tools/types'
|
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 { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
|
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 cn from '@/utils/classnames'
|
||||||
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
||||||
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
|
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
|
||||||
@@ -57,13 +54,7 @@ const AgentTools: FC = () => {
|
|||||||
|
|
||||||
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
const formattingChangedDispatcher = useFormattingChangedDispatcher()
|
||||||
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
|
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 [isShowSettingTool, setIsShowSettingTool] = useState(false)
|
||||||
const [isShowSettingAuth, setShowSettingAuth] = useState(false)
|
|
||||||
const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => {
|
const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => {
|
||||||
const collection = collectionList.find(
|
const collection = collectionList.find(
|
||||||
collection =>
|
collection =>
|
||||||
@@ -100,17 +91,6 @@ const AgentTools: FC = () => {
|
|||||||
formattingChangedDispatcher()
|
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 [isDeleting, setIsDeleting] = useState<number>(-1)
|
||||||
const getToolValue = (tool: ToolDefaultValue) => {
|
const getToolValue = (tool: ToolDefaultValue) => {
|
||||||
return {
|
return {
|
||||||
@@ -144,6 +124,20 @@ const AgentTools: FC = () => {
|
|||||||
return item.provider_name
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel
|
<Panel
|
||||||
@@ -302,7 +296,7 @@ const AgentTools: FC = () => {
|
|||||||
{item.notAuthor && (
|
{item.notAuthor && (
|
||||||
<Button variant='secondary' size='small' onClick={() => {
|
<Button variant='secondary' size='small' onClick={() => {
|
||||||
setCurrentTool(item)
|
setCurrentTool(item)
|
||||||
setShowSettingAuth(true)
|
setIsShowSettingTool(true)
|
||||||
}}>
|
}}>
|
||||||
{t('tools.notAuthorized')}
|
{t('tools.notAuthorized')}
|
||||||
<Indicator className='ml-2' color='orange' />
|
<Indicator className='ml-2' color='orange' />
|
||||||
@@ -322,21 +316,8 @@ const AgentTools: FC = () => {
|
|||||||
isModel={currentTool?.collection?.type === CollectionType.model}
|
isModel={currentTool?.collection?.type === CollectionType.model}
|
||||||
onSave={handleToolSettingChange}
|
onSave={handleToolSettingChange}
|
||||||
onHide={() => setIsShowSettingTool(false)}
|
onHide={() => setIsShowSettingTool(false)}
|
||||||
/>
|
credentialId={currentTool?.credential_id}
|
||||||
)}
|
onAuthorizationItemClick={handleAuthorizationItemClick}
|
||||||
{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)
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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 OrgInfo from '@/app/components/plugins/card/base/org-info'
|
||||||
import Description from '@/app/components/plugins/card/base/description'
|
import Description from '@/app/components/plugins/card/base/description'
|
||||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||||
|
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
|
||||||
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
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 { getLanguage } from '@/i18n/language'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||||
|
import {
|
||||||
|
AuthCategory,
|
||||||
|
PluginAuthInAgent,
|
||||||
|
} from '@/app/components/plugins/plugin-auth'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
showBackButton?: boolean
|
showBackButton?: boolean
|
||||||
@@ -36,6 +39,8 @@ type Props = {
|
|||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
onHide: () => void
|
onHide: () => void
|
||||||
onSave?: (value: Record<string, any>) => void
|
onSave?: (value: Record<string, any>) => void
|
||||||
|
credentialId?: string
|
||||||
|
onAuthorizationItemClick?: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SettingBuiltInTool: FC<Props> = ({
|
const SettingBuiltInTool: FC<Props> = ({
|
||||||
@@ -48,6 +53,8 @@ const SettingBuiltInTool: FC<Props> = ({
|
|||||||
readonly,
|
readonly,
|
||||||
onHide,
|
onHide,
|
||||||
onSave,
|
onSave,
|
||||||
|
credentialId,
|
||||||
|
onAuthorizationItemClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { locale } = useContext(I18n)
|
const { locale } = useContext(I18n)
|
||||||
const language = getLanguage(locale)
|
const language = getLanguage(locale)
|
||||||
@@ -197,8 +204,20 @@ const SettingBuiltInTool: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='system-md-semibold mt-1 text-text-primary'>{currTool?.label[language]}</div>
|
<div className='system-md-semibold mt-1 text-text-primary'>{currTool?.label[language]}</div>
|
||||||
{!!currTool?.description[language] && (
|
{!!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>
|
</div>
|
||||||
{/* form */}
|
{/* form */}
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const Question: FC<QuestionProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mb-2 flex justify-end last:mb-0'>
|
<div className='mb-2 flex justify-end last:mb-0'>
|
||||||
<div className={cn('group relative mr-4 flex max-w-full items-start pl-14 overflow-x-hidden', isEditing && 'flex-1')}>
|
<div className={cn('group relative mr-4 flex max-w-full items-start overflow-x-hidden pl-14', isEditing && 'flex-1')}>
|
||||||
<div className={cn('mr-2 gap-1', isEditing ? 'hidden' : 'flex')}>
|
<div className={cn('mr-2 gap-1', isEditing ? 'hidden' : 'flex')}>
|
||||||
<div
|
<div
|
||||||
className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex"
|
className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm group-hover:flex"
|
||||||
@@ -117,7 +117,7 @@ const Question: FC<QuestionProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
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) : {}}
|
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
|
itemClassName?: string
|
||||||
title?: string
|
title?: string
|
||||||
},
|
},
|
||||||
|
placeholder?: string
|
||||||
|
disabled?: boolean
|
||||||
|
triggerPopupSameWidth?: boolean
|
||||||
}
|
}
|
||||||
const PureSelect = ({
|
const PureSelect = ({
|
||||||
options,
|
options,
|
||||||
@@ -47,6 +50,9 @@ const PureSelect = ({
|
|||||||
containerProps,
|
containerProps,
|
||||||
triggerProps,
|
triggerProps,
|
||||||
popupProps,
|
popupProps,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
triggerPopupSameWidth,
|
||||||
}: PureSelectProps) => {
|
}: PureSelectProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
@@ -74,7 +80,7 @@ const PureSelect = ({
|
|||||||
}, [onOpenChange])
|
}, [onOpenChange])
|
||||||
|
|
||||||
const selectedOption = options.find(option => option.value === value)
|
const selectedOption = options.find(option => option.value === value)
|
||||||
const triggerText = selectedOption?.label || t('common.placeholder.select')
|
const triggerText = selectedOption?.label || placeholder || t('common.placeholder.select')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PortalToFollowElem
|
<PortalToFollowElem
|
||||||
@@ -82,6 +88,7 @@ const PureSelect = ({
|
|||||||
offset={offset || 4}
|
offset={offset || 4}
|
||||||
open={mergedOpen}
|
open={mergedOpen}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
|
triggerPopupSameWidth={triggerPopupSameWidth}
|
||||||
>
|
>
|
||||||
<PortalToFollowElemTrigger
|
<PortalToFollowElemTrigger
|
||||||
onClick={() => handleOpenChange(!mergedOpen)}
|
onClick={() => handleOpenChange(!mergedOpen)}
|
||||||
@@ -135,6 +142,7 @@ const PureSelect = ({
|
|||||||
)}
|
)}
|
||||||
title={option.label}
|
title={option.label}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (disabled) return
|
||||||
onChange?.(option.value)
|
onChange?.(option.value)
|
||||||
handleOpenChange(false)
|
handleOpenChange(false)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 { 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 ToolItem from '@/app/components/tools/provider/tool-item'
|
||||||
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
|
|
||||||
import {
|
import {
|
||||||
useAllToolProviders,
|
useAllToolProviders,
|
||||||
useBuiltinTools,
|
useBuiltinTools,
|
||||||
useInvalidateAllToolProviders,
|
|
||||||
useRemoveProviderCredentials,
|
|
||||||
useUpdateProviderCredentials,
|
|
||||||
} from '@/service/use-tools'
|
} from '@/service/use-tools'
|
||||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||||
|
|
||||||
@@ -23,35 +15,14 @@ const ActionList = ({
|
|||||||
detail,
|
detail,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isCurrentWorkspaceManager } = useAppContext()
|
|
||||||
const providerBriefInfo = detail.declaration.tool.identity
|
const providerBriefInfo = detail.declaration.tool.identity
|
||||||
const providerKey = `${detail.plugin_id}/${providerBriefInfo.name}`
|
const providerKey = `${detail.plugin_id}/${providerBriefInfo.name}`
|
||||||
const { data: collectionList = [] } = useAllToolProviders()
|
const { data: collectionList = [] } = useAllToolProviders()
|
||||||
const invalidateAllToolProviders = useInvalidateAllToolProviders()
|
|
||||||
const provider = useMemo(() => {
|
const provider = useMemo(() => {
|
||||||
return collectionList.find(collection => collection.name === providerKey)
|
return collectionList.find(collection => collection.name === providerKey)
|
||||||
}, [collectionList, providerKey])
|
}, [collectionList, providerKey])
|
||||||
const { data } = useBuiltinTools(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)
|
if (!data || !provider)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -60,26 +31,7 @@ const ActionList = ({
|
|||||||
<div className='mb-1 py-1'>
|
<div className='mb-1 py-1'>
|
||||||
<div className='system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary'>
|
<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' })}
|
{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>
|
</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>
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
{data.map(tool => (
|
{data.map(tool => (
|
||||||
@@ -93,18 +45,6 @@ const ActionList = ({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ import { useInvalidateAllToolProviders } from '@/service/use-tools'
|
|||||||
import { API_PREFIX } from '@/config'
|
import { API_PREFIX } from '@/config'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { getMarketplaceUrl } from '@/utils/var'
|
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'
|
const i18nPrefix = 'plugin.action'
|
||||||
|
|
||||||
@@ -68,7 +71,14 @@ const DetailHeader = ({
|
|||||||
meta,
|
meta,
|
||||||
plugin_id,
|
plugin_id,
|
||||||
} = detail
|
} = 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 isFromGitHub = source === PluginSource.github
|
||||||
const isFromMarketplace = source === PluginSource.marketplace
|
const isFromMarketplace = source === PluginSource.marketplace
|
||||||
|
|
||||||
@@ -262,7 +272,17 @@ const DetailHeader = ({
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{isShowPluginInfo && (
|
||||||
<PluginInfo
|
<PluginInfo
|
||||||
repository={isFromGitHub ? meta?.repo : ''}
|
repository={isFromGitHub ? meta?.repo : ''}
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ import type { FC } from 'react'
|
|||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {
|
|
||||||
RiArrowLeftLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
PortalToFollowElemContent,
|
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 ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item'
|
||||||
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
|
||||||
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
|
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 Textarea from '@/app/components/base/textarea'
|
||||||
import Divider from '@/app/components/base/divider'
|
import Divider from '@/app/components/base/divider'
|
||||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||||
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
|
import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form'
|
||||||
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||||
|
|
||||||
import { useAppContext } from '@/context/app-context'
|
|
||||||
import {
|
import {
|
||||||
useAllBuiltInTools,
|
useAllBuiltInTools,
|
||||||
useAllCustomTools,
|
useAllCustomTools,
|
||||||
useAllMCPTools,
|
useAllMCPTools,
|
||||||
useAllWorkflowTools,
|
useAllWorkflowTools,
|
||||||
useInvalidateAllBuiltInTools,
|
useInvalidateAllBuiltInTools,
|
||||||
useUpdateProviderCredentials,
|
|
||||||
} from '@/service/use-tools'
|
} from '@/service/use-tools'
|
||||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||||
import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
|
import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks'
|
||||||
@@ -46,6 +36,10 @@ import { MARKETPLACE_API_PREFIX } from '@/config'
|
|||||||
import type { Node } from 'reactflow'
|
import type { Node } from 'reactflow'
|
||||||
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
import type { NodeOutPutVar } from '@/app/components/workflow/types'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import {
|
||||||
|
AuthCategory,
|
||||||
|
PluginAuthInAgent,
|
||||||
|
} from '@/app/components/plugins/plugin-auth'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
@@ -196,23 +190,6 @@ const ToolSelector: FC<Props> = ({
|
|||||||
} as any)
|
} as any)
|
||||||
}
|
}
|
||||||
|
|
||||||
// authorization
|
|
||||||
const { isCurrentWorkspaceManager } = useAppContext()
|
|
||||||
const [isShowSettingAuth, setShowSettingAuth] = useState(false)
|
|
||||||
const handleCredentialSettingUpdate = () => {
|
|
||||||
invalidateAllBuiltinTools()
|
|
||||||
Toast.notify({
|
|
||||||
type: 'success',
|
|
||||||
message: t('common.api.actionSuccess'),
|
|
||||||
})
|
|
||||||
setShowSettingAuth(false)
|
|
||||||
onShowChange(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { mutate: updatePermission } = useUpdateProviderCredentials({
|
|
||||||
onSuccess: handleCredentialSettingUpdate,
|
|
||||||
})
|
|
||||||
|
|
||||||
// install from marketplace
|
// install from marketplace
|
||||||
const currentTool = useMemo(() => {
|
const currentTool = useMemo(() => {
|
||||||
return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
|
return currentProvider?.tools.find(tool => tool.name === value?.tool_name)
|
||||||
@@ -226,6 +203,12 @@ const ToolSelector: FC<Props> = ({
|
|||||||
invalidateAllBuiltinTools()
|
invalidateAllBuiltinTools()
|
||||||
invalidateInstalledPluginList()
|
invalidateInstalledPluginList()
|
||||||
}
|
}
|
||||||
|
const handleAuthorizationItemClick = (id: string) => {
|
||||||
|
onSelect({
|
||||||
|
...value,
|
||||||
|
credential_id: id,
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -264,7 +247,6 @@ const ToolSelector: FC<Props> = ({
|
|||||||
onSwitchChange={handleEnabledChange}
|
onSwitchChange={handleEnabledChange}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
|
noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization}
|
||||||
onAuth={() => setShowSettingAuth(true)}
|
|
||||||
uninstalled={!currentProvider && inMarketPlace}
|
uninstalled={!currentProvider && inMarketPlace}
|
||||||
versionMismatch={currentProvider && inMarketPlace && !currentTool}
|
versionMismatch={currentProvider && inMarketPlace && !currentTool}
|
||||||
installInfo={manifest?.latest_package_identifier}
|
installInfo={manifest?.latest_package_identifier}
|
||||||
@@ -284,171 +266,131 @@ const ToolSelector: FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</PortalToFollowElemTrigger>
|
</PortalToFollowElemTrigger>
|
||||||
<PortalToFollowElemContent>
|
<PortalToFollowElemContent>
|
||||||
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', !isShowSettingAuth && 'overflow-y-auto pb-2')}>
|
<div className={cn('relative max-h-[642px] min-h-20 w-[361px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pb-4 shadow-lg backdrop-blur-sm', 'overflow-y-auto pb-2')}>
|
||||||
{!isShowSettingAuth && (
|
<>
|
||||||
<>
|
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div>
|
||||||
<div className='system-xl-semibold px-4 pb-1 pt-3.5 text-text-primary'>{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}</div>
|
{/* base form */}
|
||||||
{/* base form */}
|
<div className='flex flex-col gap-3 px-4 py-2'>
|
||||||
<div className='flex flex-col gap-3 px-4 py-2'>
|
<div className='flex flex-col gap-1'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
|
||||||
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>{t('plugin.detailPanel.toolSelector.toolLabel')}</div>
|
<ToolPicker
|
||||||
<ToolPicker
|
placement='bottom'
|
||||||
placement='bottom'
|
offset={offset}
|
||||||
offset={offset}
|
trigger={
|
||||||
trigger={
|
<ToolTrigger
|
||||||
<ToolTrigger
|
open={panelShowState || isShowChooseTool}
|
||||||
open={panelShowState || isShowChooseTool}
|
value={value}
|
||||||
value={value}
|
provider={currentProvider}
|
||||||
provider={currentProvider}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
isShow={panelShowState || isShowChooseTool}
|
|
||||||
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')! },
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
)}
|
}
|
||||||
{nodeId && showTabSlider && currType === 'params' && (
|
isShow={panelShowState || isShowChooseTool}
|
||||||
<div className='px-4 py-2'>
|
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.paramsTip1')}</div>
|
||||||
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
|
<div className='system-xs-regular text-text-tertiary'>{t('plugin.detailPanel.toolSelector.paramsTip2')}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
{/* user settings only */}
|
)}
|
||||||
{userSettingsOnly && (
|
{/* user settings form */}
|
||||||
<div className='p-4 pb-1'>
|
{(currType === 'settings' || userSettingsOnly) && (
|
||||||
<div className='system-sm-semibold-uppercase text-text-primary'>{t('plugin.detailPanel.toolSelector.settings')}</div>
|
<div className='px-4 py-2'>
|
||||||
</div>
|
<ToolForm
|
||||||
)}
|
inPanel
|
||||||
{/* reasoning config only */}
|
readOnly={false}
|
||||||
{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}
|
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
|
schema={settingsFormSchemas as any}
|
||||||
|
value={getPlainValue(value?.settings || {})}
|
||||||
|
onChange={handleSettingsFormChange}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
{/* reasoning config form */}
|
||||||
</>
|
{nodeId && (currType === 'params' || reasoningConfigOnly) && (
|
||||||
)}
|
<ReasoningConfigForm
|
||||||
{/* authorization panel */}
|
value={value?.parameters || {}}
|
||||||
{isShowSettingAuth && currentProvider && (
|
onChange={handleParamsFormChange}
|
||||||
<>
|
schemas={paramsFormSchemas as any}
|
||||||
<div className='relative flex flex-col gap-1 pt-3.5'>
|
nodeOutputVars={nodeOutputVars}
|
||||||
<div className='absolute -top-2 left-2 w-[345px] rounded-t-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur pt-2 backdrop-blur-sm'></div>
|
availableNodes={availableNodes}
|
||||||
<div
|
nodeId={nodeId}
|
||||||
className='system-xs-semibold-uppercase flex h-6 cursor-pointer items-center gap-1 px-3 text-text-accent-secondary'
|
/>
|
||||||
onClick={() => setShowSettingAuth(false)}
|
)}
|
||||||
>
|
</>
|
||||||
<RiArrowLeftLine className='h-4 w-4' />
|
)}
|
||||||
BACK
|
</>
|
||||||
</div>
|
|
||||||
<div className='system-xl-semibold px-4 text-text-primary'>{t('tools.auth.setupModalTitle')}</div>
|
|
||||||
<div className='system-xs-regular px-4 text-text-tertiary'>{t('tools.auth.setupModalTitleDescription')}</div>
|
|
||||||
</div>
|
|
||||||
<ToolCredentialForm
|
|
||||||
collection={currentProvider}
|
|
||||||
onCancel={() => setShowSettingAuth(false)}
|
|
||||||
onSaved={async value => updatePermission({
|
|
||||||
providerName: currentProvider.name,
|
|
||||||
credentials: value,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</PortalToFollowElemContent>
|
</PortalToFollowElemContent>
|
||||||
</PortalToFollowElem>
|
</PortalToFollowElem>
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ type Props = {
|
|||||||
onSwitchChange?: (value: boolean) => void
|
onSwitchChange?: (value: boolean) => void
|
||||||
onDelete?: () => void
|
onDelete?: () => void
|
||||||
noAuth?: boolean
|
noAuth?: boolean
|
||||||
onAuth?: () => void
|
|
||||||
isError?: boolean
|
isError?: boolean
|
||||||
errorTip?: any
|
errorTip?: any
|
||||||
uninstalled?: boolean
|
uninstalled?: boolean
|
||||||
@@ -38,6 +37,7 @@ type Props = {
|
|||||||
onInstall?: () => void
|
onInstall?: () => void
|
||||||
versionMismatch?: boolean
|
versionMismatch?: boolean
|
||||||
open: boolean
|
open: boolean
|
||||||
|
authRemoved?: boolean
|
||||||
canChooseMCPTool?: boolean,
|
canChooseMCPTool?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,13 +53,13 @@ const ToolItem = ({
|
|||||||
onSwitchChange,
|
onSwitchChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
noAuth,
|
noAuth,
|
||||||
onAuth,
|
|
||||||
uninstalled,
|
uninstalled,
|
||||||
installInfo,
|
installInfo,
|
||||||
onInstall,
|
onInstall,
|
||||||
isError,
|
isError,
|
||||||
errorTip,
|
errorTip,
|
||||||
versionMismatch,
|
versionMismatch,
|
||||||
|
authRemoved,
|
||||||
canChooseMCPTool,
|
canChooseMCPTool,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -125,11 +125,17 @@ const ToolItem = ({
|
|||||||
<McpToolNotSupportTooltip />
|
<McpToolNotSupportTooltip />
|
||||||
)}
|
)}
|
||||||
{!isError && !uninstalled && !versionMismatch && noAuth && (
|
{!isError && !uninstalled && !versionMismatch && noAuth && (
|
||||||
<Button variant='secondary' size='small' onClick={onAuth}>
|
<Button variant='secondary' size='small'>
|
||||||
{t('tools.notAuthorized')}
|
{t('tools.notAuthorized')}
|
||||||
<Indicator className='ml-2' color='orange' />
|
<Indicator className='ml-2' color='orange' />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{!isError && !uninstalled && !versionMismatch && authRemoved && (
|
||||||
|
<Button variant='secondary' size='small'>
|
||||||
|
{t('plugin.auth.authRemoved')}
|
||||||
|
<Indicator className='ml-2' color='red' />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{!isError && !uninstalled && versionMismatch && installInfo && (
|
{!isError && !uninstalled && versionMismatch && installInfo && (
|
||||||
<div onClick={e => e.stopPropagation()}>
|
<div onClick={e => e.stopPropagation()}>
|
||||||
<SwitchPluginVersion
|
<SwitchPluginVersion
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type ToolDefaultValue = {
|
|||||||
params: Record<string, any>
|
params: Record<string, any>
|
||||||
paramSchemas: Record<string, any>[]
|
paramSchemas: Record<string, any>[]
|
||||||
output_schema: Record<string, any>
|
output_schema: Record<string, any>
|
||||||
|
credential_id?: string
|
||||||
meta?: PluginMeta
|
meta?: PluginMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,4 +47,5 @@ export type ToolValue = {
|
|||||||
parameters?: Record<string, any>
|
parameters?: Record<string, any>
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
extra?: Record<string, any>
|
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 PanelWrap from '../before-run-form/panel-wrap'
|
||||||
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
|
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
|
||||||
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
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 = {
|
type BasePanelProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -221,6 +227,22 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
return {}
|
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) {
|
if(logParams.showSpecialResultPanel) {
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
@@ -353,12 +375,42 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='pl-4'>
|
{
|
||||||
<Tab
|
showPluginAuth && (
|
||||||
value={tabType}
|
<PluginAuth
|
||||||
onChange={setTabType}
|
className='px-4 pb-2'
|
||||||
/>
|
pluginPayload={{
|
||||||
</div>
|
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 />
|
<Split />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'size-5 border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge relative flex items-center justify-center rounded-[6px]',
|
'relative flex size-5 items-center justify-center rounded-[6px] border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge',
|
||||||
)}
|
)}
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
@@ -73,7 +73,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
|||||||
src={icon}
|
src={icon}
|
||||||
alt='tool icon'
|
alt='tool icon'
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full h-full size-3.5 object-cover',
|
'size-3.5 h-full w-full object-cover',
|
||||||
notSuccess && 'opacity-50',
|
notSuccess && 'opacity-50',
|
||||||
)}
|
)}
|
||||||
onError={() => setIconFetchError(true)}
|
onError={() => setIconFetchError(true)}
|
||||||
@@ -82,7 +82,7 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
|||||||
if (typeof icon === 'object') {
|
if (typeof icon === 'object') {
|
||||||
return <AppIcon
|
return <AppIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'w-full h-full size-3.5 object-cover',
|
'size-3.5 h-full w-full object-cover',
|
||||||
notSuccess && 'opacity-50',
|
notSuccess && 'opacity-50',
|
||||||
)}
|
)}
|
||||||
icon={icon?.content}
|
icon={icon?.content}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import Split from '../_base/components/split'
|
|||||||
import type { ToolNodeType } from './types'
|
import type { ToolNodeType } from './types'
|
||||||
import useConfig from './use-config'
|
import useConfig from './use-config'
|
||||||
import ToolForm from './components/tool-form'
|
import ToolForm from './components/tool-form'
|
||||||
import Button from '@/app/components/base/button'
|
|
||||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
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 Loading from '@/app/components/base/loading'
|
||||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||||
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
|
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
|
||||||
@@ -32,10 +30,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
|||||||
setToolSettingValue,
|
setToolSettingValue,
|
||||||
currCollection,
|
currCollection,
|
||||||
isShowAuthBtn,
|
isShowAuthBtn,
|
||||||
showSetAuth,
|
|
||||||
showSetAuthModal,
|
|
||||||
hideSetAuthModal,
|
|
||||||
handleSaveAuth,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
outputSchema,
|
outputSchema,
|
||||||
hasObjectOutput,
|
hasObjectOutput,
|
||||||
@@ -52,19 +46,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='pt-2'>
|
<div className='pt-2'>
|
||||||
{!readOnly && isShowAuthBtn && (
|
|
||||||
<>
|
|
||||||
<div className='px-4'>
|
|
||||||
<Button
|
|
||||||
variant='primary'
|
|
||||||
className='w-full'
|
|
||||||
onClick={showSetAuthModal}
|
|
||||||
>
|
|
||||||
{t(`${i18nPrefix}.authorize`)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isShowAuthBtn && (
|
{!isShowAuthBtn && (
|
||||||
<div className='relative'>
|
<div className='relative'>
|
||||||
{toolInputVarSchema.length > 0 && (
|
{toolInputVarSchema.length > 0 && (
|
||||||
@@ -109,15 +90,6 @@ const Panel: FC<NodePanelProps<ToolNodeType>> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showSetAuth && (
|
|
||||||
<ConfigCredential
|
|
||||||
collection={currCollection!}
|
|
||||||
onCancel={hideSetAuthModal}
|
|
||||||
onSaved={handleSaveAuth}
|
|
||||||
isHideRemoveBtn
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<OutputVars>
|
<OutputVars>
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export type CommonNodeType<T = {}> = {
|
|||||||
error_strategy?: ErrorHandleTypeEnum
|
error_strategy?: ErrorHandleTypeEnum
|
||||||
retry_config?: WorkflowRetryConfig
|
retry_config?: WorkflowRetryConfig
|
||||||
default_value?: DefaultValueForm[]
|
default_value?: DefaultValueForm[]
|
||||||
|
credential_id?: string
|
||||||
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
|
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
|
||||||
|
|
||||||
export type CommonEdgeType = {
|
export type CommonEdgeType = {
|
||||||
|
|||||||
@@ -214,6 +214,29 @@ const translation = {
|
|||||||
requestAPlugin: 'Request a plugin',
|
requestAPlugin: 'Request a plugin',
|
||||||
publishPlugins: 'Publish plugins',
|
publishPlugins: 'Publish plugins',
|
||||||
difyVersionNotCompatible: 'The current Dify version is not compatible with this plugin, please upgrade to the minimum version required: {{minimalDifyVersion}}',
|
difyVersionNotCompatible: 'The current Dify version is not compatible with this plugin, please upgrade to the minimum version required: {{minimalDifyVersion}}',
|
||||||
|
auth: {
|
||||||
|
default: 'Default',
|
||||||
|
custom: 'Custom',
|
||||||
|
setDefault: 'Set as default',
|
||||||
|
useOAuth: 'Use OAuth',
|
||||||
|
useOAuthAuth: 'Use OAuth Authorization',
|
||||||
|
addOAuth: 'Add OAuth',
|
||||||
|
setupOAuth: 'Setup OAuth Client',
|
||||||
|
useApi: 'Use API Key',
|
||||||
|
addApi: 'Add API Key',
|
||||||
|
useApiAuth: 'API Key Authorization Configuration',
|
||||||
|
useApiAuthDesc: 'After configuring credentials, all members within the workspace can use this tool when orchestrating applications.',
|
||||||
|
oauthClientSettings: 'OAuth Client Settings',
|
||||||
|
saveOnly: 'Save only',
|
||||||
|
saveAndAuth: 'Save and Authorize',
|
||||||
|
authorization: 'Authorization',
|
||||||
|
authorizations: 'Authorizations',
|
||||||
|
authorizationName: 'Authorization Name',
|
||||||
|
workspaceDefault: 'Workspace Default',
|
||||||
|
authRemoved: 'Auth removed',
|
||||||
|
clientInfo: 'As no system client secrets found for this tool provider, setup it manually is required, for redirect_uri, please use',
|
||||||
|
oauthClient: 'OAuth Client',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
|||||||
@@ -214,6 +214,29 @@ const translation = {
|
|||||||
requestAPlugin: '申请插件',
|
requestAPlugin: '申请插件',
|
||||||
publishPlugins: '发布插件',
|
publishPlugins: '发布插件',
|
||||||
difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}',
|
difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}',
|
||||||
|
auth: {
|
||||||
|
default: '默认',
|
||||||
|
custom: '自定义',
|
||||||
|
setDefault: '设为默认',
|
||||||
|
useOAuth: '使用 OAuth',
|
||||||
|
useOAuthAuth: '使用 OAuth 授权',
|
||||||
|
addOAuth: '添加 OAuth',
|
||||||
|
setupOAuth: '设置 OAuth 客户端',
|
||||||
|
useApi: '使用 API Key',
|
||||||
|
addApi: '添加 API Key',
|
||||||
|
useApiAuth: 'API Key 授权配置',
|
||||||
|
useApiAuthDesc: '配置凭据后,工作区内的所有成员在编排应用时都可以使用此工具。',
|
||||||
|
oauthClientSettings: 'OAuth 客户端设置',
|
||||||
|
saveOnly: '仅保存',
|
||||||
|
saveAndAuth: '保存并授权',
|
||||||
|
authorization: '凭据',
|
||||||
|
authorizations: '凭据',
|
||||||
|
authorizationName: '凭据名称',
|
||||||
|
workspaceDefault: '工作区默认',
|
||||||
|
authRemoved: '凭据已移除',
|
||||||
|
clientInfo: '由于未找到此工具提供者的系统客户端密钥,因此需要手动设置,对于 redirect_uri,请使用',
|
||||||
|
oauthClient: 'OAuth 客户端',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translation
|
export default translation
|
||||||
|
|||||||
161
web/service/use-plugins-auth.ts
Normal file
161
web/service/use-plugins-auth.ts
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
} from '@tanstack/react-query'
|
||||||
|
import { del, get, post } from './base'
|
||||||
|
import { useInvalid } from './use-base'
|
||||||
|
import type {
|
||||||
|
Credential,
|
||||||
|
CredentialTypeEnum,
|
||||||
|
} from '@/app/components/plugins/plugin-auth/types'
|
||||||
|
import type { FormSchema } from '@/app/components/base/form/types'
|
||||||
|
|
||||||
|
const NAME_SPACE = 'plugins-auth'
|
||||||
|
|
||||||
|
export const useGetPluginCredentialInfo = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
enabled: !!url,
|
||||||
|
queryKey: [NAME_SPACE, 'credential-info', url],
|
||||||
|
queryFn: () => get<{
|
||||||
|
supported_credential_types: string[]
|
||||||
|
credentials: Credential[]
|
||||||
|
is_oauth_custom_client_enabled: boolean
|
||||||
|
}>(url),
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInvalidPluginCredentialInfo = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useInvalid([NAME_SPACE, 'credential-info', url])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetPluginDefaultCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => {
|
||||||
|
return post(url, { body: { id } })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginCredentialList = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'credential-list', url],
|
||||||
|
queryFn: () => get(url),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAddPluginCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: {
|
||||||
|
credentials: Record<string, any>
|
||||||
|
type: CredentialTypeEnum
|
||||||
|
name?: string
|
||||||
|
}) => {
|
||||||
|
return post(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdatePluginCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: {
|
||||||
|
credential_id: string
|
||||||
|
credentials?: Record<string, any>
|
||||||
|
name?: string
|
||||||
|
}) => {
|
||||||
|
return post(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePluginCredential = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: { credential_id: string }) => {
|
||||||
|
return post(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginCredentialSchema = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'credential-schema', url],
|
||||||
|
queryFn: () => get<FormSchema[]>(url),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginOAuthUrl = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: [NAME_SPACE, 'oauth-url', url],
|
||||||
|
mutationFn: () => {
|
||||||
|
return get<
|
||||||
|
{
|
||||||
|
authorization_url: string
|
||||||
|
state: string
|
||||||
|
context_id: string
|
||||||
|
}>(url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetPluginOAuthClientSchema = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [NAME_SPACE, 'oauth-client-schema', url],
|
||||||
|
queryFn: () => get<{
|
||||||
|
schema: FormSchema[]
|
||||||
|
is_oauth_custom_client_enabled: boolean
|
||||||
|
is_system_oauth_params_exists?: boolean
|
||||||
|
client_params?: Record<string, any>
|
||||||
|
redirect_uri?: string
|
||||||
|
}>(url),
|
||||||
|
staleTime: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInvalidPluginOAuthClientSchema = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useInvalid([NAME_SPACE, 'oauth-client-schema', url])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetPluginOAuthCustomClient = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (params: {
|
||||||
|
client_params: Record<string, any>
|
||||||
|
enable_oauth_custom_client: boolean
|
||||||
|
}) => {
|
||||||
|
return post<{ result: string }>(url, { body: params })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeletePluginOAuthCustomClient = (
|
||||||
|
url: string,
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => {
|
||||||
|
return del<{ result: string }>(url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -16,10 +16,11 @@ import {
|
|||||||
const NAME_SPACE = 'tools'
|
const NAME_SPACE = 'tools'
|
||||||
|
|
||||||
const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders']
|
const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders']
|
||||||
export const useAllToolProviders = () => {
|
export const useAllToolProviders = (enabled = true) => {
|
||||||
return useQuery<Collection[]>({
|
return useQuery<Collection[]>({
|
||||||
queryKey: useAllToolProvidersKey,
|
queryKey: useAllToolProvidersKey,
|
||||||
queryFn: () => get<Collection[]>('/workspaces/current/tool-providers'),
|
queryFn: () => get<Collection[]>('/workspaces/current/tool-providers'),
|
||||||
|
enabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export type AgentTool = {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
isDeleted?: boolean
|
isDeleted?: boolean
|
||||||
notAuthor?: boolean
|
notAuthor?: boolean
|
||||||
|
credential_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ToolItem = {
|
export type ToolItem = {
|
||||||
|
|||||||
Reference in New Issue
Block a user