diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index a1b82ab2fe..b4711ea39a 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -18,7 +18,6 @@ import AppIcon from '@/app/components/base/app-icon' import Button from '@/app/components/base/button' import Indicator from '@/app/components/header/indicator' import Switch from '@/app/components/base/switch' -import Toast from '@/app/components/base/toast' import ConfigContext from '@/context/debug-configuration' import type { AgentTool } from '@/types/app' import { type Collection, CollectionType } from '@/app/components/tools/types' @@ -26,8 +25,6 @@ import { MAX_TOOLS_NUM } from '@/config' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import Tooltip from '@/app/components/base/tooltip' import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other' -import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' -import { updateBuiltInToolCredential } from '@/service/tools' import cn from '@/utils/classnames' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types' @@ -57,13 +54,7 @@ const AgentTools: FC = () => { const formattingChangedDispatcher = useFormattingChangedDispatcher() const [currentTool, setCurrentTool] = useState(null) - const currentCollection = useMemo(() => { - if (!currentTool) return null - const collection = collectionList.find(collection => canFindTool(collection.id, currentTool?.provider_id) && collection.type === currentTool?.provider_type) - return collection - }, [currentTool, collectionList]) const [isShowSettingTool, setIsShowSettingTool] = useState(false) - const [isShowSettingAuth, setShowSettingAuth] = useState(false) const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => { const collection = collectionList.find( collection => @@ -100,17 +91,6 @@ const AgentTools: FC = () => { formattingChangedDispatcher() } - const handleToolAuthSetting = (value: AgentToolWithMoreInfo) => { - const newModelConfig = produce(modelConfig, (draft) => { - const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === value?.collection?.id && item.tool_name === value?.tool_name) - if (tool) - (tool as AgentTool).notAuthor = false - }) - setModelConfig(newModelConfig) - setIsShowSettingTool(false) - formattingChangedDispatcher() - } - const [isDeleting, setIsDeleting] = useState(-1) const getToolValue = (tool: ToolDefaultValue) => { return { @@ -144,6 +124,20 @@ const AgentTools: FC = () => { return item.provider_name } + const handleAuthorizationItemClick = useCallback((credentialId: string) => { + const newModelConfig = produce(modelConfig, (draft) => { + const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === currentTool?.provider_id) + if (tool) + (tool as AgentTool).credential_id = credentialId + }) + setCurrentTool({ + ...currentTool, + credential_id: credentialId, + } as any) + setModelConfig(newModelConfig) + formattingChangedDispatcher() + }, [currentTool, modelConfig, setModelConfig, formattingChangedDispatcher]) + return ( <> { {item.notAuthor && ( +
+ + ) + } + + + + + {bottomSlot} + + + + ) +} + +export default memo(Modal) diff --git a/web/app/components/base/select/pure.tsx b/web/app/components/base/select/pure.tsx index 81cc2fbadf..be88c936fd 100644 --- a/web/app/components/base/select/pure.tsx +++ b/web/app/components/base/select/pure.tsx @@ -39,6 +39,9 @@ type PureSelectProps = { itemClassName?: string title?: string }, + placeholder?: string + disabled?: boolean + triggerPopupSameWidth?: boolean } const PureSelect = ({ options, @@ -47,6 +50,9 @@ const PureSelect = ({ containerProps, triggerProps, popupProps, + placeholder, + disabled, + triggerPopupSameWidth, }: PureSelectProps) => { const { t } = useTranslation() const { @@ -74,7 +80,7 @@ const PureSelect = ({ }, [onOpenChange]) const selectedOption = options.find(option => option.value === value) - const triggerText = selectedOption?.label || t('common.placeholder.select') + const triggerText = selectedOption?.label || placeholder || t('common.placeholder.select') return ( handleOpenChange(!mergedOpen)} @@ -135,6 +142,7 @@ const PureSelect = ({ )} title={option.label} onClick={() => { + if (disabled) return onChange?.(option.value) handleOpenChange(false) }} diff --git a/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx new file mode 100644 index 0000000000..295fc4fa9d --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx @@ -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 ( + <> + + { + isApiKeyModalOpen && ( + setIsApiKeyModalOpen(false)} + onUpdate={onUpdate} + /> + ) + } + + + ) +} + +export default memo(AddApiKeyButton) diff --git a/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx new file mode 100644 index 0000000000..5cb474f1bb --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx @@ -0,0 +1,262 @@ +import { + memo, + useCallback, + useMemo, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { IS_CE_EDITION } from '@/config' +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 || Object.keys(client_params || {}).length > 0 + const handleOAuth = useCallback(async () => { + const { authorization_url } = await getPluginOAuthUrl() + + if (authorization_url) { + openOAuthPopup( + authorization_url, + () => onUpdate?.(), + ) + } + }, [getPluginOAuthUrl, onUpdate]) + + const renderCustomLabel = useCallback((item: FormSchema) => { + return ( +
+
+
+ +
+
+
+ {t('plugin.auth.clientInfo')} +
+ { + redirect_uri && ( +
+
{redirect_uri}
+ { + navigator.clipboard.writeText(redirect_uri || '') + }} + > + + +
+ ) + } +
+
+
+ {renderI18nObject(item.label as Record)} + { + item.required && ( + * + ) + } +
+
+ ) + }, [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_CE_EDITION) + return 'custom' + + return 'default' + } + else { + if (is_oauth_custom_client_enabled) + return 'custom' + + return 'default' + } + }, [isConfigured, is_oauth_custom_client_enabled]) + + return ( + <> + { + isConfigured && ( + + ) + } + { + !isConfigured && ( + + ) + } + { + isOAuthSettingsOpen && ( + 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) diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx new file mode 100644 index 0000000000..d582c660b6 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -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 + 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) + 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(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 ( + + + {renderI18nObject(helpField?.help as any)} + + + + ) + } + bottomSlot={ +
+ + {t('common.modelProvider.encrypted.front')} + + PKCS1_OAEP + + {t('common.modelProvider.encrypted.back')} +
+ } + onConfirm={handleConfirm} + showExtraButton={!!editValues} + onExtraButtonClick={onRemove} + disabled={disabled || isLoading || doingAction} + > + { + isLoading && ( +
+ +
+ ) + } + { + !isLoading && !!data.length && ( + + ) + } +
+ ) +} + +export default memo(ApiKeyModal) diff --git a/web/app/components/plugins/plugin-auth/authorize/index.tsx b/web/app/components/plugins/plugin-auth/authorize/index.tsx new file mode 100644 index 0000000000..f430d8d48e --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/index.tsx @@ -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 ( + <> +
+ { + canOAuth && ( +
+ +
+ ) + } + { + showDivider && canOAuth && canApiKey && ( +
+
+ or +
+
+ ) + } + { + canApiKey && ( +
+ +
+ ) + } +
+ + ) +} + +export default memo(Authorize) diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx new file mode 100644 index 0000000000..14c7ed957f --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx @@ -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 + disabled?: boolean + schemas: FormSchema[] + onAuth?: () => Promise + 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) + const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload) + const invalidPluginOAuthClientSchema = useInvalidPluginOAuthClientSchemaHook(pluginPayload) + const formRef = useRef(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 ( + + + + ) + } + > + <> + + { + helpField && __oauth_client__ === 'custom' && ( + + + {renderI18nObject(helpField?.help as any)} + + + + )} + + + ) +} + +export default memo(OAuthClientSettings) diff --git a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx new file mode 100644 index 0000000000..79189fa585 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx @@ -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 ( + + ) + }, [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 ( + + ) +} + +export default memo(AuthorizedInNode) diff --git a/web/app/components/plugins/plugin-auth/authorized/index.tsx b/web/app/components/plugins/plugin-auth/authorized/index.tsx new file mode 100644 index 0000000000..ac771afdd3 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized/index.tsx @@ -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(null) + const [deleteCredentialId, setDeleteCredentialId] = useState(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 | null>(null) + const handleEdit = useCallback((id: string, values: Record) => { + 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 ( + <> + + setMergedIsOpen(!mergedIsOpen)} + asChild + > + { + renderTrigger + ? renderTrigger(mergedIsOpen) + : ( + + ) + } + + +
+
+ { + !!extraAuthorizationItems?.length && ( +
+ { + extraAuthorizationItems.map(credential => ( + + )) + } +
+ ) + } + { + !!oAuthCredentials.length && ( +
+
+ OAuth +
+ { + oAuthCredentials.map(credential => ( + + )) + } +
+ ) + } + { + !!apiKeyCredentials.length && ( +
+
+ API Keys +
+ { + apiKeyCredentials.map(credential => ( + + )) + } +
+ ) + } +
+
+
+ +
+
+
+
+ { + deleteCredentialId && ( + + ) + } + { + !!editValues && ( + { + setEditValues(null) + pendingOperationCredentialId.current = null + }} + onRemove={handleRemove} + disabled={disabled || doingAction} + onUpdate={onUpdate} + /> + ) + } + + ) +} + +export default memo(Authorized) diff --git a/web/app/components/plugins/plugin-auth/authorized/item.tsx b/web/app/components/plugins/plugin-auth/authorized/item.tsx new file mode 100644 index 0000000000..5508bcc324 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -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) => 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 ( +
onItemClick?.(credential.id === '__workspace_default__' ? '' : credential.id)} + > + { + renaming && ( +
+ setRenameValue(e.target.value)} + placeholder={t('common.placeholder.input')} + onClick={e => e.stopPropagation()} + /> + + +
+ ) + } + { + !renaming && ( +
+ { + showSelectedIcon && ( +
+ { + selectedCredentialId === credential.id && ( + + ) + } +
+ ) + } + +
+ {credential.name} +
+ { + credential.is_default && ( + + {t('plugin.auth.default')} + + ) + } +
+ ) + } + { + showAction && !renaming && ( +
+ { + !credential.is_default && !disableSetDefault && ( + + ) + } + { + !disableRename && ( + + { + e.stopPropagation() + setRenaming(true) + setRenameValue(credential.name) + }} + > + + + + ) + } + { + !isOAuth && !disableEdit && ( + + { + e.stopPropagation() + onEdit?.( + credential.id, + { + ...credential.credentials, + __name__: credential.name, + __credential_id__: credential.id, + }, + ) + }} + > + + + + ) + } + { + !disableDelete && ( + + { + e.stopPropagation() + onDelete?.(credential.id) + }} + > + + + + ) + } +
+ ) + } +
+ ) +} + +export default memo(Item) diff --git a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts new file mode 100644 index 0000000000..5a7a497ad9 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts @@ -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) +} diff --git a/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts b/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts new file mode 100644 index 0000000000..14199ddc4d --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts @@ -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: '', + } +} diff --git a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts new file mode 100644 index 0000000000..e449a4bb65 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts @@ -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, + } +} diff --git a/web/app/components/plugins/plugin-auth/index.tsx b/web/app/components/plugins/plugin-auth/index.tsx new file mode 100644 index 0000000000..e4f6ae8b2f --- /dev/null +++ b/web/app/components/plugins/plugin-auth/index.tsx @@ -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' diff --git a/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx b/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx new file mode 100644 index 0000000000..f3557f3d6f --- /dev/null +++ b/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx @@ -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 ( + + ) + }, [credentialId, credentials, t]) + + return ( + <> + { + !isAuthorized && ( + + ) + } + { + isAuthorized && ( + + ) + } + + ) +} + +export default memo(PluginAuthInAgent) diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx new file mode 100644 index 0000000000..76b405a750 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -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 ( +
+ { + !isAuthorized && ( + + ) + } + { + isAuthorized && !children && ( + + ) + } + { + isAuthorized && children + } +
+ ) +} + +export default memo(PluginAuth) diff --git a/web/app/components/plugins/plugin-auth/types.ts b/web/app/components/plugins/plugin-auth/types.ts new file mode 100644 index 0000000000..ad41733bde --- /dev/null +++ b/web/app/components/plugins/plugin-auth/types.ts @@ -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 + isWorkspaceDefault?: boolean +} diff --git a/web/app/components/plugins/plugin-auth/utils.ts b/web/app/components/plugins/plugin-auth/utils.ts new file mode 100644 index 0000000000..d264cfb198 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/utils.ts @@ -0,0 +1,10 @@ +export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record) => { + const transformedValues: Record = { ...values } + + isPristineSecretInputNames.forEach((name) => { + if (transformedValues[name]) + transformedValues[name] = '[__HIDDEN__]' + }) + + return transformedValues +} diff --git a/web/app/components/plugins/plugin-detail-panel/action-list.tsx b/web/app/components/plugins/plugin-detail-panel/action-list.tsx index 2505b6d5aa..040c728630 100644 --- a/web/app/components/plugins/plugin-detail-panel/action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/action-list.tsx @@ -1,17 +1,9 @@ -import React, { useMemo, useState } from 'react' +import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import { useAppContext } from '@/context/app-context' -import Button from '@/app/components/base/button' -import Toast from '@/app/components/base/toast' -import Indicator from '@/app/components/header/indicator' import ToolItem from '@/app/components/tools/provider/tool-item' -import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import { useAllToolProviders, useBuiltinTools, - useInvalidateAllToolProviders, - useRemoveProviderCredentials, - useUpdateProviderCredentials, } from '@/service/use-tools' import type { PluginDetail } from '@/app/components/plugins/types' @@ -23,35 +15,14 @@ const ActionList = ({ detail, }: Props) => { const { t } = useTranslation() - const { isCurrentWorkspaceManager } = useAppContext() const providerBriefInfo = detail.declaration.tool.identity const providerKey = `${detail.plugin_id}/${providerBriefInfo.name}` const { data: collectionList = [] } = useAllToolProviders() - const invalidateAllToolProviders = useInvalidateAllToolProviders() const provider = useMemo(() => { return collectionList.find(collection => collection.name === providerKey) }, [collectionList, providerKey]) const { data } = useBuiltinTools(providerKey) - const [showSettingAuth, setShowSettingAuth] = useState(false) - - const handleCredentialSettingUpdate = () => { - invalidateAllToolProviders() - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - setShowSettingAuth(false) - } - - const { mutate: updatePermission, isPending } = useUpdateProviderCredentials({ - onSuccess: handleCredentialSettingUpdate, - }) - - const { mutate: removePermission } = useRemoveProviderCredentials({ - onSuccess: handleCredentialSettingUpdate, - }) - if (!data || !provider) return null @@ -60,26 +31,7 @@ const ActionList = ({
{t('plugin.detailPanel.actionNum', { num: data.length, action: data.length > 1 ? 'actions' : 'action' })} - {provider.is_team_authorization && provider.allow_delete && ( - - )}
- {!provider.is_team_authorization && provider.allow_delete && ( - - )}
{data.map(tool => ( @@ -93,18 +45,6 @@ const ActionList = ({ /> ))}
- {showSettingAuth && ( - setShowSettingAuth(false)} - onSaved={async value => updatePermission({ - providerName: provider.name, - credentials: value, - })} - onRemove={async () => removePermission(provider.name)} - isSaving={isPending} - /> - )} ) } diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx index 0a5a8b87d6..124e133c2b 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -36,6 +36,9 @@ import { useInvalidateAllToolProviders } from '@/service/use-tools' import { API_PREFIX } from '@/config' import cn from '@/utils/classnames' import { getMarketplaceUrl } from '@/utils/var' +import { PluginAuth } from '@/app/components/plugins/plugin-auth' +import { AuthCategory } from '@/app/components/plugins/plugin-auth' +import { useAllToolProviders } from '@/service/use-tools' const i18nPrefix = 'plugin.action' @@ -68,7 +71,14 @@ const DetailHeader = ({ meta, plugin_id, } = detail - const { author, category, name, label, description, icon, verified } = detail.declaration + const { author, category, name, label, description, icon, verified, tool } = detail.declaration + const isTool = category === PluginType.tool + const providerBriefInfo = tool?.identity + const providerKey = `${plugin_id}/${providerBriefInfo?.name}` + const { data: collectionList = [] } = useAllToolProviders(isTool) + const provider = useMemo(() => { + return collectionList.find(collection => collection.name === providerKey) + }, [collectionList, providerKey]) const isFromGitHub = source === PluginSource.github const isFromMarketplace = source === PluginSource.marketplace @@ -262,7 +272,17 @@ const DetailHeader = ({ - + + { + category === PluginType.tool && ( + + ) + } {isShowPluginInfo && ( = ({ } as any) } - // authorization - const { isCurrentWorkspaceManager } = useAppContext() - const [isShowSettingAuth, setShowSettingAuth] = useState(false) - const handleCredentialSettingUpdate = () => { - invalidateAllBuiltinTools() - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - setShowSettingAuth(false) - onShowChange(false) - } - - const { mutate: updatePermission } = useUpdateProviderCredentials({ - onSuccess: handleCredentialSettingUpdate, - }) - // install from marketplace const currentTool = useMemo(() => { return currentProvider?.tools.find(tool => tool.name === value?.tool_name) @@ -226,6 +203,12 @@ const ToolSelector: FC = ({ invalidateAllBuiltinTools() invalidateInstalledPluginList() } + const handleAuthorizationItemClick = (id: string) => { + onSelect({ + ...value, + credential_id: id, + } as any) + } return ( <> @@ -264,7 +247,6 @@ const ToolSelector: FC = ({ onSwitchChange={handleEnabledChange} onDelete={onDelete} noAuth={currentProvider && currentTool && !currentProvider.is_team_authorization} - onAuth={() => setShowSettingAuth(true)} uninstalled={!currentProvider && inMarketPlace} versionMismatch={currentProvider && inMarketPlace && !currentTool} installInfo={manifest?.latest_package_identifier} @@ -284,171 +266,131 @@ const ToolSelector: FC = ({ )}
-
- {!isShowSettingAuth && ( - <> -
{t(`plugin.detailPanel.toolSelector.${isEdit ? 'toolSetting' : 'title'}`)}
- {/* base form */} -
-
-
{t('plugin.detailPanel.toolSelector.toolLabel')}
- - } - isShow={panelShowState || isShowChooseTool} - onShowChange={trigger ? onPanelShowStateChange as any : setIsShowChooseTool} - disabled={false} - supportAddCustomTool - onSelect={handleSelectTool} - onSelectMultiple={handleSelectMultipleTool} - scope={scope} - selectedTools={selectedTools} - canChooseMCPTool={canChooseMCPTool} - /> -
-
-
{t('plugin.detailPanel.toolSelector.descriptionLabel')}
-