From c53d5c105be2dba1c837eb402abc292f47ac4acf Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Thu, 3 Jul 2025 17:55:52 +0800 Subject: [PATCH 01/25] feat: tool oauth --- web/app/components/base/modal/modal.tsx | 118 +++++++++++++++++ .../tools/tool-auth/add-api-key-button.tsx | 40 ++++++ .../tools/tool-auth/add-oauth-button.tsx | 69 ++++++++++ .../tools/tool-auth/api-key-modal.tsx | 52 ++++++++ .../tools/tool-auth/authorization.tsx | 124 ++++++++++++++++++ web/app/components/tools/tool-auth/index.tsx | 23 ++++ .../tools/tool-auth/oauth-client-settings.tsx | 26 ++++ .../components/workflow/nodes/tool/panel.tsx | 6 + 8 files changed, 458 insertions(+) create mode 100644 web/app/components/base/modal/modal.tsx create mode 100644 web/app/components/tools/tool-auth/add-api-key-button.tsx create mode 100644 web/app/components/tools/tool-auth/add-oauth-button.tsx create mode 100644 web/app/components/tools/tool-auth/api-key-modal.tsx create mode 100644 web/app/components/tools/tool-auth/authorization.tsx create mode 100644 web/app/components/tools/tool-auth/index.tsx create mode 100644 web/app/components/tools/tool-auth/oauth-client-settings.tsx diff --git a/web/app/components/base/modal/modal.tsx b/web/app/components/base/modal/modal.tsx new file mode 100644 index 0000000000..3137e7adcc --- /dev/null +++ b/web/app/components/base/modal/modal.tsx @@ -0,0 +1,118 @@ +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 +} +const Modal = ({ + onClose, + size = 'sm', + title, + subTitle, + children, + confirmButtonText, + onConfirm, + cancelButtonText, + onCancel, + showExtraButton, + extraButtonVariant = 'warning', + extraButtonText, + onExtraButtonClick, + footerSlot, + bottomSlot, +}: ModalProps) => { + const { t } = useTranslation() + + return ( + + +
e.stopPropagation()} + > +
+ {title} + { + subTitle && ( +
+ {subTitle} +
+ ) + } +
+ +
+
+ { + children && ( +
{children}
+ ) + } +
+ {footerSlot} + { + showExtraButton && ( + <> + +
+ + ) + } + + +
+ {bottomSlot} +
+
+
+ ) +} + +export default memo(Modal) diff --git a/web/app/components/tools/tool-auth/add-api-key-button.tsx b/web/app/components/tools/tool-auth/add-api-key-button.tsx new file mode 100644 index 0000000000..21eeed5600 --- /dev/null +++ b/web/app/components/tools/tool-auth/add-api-key-button.tsx @@ -0,0 +1,40 @@ +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' + +type AddApiKeyButtonProps = { + buttonVariant?: ButtonProps['variant'] + buttonText?: string +} +const AddApiKeyButton = ({ + buttonVariant = 'secondary-accent', + buttonText = 'use api key', +}: AddApiKeyButtonProps) => { + const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false) + + return ( + <> + + { + isApiKeyModalOpen && ( + setIsApiKeyModalOpen(false)} + /> + ) + } + + + ) +} + +export default memo(AddApiKeyButton) diff --git a/web/app/components/tools/tool-auth/add-oauth-button.tsx b/web/app/components/tools/tool-auth/add-oauth-button.tsx new file mode 100644 index 0000000000..7e6bc91774 --- /dev/null +++ b/web/app/components/tools/tool-auth/add-oauth-button.tsx @@ -0,0 +1,69 @@ +import { + memo, + useState, +} from 'react' +import { RiEqualizer2Line } 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' + +type AddOAuthButtonProps = { + buttonVariant?: ButtonProps['variant'] + buttonText?: string + className?: string + buttonLeftClassName?: string + buttonRightClassName?: string + dividerClassName?: string +} +const AddOAuthButton = ({ + buttonVariant = 'primary', + buttonText = 'use oauth', + className, + buttonLeftClassName, + buttonRightClassName, + dividerClassName, +}: AddOAuthButtonProps) => { + const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false) + + return ( + <> + + { + isOAuthSettingsOpen && ( + setIsOAuthSettingsOpen(false)} + /> + ) + } + + ) +} + +export default memo(AddOAuthButton) diff --git a/web/app/components/tools/tool-auth/api-key-modal.tsx b/web/app/components/tools/tool-auth/api-key-modal.tsx new file mode 100644 index 0000000000..f4a2332861 --- /dev/null +++ b/web/app/components/tools/tool-auth/api-key-modal.tsx @@ -0,0 +1,52 @@ +import { memo } 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' + +export type ApiKeyModalProps = { + onClose?: () => void +} +const ApiKeyModal = ({ + onClose, +}: ApiKeyModalProps) => { + const { t } = useTranslation() + + return ( + + Get your API Key from OpenAI + + + } + bottomSlot={ +
+ + {t('common.modelProvider.encrypted.front')} + + PKCS1_OAEP + + {t('common.modelProvider.encrypted.back')} +
+ } + > +
oauth
+
+ ) +} + +export default memo(ApiKeyModal) diff --git a/web/app/components/tools/tool-auth/authorization.tsx b/web/app/components/tools/tool-auth/authorization.tsx new file mode 100644 index 0000000000..5d3322d629 --- /dev/null +++ b/web/app/components/tools/tool-auth/authorization.tsx @@ -0,0 +1,124 @@ +import { + memo, + useState, +} from 'react' +import { + RiArrowDownSLine, + RiDeleteBinLine, + RiEditLine, +} from '@remixicon/react' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Button from '@/app/components/base/button' +import Indicator from '@/app/components/header/indicator' +import Badge from '@/app/components/base/badge' +import ActionButton from '@/app/components/base/action-button' +import cn from '@/utils/classnames' +import AddOauthButton from './add-oauth-button' +import AddApiKeyButton from './add-api-key-button' + +const Authorization = () => { + const [isOpen, setIsOpen] = useState(false) + + return ( + + setIsOpen(!isOpen)} + asChild + > + + + +
+
+
+
+ OAuth +
+
+
+ +
+ Auth 1 +
+ + Default + +
+
+ + + + + + +
+
+
+
+
+ API Keys +
+
+
+ +
+ Production +
+
+
+ + 0.2 + + + ENTERPRISE + +
+
+
+
+
+
+ + +
+
+
+
+ ) +} + +export default memo(Authorization) diff --git a/web/app/components/tools/tool-auth/index.tsx b/web/app/components/tools/tool-auth/index.tsx new file mode 100644 index 0000000000..a632029ca2 --- /dev/null +++ b/web/app/components/tools/tool-auth/index.tsx @@ -0,0 +1,23 @@ +import { + memo, +} from 'react' +import AddOAuthButton from './add-oauth-button' +import AddApiKeyButton from './add-api-key-button' + +const ToolAuth = () => { + return ( + <> +
+ +
+
+ or +
+
+ +
+ + ) +} + +export default memo(ToolAuth) diff --git a/web/app/components/tools/tool-auth/oauth-client-settings.tsx b/web/app/components/tools/tool-auth/oauth-client-settings.tsx new file mode 100644 index 0000000000..724351bd57 --- /dev/null +++ b/web/app/components/tools/tool-auth/oauth-client-settings.tsx @@ -0,0 +1,26 @@ +import { memo } from 'react' +import Modal from '@/app/components/base/modal/modal' + +type OAuthClientSettingsProps = { + onClose?: () => void +} +const OAuthClientSettings = ({ + onClose, +}: OAuthClientSettingsProps) => { + return ( + +
oauth
+
+ ) +} + +export default memo(OAuthClientSettings) diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index 038159870e..561a6e8eea 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -14,6 +14,8 @@ import Loading from '@/app/components/base/loading' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show' import { Type } from '../llm/types' +import ToolAuth from '@/app/components/tools/tool-auth' +import Authorization from '@/app/components/tools/tool-auth/authorization' const i18nPrefix = 'workflow.nodes.tool' @@ -53,6 +55,10 @@ const Panel: FC> = ({ return (
+
+ + +
{!readOnly && isShowAuthBtn && ( <>
From 0f1be60daa3423f67d5a00499c2cad7f4198356f Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Tue, 8 Jul 2025 18:20:30 +0800 Subject: [PATCH 02/25] tool oauth --- .../base/form/components/base/base-field.tsx | 76 +++++++ .../base/form/components/base/base-form.tsx | 91 ++++++++ .../base/form/components/base/index.tsx | 2 + .../base/form/form-scenarios/auth/index.tsx | 21 ++ web/app/components/base/form/types.ts | 51 +++++ .../authorize}/add-api-key-button.tsx | 8 +- .../authorize}/add-oauth-button.tsx | 5 +- .../plugin-auth/authorize/api-key-modal.tsx | 109 ++++++++++ .../plugins/plugin-auth/authorize/index.tsx | 91 ++++++++ .../authorize}/oauth-client-settings.tsx | 0 .../plugins/plugin-auth/authorized/index.tsx | 205 ++++++++++++++++++ .../plugins/plugin-auth/authorized/item.tsx | 96 ++++++++ .../components/plugins/plugin-auth/index.tsx | 1 + .../plugins/plugin-auth/plugin-auth.tsx | 47 ++++ .../components/plugins/plugin-auth/types.ts | 13 ++ .../plugin-detail-panel/action-list.tsx | 62 +----- .../plugin-detail-panel/detail-header.tsx | 18 +- .../tools/tool-auth/api-key-modal.tsx | 52 ----- .../tools/tool-auth/authorization.tsx | 124 ----------- web/app/components/tools/tool-auth/index.tsx | 23 -- .../components/workflow/nodes/tool/panel.tsx | 10 +- web/service/use-plugins-auth.ts | 138 ++++++++++++ web/service/use-tools.ts | 3 +- 23 files changed, 978 insertions(+), 268 deletions(-) create mode 100644 web/app/components/base/form/components/base/base-field.tsx create mode 100644 web/app/components/base/form/components/base/base-form.tsx create mode 100644 web/app/components/base/form/components/base/index.tsx create mode 100644 web/app/components/base/form/form-scenarios/auth/index.tsx create mode 100644 web/app/components/base/form/types.ts rename web/app/components/{tools/tool-auth => plugins/plugin-auth/authorize}/add-api-key-button.tsx (83%) rename web/app/components/{tools/tool-auth => plugins/plugin-auth/authorize}/add-oauth-button.tsx (95%) create mode 100644 web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx create mode 100644 web/app/components/plugins/plugin-auth/authorize/index.tsx rename web/app/components/{tools/tool-auth => plugins/plugin-auth/authorize}/oauth-client-settings.tsx (100%) create mode 100644 web/app/components/plugins/plugin-auth/authorized/index.tsx create mode 100644 web/app/components/plugins/plugin-auth/authorized/item.tsx create mode 100644 web/app/components/plugins/plugin-auth/index.tsx create mode 100644 web/app/components/plugins/plugin-auth/plugin-auth.tsx create mode 100644 web/app/components/plugins/plugin-auth/types.ts delete mode 100644 web/app/components/tools/tool-auth/api-key-modal.tsx delete mode 100644 web/app/components/tools/tool-auth/authorization.tsx delete mode 100644 web/app/components/tools/tool-auth/index.tsx create mode 100644 web/service/use-plugins-auth.ts diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx new file mode 100644 index 0000000000..df4c0d18ee --- /dev/null +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -0,0 +1,76 @@ +import { + isValidElement, + memo, + useMemo, +} from 'react' +import type { AnyFieldApi } from '@tanstack/react-form' +import cn from '@/utils/classnames' +import Input from '@/app/components/base/input' +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 +} +const BaseField = ({ + fieldClassName, + labelClassName, + inputContainerClassName, + inputClassName, + formSchema, + field, +}: BaseFieldProps) => { + const renderI18nObject = useRenderI18nObject() + const { + label, + } = formSchema + + const memorizedLabel = useMemo(() => { + if (isValidElement(label)) + return label + + if (typeof label === 'object' && label !== null) + return renderI18nObject(label as Record) + }, [label, renderI18nObject]) + + return ( +
+
+ {memorizedLabel} +
+
+ { + formSchema.type === FormTypeEnum.textInput && ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + ) + } + { + formSchema.type === FormTypeEnum.secretInput && ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + /> + ) + } +
+
+ ) +} + +export default memo(BaseField) diff --git a/web/app/components/base/form/components/base/base-form.tsx b/web/app/components/base/form/components/base/base-form.tsx new file mode 100644 index 0000000000..5d4ca2618d --- /dev/null +++ b/web/app/components/base/form/components/base/base-form.tsx @@ -0,0 +1,91 @@ +import { + memo, + useCallback, + useImperativeHandle, +} from 'react' +import type { + AnyFieldApi, +} 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' + +export type BaseFormProps = { + formSchemas?: FormSchema[] + defaultValues?: Record + formClassName?: string + ref?: FormRef +} & Pick + +const BaseForm = ({ + formSchemas, + defaultValues, + formClassName, + fieldClassName, + labelClassName, + inputContainerClassName, + inputClassName, + ref, +}: BaseFormProps) => { + const form = useForm({ + defaultValues, + }) + + useImperativeHandle(ref, () => { + return { + getFormStore() { + return form.store + }, + } + }, [form]) + + const renderField = useCallback((field: AnyFieldApi) => { + const formSchema = formSchemas?.find(schema => schema.name === field.name) + + if (formSchema) { + return ( + + ) + } + + return null + }, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName]) + + if (!formSchemas?.length) + return null + + return ( +
+ { + formSchemas.map((formSchema) => { + return ( + + {renderField} + + ) + }) + } +
+ ) +} + +export default memo(BaseForm) diff --git a/web/app/components/base/form/components/base/index.tsx b/web/app/components/base/form/components/base/index.tsx new file mode 100644 index 0000000000..0d6f0808ff --- /dev/null +++ b/web/app/components/base/form/components/base/index.tsx @@ -0,0 +1,2 @@ +export { default as BaseForm, type BaseFormProps } from './base-form' +export { default as BaseField, type BaseFieldProps } from './base-field' diff --git a/web/app/components/base/form/form-scenarios/auth/index.tsx b/web/app/components/base/form/form-scenarios/auth/index.tsx new file mode 100644 index 0000000000..5a88f94ac6 --- /dev/null +++ b/web/app/components/base/form/form-scenarios/auth/index.tsx @@ -0,0 +1,21 @@ +import { memo } from 'react' +import { BaseForm } from '../../components/base' +import type { BaseFormProps } from '../../components/base' + +const AuthForm = ({ + formSchemas = [], + defaultValues, + ref, +}: BaseFormProps) => { + return ( + + ) +} + +export default memo(AuthForm) diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts new file mode 100644 index 0000000000..69a4fe7795 --- /dev/null +++ b/web/app/components/base/form/types.ts @@ -0,0 +1,51 @@ +import type { + ForwardedRef, + ReactNode, +} from 'react' +import type { AnyFormApi } from '@tanstack/react-form' + +export type TypeWithI18N = { + 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 FormSchema = { + type: FormTypeEnum + name: string + label: string | ReactNode | TypeWithI18N + required: boolean + default?: any + tooltip?: string | TypeWithI18N + show_on: FormShowOnObject[] + url?: string + scope?: string +} + +export type FormValues = Record + +export type FromRefObject = { + getFormStore: () => AnyFormApi['store'] +} +export type FormRef = ForwardedRef diff --git a/web/app/components/tools/tool-auth/add-api-key-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx similarity index 83% rename from web/app/components/tools/tool-auth/add-api-key-button.tsx rename to web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx index 21eeed5600..e7312168b4 100644 --- a/web/app/components/tools/tool-auth/add-api-key-button.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/add-api-key-button.tsx @@ -6,13 +6,17 @@ import Button from '@/app/components/base/button' import type { ButtonProps } from '@/app/components/base/button' import ApiKeyModal from './api-key-modal' -type AddApiKeyButtonProps = { +export type AddApiKeyButtonProps = { + provider?: string buttonVariant?: ButtonProps['variant'] buttonText?: string + disabled?: boolean } const AddApiKeyButton = ({ + provider = '', buttonVariant = 'secondary-accent', buttonText = 'use api key', + disabled, }: AddApiKeyButtonProps) => { const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false) @@ -22,12 +26,14 @@ const AddApiKeyButton = ({ className='grow' variant={buttonVariant} onClick={() => setIsApiKeyModalOpen(true)} + disabled={disabled} > {buttonText} { isApiKeyModalOpen && ( setIsApiKeyModalOpen(false)} /> ) diff --git a/web/app/components/tools/tool-auth/add-oauth-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx similarity index 95% rename from web/app/components/tools/tool-auth/add-oauth-button.tsx rename to web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx index 7e6bc91774..6f2460b2de 100644 --- a/web/app/components/tools/tool-auth/add-oauth-button.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx @@ -8,13 +8,14 @@ import type { ButtonProps } from '@/app/components/base/button' import OAuthClientSettings from './oauth-client-settings' import cn from '@/utils/classnames' -type AddOAuthButtonProps = { +export type AddOAuthButtonProps = { buttonVariant?: ButtonProps['variant'] buttonText?: string className?: string buttonLeftClassName?: string buttonRightClassName?: string dividerClassName?: string + disabled?: boolean } const AddOAuthButton = ({ buttonVariant = 'primary', @@ -23,6 +24,7 @@ const AddOAuthButton = ({ buttonLeftClassName, buttonRightClassName, dividerClassName, + disabled, }: AddOAuthButtonProps) => { const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false) @@ -34,6 +36,7 @@ const AddOAuthButton = ({ 'grow px-0 py-0 hover:bg-components-button-primary-bg', className, )} + disabled={disabled} >
void + editValues?: Record + onRemove?: () => void +} +const ApiKeyModal = ({ + provider, + onClose, + editValues, + onRemove, +}: ApiKeyModalProps) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const { data } = useGetPluginToolCredentialSchema(provider, CredentialTypeEnum.API_KEY) + const { mutateAsync: addPluginToolCredential } = useAddPluginToolCredential(provider) + const { mutateAsync: updatePluginToolCredential } = useUpdatePluginToolCredential(provider) + const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider) + const formRef = useRef(null) + const handleConfirm = useCallback(async () => { + const store = formRef.current?.getFormStore() + const values = store?.state.values + + if (editValues) { + await updatePluginToolCredential({ + credentials: values, + type: CredentialTypeEnum.API_KEY, + }) + } + else { + await addPluginToolCredential({ + credentials: values, + type: CredentialTypeEnum.API_KEY, + }) + } + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + + onClose?.() + invalidatePluginToolCredentialInfo() + }, [addPluginToolCredential, onClose, invalidatePluginToolCredentialInfo, updatePluginToolCredential, notify, t, editValues]) + + return ( + + Get your API Key from OpenAI + + + } + bottomSlot={ +
+ + {t('common.modelProvider.encrypted.front')} + + PKCS1_OAEP + + {t('common.modelProvider.encrypted.back')} +
+ } + onConfirm={handleConfirm} + showExtraButton={!!editValues} + onExtraButtonClick={onRemove} + > + +
+ ) +} + +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..f40451e7fa --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorize/index.tsx @@ -0,0 +1,91 @@ +import { + memo, + useMemo, +} from 'react' +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' + +type AuthorizeProps = { + provider?: string + theme?: 'primary' | 'secondary' + showDivider?: boolean + canOAuth?: boolean + canApiKey?: boolean + disabled?: boolean +} +const Authorize = ({ + provider = '', + theme = 'primary', + showDivider = true, + canOAuth, + canApiKey, + disabled, +}: AuthorizeProps) => { + const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => { + if (theme === 'secondary') { + return { + buttonText: !canApiKey ? 'Add OAuth Authorization' : 'Add OAuth', + 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', + } + } + + return { + buttonText: !canApiKey ? 'Use OAuth Authorization' : '', + } + }, [canApiKey, theme]) + + const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => { + if (theme === 'secondary') { + return { + provider, + buttonVariant: 'secondary', + buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Add API Key', + } + } + return { + provider, + buttonText: !canOAuth ? 'API Key Authorization Configuration' : '', + buttonVariant: !canOAuth ? 'primary' : 'secondary-accent', + } + }, [canOAuth, theme, provider]) + + return ( + <> +
+ { + canOAuth && ( + + ) + } + { + showDivider && canOAuth && canApiKey && ( +
+
+ or +
+
+ ) + } + { + canApiKey && ( + + ) + } +
+ + ) +} + +export default memo(Authorize) diff --git a/web/app/components/tools/tool-auth/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx similarity index 100% rename from web/app/components/tools/tool-auth/oauth-client-settings.tsx rename to web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx 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..98e90d357f --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized/index.tsx @@ -0,0 +1,205 @@ +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 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 { + useDeletePluginToolCredential, + useInvalidPluginToolCredentialInfo, + useSetPluginToolDefaultCredential, +} from '@/service/use-plugins-auth' +import { useToastContext } from '@/app/components/base/toast' + +type AuthorizedProps = { + provider: string + credentials: Credential[] + canOAuth?: boolean + canApiKey?: boolean + disabled?: boolean +} +const Authorized = ({ + provider, + credentials, + canOAuth, + canApiKey, + disabled, +}: AuthorizedProps) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const [isOpen, setIsOpen] = useState(false) + 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: deletePluginToolCredential } = useDeletePluginToolCredential(provider) + const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider) + const openConfirm = useCallback((credentialId?: string) => { + if (credentialId) + pendingOperationCredentialId.current = credentialId + + setDeleteCredentialId(pendingOperationCredentialId.current) + }, []) + const closeConfirm = useCallback(() => { + setDeleteCredentialId(null) + pendingOperationCredentialId.current = null + }, []) + const handleConfirm = useCallback(async () => { + if (!pendingOperationCredentialId.current) { + setDeleteCredentialId(null) + return + } + + await deletePluginToolCredential({ credential_id: pendingOperationCredentialId.current }) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + invalidatePluginToolCredentialInfo() + setDeleteCredentialId(null) + pendingOperationCredentialId.current = null + }, [deletePluginToolCredential, invalidatePluginToolCredentialInfo, notify, t]) + 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: setPluginToolDefaultCredential } = useSetPluginToolDefaultCredential(provider) + const handleSetDefault = useCallback(async (id: string) => { + await setPluginToolDefaultCredential(id) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + invalidatePluginToolCredentialInfo() + }, [setPluginToolDefaultCredential, invalidatePluginToolCredentialInfo, notify, t]) + + return ( + <> + + setIsOpen(!isOpen)} + asChild + > + + + +
+
+ { + !!oAuthCredentials.length && ( +
+
+ OAuth +
+ { + oAuthCredentials.map(credential => ( + + )) + } +
+ ) + } + { + !!apiKeyCredentials.length && ( +
+
+ API Keys +
+ { + apiKeyCredentials.map(credential => ( + + )) + } +
+ ) + } +
+
+
+ +
+
+
+
+ { + deleteCredentialId && ( + + ) + } + { + !!editValues && ( + { + setEditValues(null) + pendingOperationCredentialId.current = null + }} + onRemove={handleRemove} + /> + ) + } + + ) +} + +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..c32d0a9008 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -0,0 +1,96 @@ +import { + memo, +} from 'react' +import { + 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 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 +} +const Item = ({ + credential, + disabled, + onDelete, + onEdit, + onSetDefault, +}: ItemProps) => { + const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2 + + return ( +
+
+ +
+ {credential.name} +
+ { + credential.is_default && ( + + Default + + ) + } +
+
+ + { + isOAuth && ( + + + + + + ) + } + { + !isOAuth && ( + + onEdit?.(credential.id, credential.credentials)} + > + + + + ) + } + + onDelete?.(credential.id)} + > + + + +
+
+ ) +} + +export default memo(Item) 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..274697e061 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/index.tsx @@ -0,0 +1 @@ +export { default as PluginAuth } from './plugin-auth' 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..045abd2318 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -0,0 +1,47 @@ +import { memo } from 'react' +import Authorize from './authorize' +import Authorized from './authorized' +import { useAppContext } from '@/context/app-context' +import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth' +import { CredentialTypeEnum } from './types' + +type PluginAuthProps = { + provider?: string +} +const PluginAuth = ({ + provider = '', +}: PluginAuthProps) => { + const { data } = useGetPluginToolCredentialInfo(provider) + 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) + + return ( + <> + { + !isAuthorized && ( + + ) + } + { + isAuthorized && ( + + ) + } + + ) +} + +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..b9c63d2965 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/types.ts @@ -0,0 +1,13 @@ +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 +} 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..a1f14e6301 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,8 @@ 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 { useAllToolProviders } from '@/service/use-tools' const i18nPrefix = 'plugin.action' @@ -68,7 +70,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 @@ -263,6 +272,13 @@ const DetailHeader = ({
+ { + category === PluginType.tool && ( + + ) + } {isShowPluginInfo && ( void -} -const ApiKeyModal = ({ - onClose, -}: ApiKeyModalProps) => { - const { t } = useTranslation() - - return ( - - Get your API Key from OpenAI - - - } - bottomSlot={ -
- - {t('common.modelProvider.encrypted.front')} - - PKCS1_OAEP - - {t('common.modelProvider.encrypted.back')} -
- } - > -
oauth
-
- ) -} - -export default memo(ApiKeyModal) diff --git a/web/app/components/tools/tool-auth/authorization.tsx b/web/app/components/tools/tool-auth/authorization.tsx deleted file mode 100644 index 5d3322d629..0000000000 --- a/web/app/components/tools/tool-auth/authorization.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { - memo, - useState, -} from 'react' -import { - RiArrowDownSLine, - RiDeleteBinLine, - RiEditLine, -} from '@remixicon/react' -import { - PortalToFollowElem, - PortalToFollowElemContent, - PortalToFollowElemTrigger, -} from '@/app/components/base/portal-to-follow-elem' -import Button from '@/app/components/base/button' -import Indicator from '@/app/components/header/indicator' -import Badge from '@/app/components/base/badge' -import ActionButton from '@/app/components/base/action-button' -import cn from '@/utils/classnames' -import AddOauthButton from './add-oauth-button' -import AddApiKeyButton from './add-api-key-button' - -const Authorization = () => { - const [isOpen, setIsOpen] = useState(false) - - return ( - - setIsOpen(!isOpen)} - asChild - > - - - -
-
-
-
- OAuth -
-
-
- -
- Auth 1 -
- - Default - -
-
- - - - - - -
-
-
-
-
- API Keys -
-
-
- -
- Production -
-
-
- - 0.2 - - - ENTERPRISE - -
-
-
-
-
-
- - -
-
-
-
- ) -} - -export default memo(Authorization) diff --git a/web/app/components/tools/tool-auth/index.tsx b/web/app/components/tools/tool-auth/index.tsx deleted file mode 100644 index a632029ca2..0000000000 --- a/web/app/components/tools/tool-auth/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { - memo, -} from 'react' -import AddOAuthButton from './add-oauth-button' -import AddApiKeyButton from './add-api-key-button' - -const ToolAuth = () => { - return ( - <> -
- -
-
- or -
-
- -
- - ) -} - -export default memo(ToolAuth) diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index 561a6e8eea..9f6465c008 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -14,8 +14,9 @@ import Loading from '@/app/components/base/loading' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show' import { Type } from '../llm/types' -import ToolAuth from '@/app/components/tools/tool-auth' -import Authorization from '@/app/components/tools/tool-auth/authorization' +import { + PluginAuth, +} from '@/app/components/plugins/plugin-auth' const i18nPrefix = 'workflow.nodes.tool' @@ -56,8 +57,9 @@ const Panel: FC> = ({ return (
- - +
{!readOnly && isShowAuthBtn && ( <> diff --git a/web/service/use-plugins-auth.ts b/web/service/use-plugins-auth.ts new file mode 100644 index 0000000000..86bed7d343 --- /dev/null +++ b/web/service/use-plugins-auth.ts @@ -0,0 +1,138 @@ +import { + useMutation, + useQuery, +} from '@tanstack/react-query' +import { 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 useGetPluginToolCredentialInfo = ( + provider: string, +) => { + return useQuery({ + enabled: !!provider, + queryKey: [NAME_SPACE, 'credential-info', provider], + queryFn: () => get<{ + supported_credential_types: string[] + credentials: Credential[] + is_oauth_custom_client_enabled: boolean + }>(`/workspaces/current/tool-provider/builtin/${provider}/credential/info`), + staleTime: 0, + }) +} + +export const useInvalidPluginToolCredentialInfo = ( + provider: string, +) => { + return useInvalid([NAME_SPACE, 'credential-info', provider]) +} + +export const useSetPluginToolDefaultCredential = ( + provider: string, +) => { + return useMutation({ + mutationFn: (id: string) => { + return post(`/workspaces/current/tool-provider/builtin/${provider}/default-credential`, { body: { id } }) + }, + }) +} + +export const useGetPluginToolCredentialList = ( + provider: string, +) => { + return useQuery({ + queryKey: [NAME_SPACE, 'credential-list', provider], + queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/credentials`), + }) +} + +export const useAddPluginToolCredential = ( + provider: string, +) => { + return useMutation({ + mutationFn: (params: { + credentials: Record + type: CredentialTypeEnum + name?: string + }) => { + return post(`/workspaces/current/tool-provider/builtin/${provider}/add`, { body: params }) + }, + }) +} + +export const useUpdatePluginToolCredential = ( + provider: string, +) => { + return useMutation({ + mutationFn: (params: { + credentials: Record + type: CredentialTypeEnum + name?: string + }) => { + return post(`/workspaces/current/tool-provider/builtin/${provider}/update`, { body: params }) + }, + }) +} + +export const useDeletePluginToolCredential = ( + provider: string, +) => { + return useMutation({ + mutationFn: (params: { credential_id: string }) => { + return post(`/workspaces/current/tool-provider/builtin/${provider}/delete`, { body: params }) + }, + }) +} + +export const useGetPluginToolCredentialSchema = ( + provider: string, + credential_type: CredentialTypeEnum, +) => { + return useQuery({ + queryKey: [NAME_SPACE, 'credential-schema', provider, credential_type], + queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/credential/schema/${credential_type}`), + }) +} + +export const useGetPluginToolOAuthUrl = ( + provider: string, +) => { + return useQuery({ + queryKey: [NAME_SPACE, 'oauth-url', provider], + queryFn: () => get(`oauth/plugin/${provider}/tool/authorization-url`), + }) +} + +export const useGetPluginToolOAuthClientSchema = ( + provider: string, +) => { + return useQuery({ + queryKey: [NAME_SPACE, 'oauth-client-schema', provider], + queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`), + }) +} + +export const useSetPluginToolOAuthCustomClient = ( + provider: string, +) => { + return useMutation({ + mutationFn: (params) => { + return post(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, { body: params }) + }, + }) +} + +export const useGetPluginToolOAuthCustomClientSchema = ( + provider: string, +) => { + return useQuery({ + queryKey: [NAME_SPACE, 'oauth-custom-client-schema', provider], + queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`), + }) +} diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index ceaa4b14b3..9238826ea4 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -14,10 +14,11 @@ import { const NAME_SPACE = 'tools' const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders'] -export const useAllToolProviders = () => { +export const useAllToolProviders = (enabled = true) => { return useQuery({ queryKey: useAllToolProvidersKey, queryFn: () => get('/workspaces/current/tool-providers'), + enabled, }) } From 8968a3e254a6b6d19af23f08c0f5f23458caec59 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 9 Jul 2025 18:28:39 +0800 Subject: [PATCH 03/25] tool oauth --- .../base/form/components/base/base-field.tsx | 19 ++- .../base/form/components/base/base-form.tsx | 13 +- web/app/components/base/form/types.ts | 4 +- web/app/components/base/modal/modal.tsx | 7 +- .../plugin-auth/authorize/api-key-modal.tsx | 50 ++++++- .../plugins/plugin-auth/authorize/index.tsx | 4 +- .../plugin-auth/authorized-in-node.tsx | 94 +++++++++++++ .../plugins/plugin-auth/authorized/index.tsx | 96 +++++++++++--- .../plugins/plugin-auth/authorized/item.tsx | 123 ++++++++++++------ .../components/plugins/plugin-auth/hooks.ts | 20 +++ .../components/plugins/plugin-auth/index.tsx | 3 + .../plugins/plugin-auth/plugin-auth.tsx | 7 +- .../components/plugins/plugin-auth/types.ts | 5 +- .../components/plugins/plugin-auth/utils.ts | 10 ++ .../workflow/block-selector/types.ts | 1 + .../_base/components/workflow-panel/index.tsx | 60 ++++++++- .../components/workflow/nodes/tool/panel.tsx | 37 ------ web/app/components/workflow/types.ts | 2 +- web/service/use-plugins-auth.ts | 1 + 19 files changed, 432 insertions(+), 124 deletions(-) create mode 100644 web/app/components/plugins/plugin-auth/authorized-in-node.tsx create mode 100644 web/app/components/plugins/plugin-auth/hooks.ts create mode 100644 web/app/components/plugins/plugin-auth/utils.ts diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index df4c0d18ee..f997297691 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -4,6 +4,7 @@ import { 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 type { FormSchema } from '@/app/components/base/form/types' @@ -17,6 +18,7 @@ export type BaseFieldProps = { inputClassName?: string formSchema: FormSchema field: AnyFieldApi + disabled?: boolean } const BaseField = ({ fieldClassName, @@ -25,6 +27,7 @@ const BaseField = ({ inputClassName, formSchema, field, + disabled, }: BaseFieldProps) => { const renderI18nObject = useRenderI18nObject() const { @@ -35,9 +38,13 @@ const BaseField = ({ if (isValidElement(label)) return label + if (typeof label === 'string') + return label + if (typeof label === 'object' && label !== null) return renderI18nObject(label as Record) }, [label, renderI18nObject]) + const value = useStore(field.form.store, s => s.values[field.name]) return (
@@ -48,23 +55,27 @@ const BaseField = ({ { formSchema.type === FormTypeEnum.textInput && ( field.handleChange(e.target.value)} onBlur={field.handleBlur} + disabled={disabled} /> ) } { formSchema.type === FormTypeEnum.secretInput && ( field.handleChange(e.target.value)} onBlur={field.handleBlur} + disabled={disabled} /> ) } diff --git a/web/app/components/base/form/components/base/base-form.tsx b/web/app/components/base/form/components/base/base-form.tsx index 5d4ca2618d..3fee80bac1 100644 --- a/web/app/components/base/form/components/base/base-form.tsx +++ b/web/app/components/base/form/components/base/base-form.tsx @@ -24,6 +24,7 @@ export type BaseFormProps = { defaultValues?: Record formClassName?: string ref?: FormRef + disabled?: boolean } & Pick const BaseForm = ({ @@ -35,6 +36,7 @@ const BaseForm = ({ inputContainerClassName, inputClassName, ref, + disabled, }: BaseFormProps) => { const form = useForm({ defaultValues, @@ -42,8 +44,8 @@ const BaseForm = ({ useImperativeHandle(ref, () => { return { - getFormStore() { - return form.store + getForm() { + return form }, } }, [form]) @@ -60,18 +62,21 @@ const BaseForm = ({ labelClassName={labelClassName} inputContainerClassName={inputContainerClassName} inputClassName={inputClassName} + disabled={disabled} /> ) } return null - }, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName]) + }, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled]) if (!formSchemas?.length) return null return ( -
+ { formSchemas.map((formSchema) => { return ( diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index 69a4fe7795..5156477a38 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -38,7 +38,7 @@ export type FormSchema = { required: boolean default?: any tooltip?: string | TypeWithI18N - show_on: FormShowOnObject[] + show_on?: FormShowOnObject[] url?: string scope?: string } @@ -46,6 +46,6 @@ export type FormSchema = { export type FormValues = Record export type FromRefObject = { - getFormStore: () => AnyFormApi['store'] + getForm: () => AnyFormApi } export type FormRef = ForwardedRef diff --git a/web/app/components/base/modal/modal.tsx b/web/app/components/base/modal/modal.tsx index 3137e7adcc..5738704722 100644 --- a/web/app/components/base/modal/modal.tsx +++ b/web/app/components/base/modal/modal.tsx @@ -25,6 +25,7 @@ type ModalProps = { onExtraButtonClick?: () => void footerSlot?: React.ReactNode bottomSlot?: React.ReactNode + disabled?: boolean } const Modal = ({ onClose, @@ -42,13 +43,14 @@ const Modal = ({ onExtraButtonClick, footerSlot, bottomSlot, + disabled, }: ModalProps) => { const { t } = useTranslation() return (
{extraButtonText || t('common.operation.remove')} @@ -97,6 +100,7 @@ const Modal = ({ } @@ -104,6 +108,7 @@ const Modal = ({ className='ml-2' variant='primary' onClick={onConfirm} + disabled={disabled} > {confirmButtonText || t('common.operation.save')} 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 index 7a6826066e..b7aae3c9fd 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, + useMemo, useRef, } from 'react' import { useTranslation } from 'react-i18next' @@ -14,8 +15,10 @@ import { useUpdatePluginToolCredential, } from '@/service/use-plugins-auth' import { CredentialTypeEnum } from '../types' +import { transformFormSchemasSecretInput } from '../utils' import AuthForm from '@/app/components/base/form/form-scenarios/auth' import type { FromRefObject } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' import { useToastContext } from '@/app/components/base/toast' export type ApiKeyModalProps = { @@ -23,34 +26,65 @@ export type ApiKeyModalProps = { onClose?: () => void editValues?: Record onRemove?: () => void + disabled?: boolean } const ApiKeyModal = ({ provider, onClose, editValues, onRemove, + disabled, }: ApiKeyModalProps) => { const { t } = useTranslation() const { notify } = useToastContext() - const { data } = useGetPluginToolCredentialSchema(provider, CredentialTypeEnum.API_KEY) + const { data = [] } = useGetPluginToolCredentialSchema(provider, CredentialTypeEnum.API_KEY) + const formSchemas = useMemo(() => { + return [ + { + type: FormTypeEnum.textInput, + name: '__name__', + label: 'Authorization name', + required: false, + }, + ...data, + ] + }, [data]) const { mutateAsync: addPluginToolCredential } = useAddPluginToolCredential(provider) const { mutateAsync: updatePluginToolCredential } = useUpdatePluginToolCredential(provider) const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider) const formRef = useRef(null) const handleConfirm = useCallback(async () => { - const store = formRef.current?.getFormStore() - const values = store?.state.values + const form = formRef.current?.getForm() + const store = form?.store + const { + __name__, + __credential_id__, + ...values + } = store?.state.values + const isPristineSecretInputNames: string[] = [] + formSchemas.forEach((schema) => { + if (schema.type === FormTypeEnum.secretInput) { + const fieldMeta = form?.getFieldMeta(schema.name) + if (fieldMeta?.isPristine) + isPristineSecretInputNames.push(schema.name) + } + }) + + const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values) if (editValues) { await updatePluginToolCredential({ - credentials: values, + credentials: transformedValues, + credential_id: __credential_id__, type: CredentialTypeEnum.API_KEY, + name: __name__ || '', }) } else { await addPluginToolCredential({ - credentials: values, + credentials: transformedValues, type: CredentialTypeEnum.API_KEY, + name: __name__ || '', }) } notify({ @@ -60,7 +94,7 @@ const ApiKeyModal = ({ onClose?.() invalidatePluginToolCredentialInfo() - }, [addPluginToolCredential, onClose, invalidatePluginToolCredentialInfo, updatePluginToolCredential, notify, t, editValues]) + }, [addPluginToolCredential, onClose, invalidatePluginToolCredentialInfo, updatePluginToolCredential, notify, t, editValues, formSchemas]) return ( ) diff --git a/web/app/components/plugins/plugin-auth/authorize/index.tsx b/web/app/components/plugins/plugin-auth/authorize/index.tsx index f40451e7fa..ceb5e79023 100644 --- a/web/app/components/plugins/plugin-auth/authorize/index.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/index.tsx @@ -36,7 +36,7 @@ const Authorize = ({ } return { - buttonText: !canApiKey ? 'Use OAuth Authorization' : '', + buttonText: !canApiKey ? 'Use OAuth Authorization' : 'Use OAuth', } }, [canApiKey, theme]) @@ -50,7 +50,7 @@ const Authorize = ({ } return { provider, - buttonText: !canOAuth ? 'API Key Authorization Configuration' : '', + buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Use API Key', buttonVariant: !canOAuth ? 'primary' : 'secondary-accent', } }, [canOAuth, theme, provider]) 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..1bf0425695 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx @@ -0,0 +1,94 @@ +import { + memo, + useCallback, + useMemo, + useState, +} from 'react' +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 } from './types' +import { + Authorized, + usePluginAuth, +} from '.' + +type AuthorizedInNodeProps = { + provider: string + onAuthorizationItemClick: (id: string) => void + credentialId?: string +} +const AuthorizedInNode = ({ + provider = '', + onAuthorizationItemClick, + credentialId, +}: AuthorizedInNodeProps) => { + const [isOpen, setIsOpen] = useState(false) + const { + canApiKey, + canOAuth, + credentials, + disabled, + } = usePluginAuth(provider, isOpen) + const label = useMemo(() => { + if (!credentialId) + return 'Workspace default' + const credential = credentials.find(c => c.id === credentialId) + + if (!credential) + return 'Auth removed' + + return credential.name + }, [credentials, credentialId]) + const renderTrigger = useCallback((open?: boolean) => { + return ( + + ) + }, [label]) + const extraAuthorizationItems: Credential[] = [ + { + id: '__workspace_default__', + name: 'Workspace default', + provider: '', + is_default: false, + 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 index 98e90d357f..f1188aaa70 100644 --- a/web/app/components/plugins/plugin-auth/authorized/index.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/index.tsx @@ -13,6 +13,9 @@ import { 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' @@ -35,6 +38,16 @@ type AuthorizedProps = { 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[] } const Authorized = ({ provider, @@ -42,10 +55,27 @@ const Authorized = ({ canOAuth, canApiKey, disabled, + renderTrigger, + isOpen, + onOpenChange, + offset = 8, + placement = 'bottom-start', + triggerPopupSameWidth = true, + popupClassName, + disableSetDefault, + onItemClick, + extraAuthorizationItems, }: AuthorizedProps) => { const { t } = useTranslation() const { notify } = useToastContext() - const [isOpen, setIsOpen] = useState(false) + 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) @@ -98,28 +128,57 @@ const Authorized = ({ return ( <> setIsOpen(!isOpen)} + onClick={() => setMergedIsOpen(!mergedIsOpen)} asChild > - + { + renderTrigger + ? renderTrigger(mergedIsOpen) + : ( + + ) + } -
+
+ { + !!extraAuthorizationItems?.length && ( +
+ { + extraAuthorizationItems.map(credential => ( + + )) + } +
+ ) + }
{ !!oAuthCredentials.length && ( @@ -153,6 +212,8 @@ const Authorized = ({ onDelete={openConfirm} onEdit={handleEdit} onSetDefault={handleSetDefault} + disableSetDefault={disableSetDefault} + onItemClick={onItemClick} /> )) } @@ -195,6 +256,7 @@ const Authorized = ({ pendingOperationCredentialId.current = null }} onRemove={handleRemove} + disabled={disabled} /> ) } diff --git a/web/app/components/plugins/plugin-auth/authorized/item.tsx b/web/app/components/plugins/plugin-auth/authorized/item.tsx index c32d0a9008..f54ef5ac9b 100644 --- a/web/app/components/plugins/plugin-auth/authorized/item.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -1,5 +1,6 @@ import { memo, + useMemo, } from 'react' import { RiDeleteBinLine, @@ -20,6 +21,11 @@ type ItemProps = { onDelete?: (id: string) => void onEdit?: (id: string, values: Record) => void onSetDefault?: (id: string) => void + disableRename?: boolean + disableEdit?: boolean + disableDelete?: boolean + disableSetDefault?: boolean + onItemClick?: (id: string) => void } const Item = ({ credential, @@ -27,16 +33,25 @@ const Item = ({ onDelete, onEdit, onSetDefault, + disableRename, + disableEdit, + disableDelete, + disableSetDefault, + onItemClick, }: ItemProps) => { const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2 + const showAction = useMemo(() => { + return !(disableRename && disableEdit && disableDelete && disableSetDefault) + }, [disableRename, disableEdit, disableDelete, disableSetDefault]) return (
onItemClick?.(credential.id)} > -
- +
+
-
- - { - isOAuth && ( - - - - - - ) - } - { - !isOAuth && ( - - onEdit?.(credential.id, credential.credentials)} - > - - - - ) - } - - onDelete?.(credential.id)} - > - - - -
+ { + showAction && ( +
+ { + !credential.is_default && !disableSetDefault && ( + + ) + } + { + isOAuth && !disableRename && ( + + + + + + ) + } + { + !isOAuth && !disableEdit && ( + + { + e.stopPropagation() + onEdit?.( + credential.id, + { + ...credential.credentials, + __name__: credential.name, + __credential_id__: credential.id, + }, + ) + }} + > + + + + ) + } + { + !disableDelete && ( + + { + e.stopPropagation() + onDelete?.(credential.id) + }} + > + + + + ) + } +
+ ) + }
) } diff --git a/web/app/components/plugins/plugin-auth/hooks.ts b/web/app/components/plugins/plugin-auth/hooks.ts new file mode 100644 index 0000000000..3c47997e4e --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks.ts @@ -0,0 +1,20 @@ +import { useAppContext } from '@/context/app-context' +import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth' +import { CredentialTypeEnum } from './types' + +export const usePluginAuth = (provider: string, enable?: boolean) => { + const { data } = useGetPluginToolCredentialInfo(enable ? provider : '') + 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) + + return { + isAuthorized, + canOAuth, + canApiKey, + credentials: data?.credentials || [], + provider, + disabled: !isCurrentWorkspaceManager, + } +} diff --git a/web/app/components/plugins/plugin-auth/index.tsx b/web/app/components/plugins/plugin-auth/index.tsx index 274697e061..a3cde28c72 100644 --- a/web/app/components/plugins/plugin-auth/index.tsx +++ b/web/app/components/plugins/plugin-auth/index.tsx @@ -1 +1,4 @@ export { default as PluginAuth } from './plugin-auth' +export { default as Authorized } from './authorized' +export { default as AuthorizedInNode } from './authorized-in-node' +export { usePluginAuth } from './hooks' diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx index 045abd2318..f02c66bc5f 100644 --- a/web/app/components/plugins/plugin-auth/plugin-auth.tsx +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -7,9 +7,11 @@ import { CredentialTypeEnum } from './types' type PluginAuthProps = { provider?: string + children?: React.ReactNode } const PluginAuth = ({ provider = '', + children, }: PluginAuthProps) => { const { data } = useGetPluginToolCredentialInfo(provider) const { isCurrentWorkspaceManager } = useAppContext() @@ -30,7 +32,7 @@ const PluginAuth = ({ ) } { - isAuthorized && ( + isAuthorized && !children && ( ) } + { + isAuthorized && children + } ) } diff --git a/web/app/components/plugins/plugin-auth/types.ts b/web/app/components/plugins/plugin-auth/types.ts index b9c63d2965..f60324b7f7 100644 --- a/web/app/components/plugins/plugin-auth/types.ts +++ b/web/app/components/plugins/plugin-auth/types.ts @@ -7,7 +7,8 @@ export type Credential = { id: string name: string provider: string - credential_type: CredentialTypeEnum + credential_type?: CredentialTypeEnum is_default: boolean - credentials: Record + 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/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index f1bdbbfbd9..ce7dcea0e2 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -30,6 +30,7 @@ export type ToolDefaultValue = { params: Record paramSchemas: Record[] output_schema: Record + credential_id?: string } export type ToolValue = { diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 164369e64c..59c46b1e45 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -59,6 +59,11 @@ import { useLogs } from '@/app/components/workflow/run/hooks' import PanelWrap from '../before-run-form/panel-wrap' import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' +import { + AuthorizedInNode, + PluginAuth, +} from '@/app/components/plugins/plugin-auth' +import { canFindTool } from '@/utils' type BasePanelProps = { children: ReactNode @@ -215,6 +220,22 @@ const BasePanel: FC = ({ 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.is_team_authorization + }, [currCollection, data.type]) + const handleAuthorizationItemClick = useCallback((id: string) => { + handleNodeDataUpdate({ + id, + data: { + credential_id: id === '__workspace_default__' ? undefined : id, + }, + }) + }, [handleNodeDataUpdate]) + if(logParams.showSpecialResultPanel) { return (
= ({ onChange={handleDescriptionChange} />
-
- -
+ { + showPluginAuth && ( + +
+ +
+
+ ) + } + { + !showPluginAuth && ( +
+ + { + currCollection?.allow_delete && ( + + ) + } +
+ ) + }
diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index 9f6465c008..a245dd63aa 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -5,18 +5,13 @@ import Split from '../_base/components/split' import type { ToolNodeType } from './types' import useConfig from './use-config' import InputVarList from './components/input-var-list' -import Button from '@/app/components/base/button' import Field from '@/app/components/workflow/nodes/_base/components/field' import type { NodePanelProps } from '@/app/components/workflow/types' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' -import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import Loading from '@/app/components/base/loading' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show' import { Type } from '../llm/types' -import { - PluginAuth, -} from '@/app/components/plugins/plugin-auth' const i18nPrefix = 'workflow.nodes.tool' @@ -38,10 +33,6 @@ const Panel: FC> = ({ setToolSettingValue, currCollection, isShowAuthBtn, - showSetAuth, - showSetAuthModal, - hideSetAuthModal, - handleSaveAuth, isLoading, outputSchema, hasObjectOutput, @@ -56,24 +47,6 @@ const Panel: FC> = ({ return (
-
- -
- {!readOnly && isShowAuthBtn && ( - <> -
- -
- - )} {!isShowAuthBtn && <>
{toolInputVarSchema.length > 0 && ( @@ -114,16 +87,6 @@ const Panel: FC> = ({ />
} - - {showSetAuth && ( - - )} -
<> diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index c9e5aa31b5..eb30e1f265 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -92,7 +92,7 @@ export type CommonNodeType = { error_strategy?: ErrorHandleTypeEnum retry_config?: WorkflowRetryConfig default_value?: DefaultValueForm[] -} & T & Partial> +} & T & Partial> export type CommonEdgeType = { _hovering?: boolean diff --git a/web/service/use-plugins-auth.ts b/web/service/use-plugins-auth.ts index 86bed7d343..000d6db723 100644 --- a/web/service/use-plugins-auth.ts +++ b/web/service/use-plugins-auth.ts @@ -71,6 +71,7 @@ export const useUpdatePluginToolCredential = ( ) => { return useMutation({ mutationFn: (params: { + credential_id: string credentials: Record type: CredentialTypeEnum name?: string From bdf5af7a6f0e59bf6f6ad8377dbaa58f4b10c6e7 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Thu, 10 Jul 2025 11:38:51 +0800 Subject: [PATCH 04/25] tool oauth --- .../authorize/add-api-key-button.tsx | 7 +- .../authorize/add-oauth-button.tsx | 2 + .../plugin-auth/authorize/api-key-modal.tsx | 33 +++---- .../plugins/plugin-auth/authorize/index.tsx | 15 ++-- .../plugin-auth/authorized-in-node.tsx | 53 ++++++----- .../plugins/plugin-auth/authorized/index.tsx | 37 ++++---- .../plugin-auth/hooks/use-credential.ts | 53 +++++++++++ .../plugins/plugin-auth/hooks/use-get-api.ts | 39 +++++++++ .../{hooks.ts => hooks/use-plugin-auth.ts} | 10 +-- .../components/plugins/plugin-auth/index.tsx | 3 +- .../plugins/plugin-auth/plugin-auth.tsx | 31 +++---- .../components/plugins/plugin-auth/types.ts | 11 +++ .../plugin-detail-panel/detail-header.tsx | 6 +- .../_base/components/workflow-panel/index.tsx | 17 ++-- web/service/use-plugins-auth.ts | 87 +++++++++---------- 15 files changed, 270 insertions(+), 134 deletions(-) create mode 100644 web/app/components/plugins/plugin-auth/hooks/use-credential.ts create mode 100644 web/app/components/plugins/plugin-auth/hooks/use-get-api.ts rename web/app/components/plugins/plugin-auth/{hooks.ts => hooks/use-plugin-auth.ts} (60%) 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 index e7312168b4..733ebbd945 100644 --- 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 @@ -5,15 +5,16 @@ import { 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 = { - provider?: string + pluginPayload: PluginPayload buttonVariant?: ButtonProps['variant'] buttonText?: string disabled?: boolean } const AddApiKeyButton = ({ - provider = '', + pluginPayload, buttonVariant = 'secondary-accent', buttonText = 'use api key', disabled, @@ -33,7 +34,7 @@ const AddApiKeyButton = ({ { isApiKeyModalOpen && ( setIsApiKeyModalOpen(false)} /> ) 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 index 6f2460b2de..379dfb7e61 100644 --- a/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx @@ -7,8 +7,10 @@ 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' export type AddOAuthButtonProps = { + pluginPayload: PluginPayload buttonVariant?: ButtonProps['variant'] buttonText?: string className?: string 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 index b7aae3c9fd..d7d4253089 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -8,28 +8,29 @@ 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 { - useAddPluginToolCredential, - useGetPluginToolCredentialSchema, - useInvalidPluginToolCredentialInfo, - useUpdatePluginToolCredential, -} from '@/service/use-plugins-auth' import { CredentialTypeEnum } from '../types' import { transformFormSchemasSecretInput } from '../utils' import AuthForm from '@/app/components/base/form/form-scenarios/auth' import type { FromRefObject } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types' import { useToastContext } from '@/app/components/base/toast' +import type { PluginPayload } from '../types' +import { + useAddPluginCredentialHook, + useGetPluginCredentialSchemaHook, + useInvalidPluginCredentialInfoHook, + useUpdatePluginCredentialHook, +} from '../hooks/use-credential' export type ApiKeyModalProps = { - provider: string + pluginPayload: PluginPayload onClose?: () => void editValues?: Record onRemove?: () => void disabled?: boolean } const ApiKeyModal = ({ - provider, + pluginPayload, onClose, editValues, onRemove, @@ -37,7 +38,7 @@ const ApiKeyModal = ({ }: ApiKeyModalProps) => { const { t } = useTranslation() const { notify } = useToastContext() - const { data = [] } = useGetPluginToolCredentialSchema(provider, CredentialTypeEnum.API_KEY) + const { data = [] } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY) const formSchemas = useMemo(() => { return [ { @@ -49,9 +50,9 @@ const ApiKeyModal = ({ ...data, ] }, [data]) - const { mutateAsync: addPluginToolCredential } = useAddPluginToolCredential(provider) - const { mutateAsync: updatePluginToolCredential } = useUpdatePluginToolCredential(provider) - const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider) + const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload) + const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) + const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) const formRef = useRef(null) const handleConfirm = useCallback(async () => { const form = formRef.current?.getForm() @@ -73,7 +74,7 @@ const ApiKeyModal = ({ const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values) if (editValues) { - await updatePluginToolCredential({ + await updatePluginCredential({ credentials: transformedValues, credential_id: __credential_id__, type: CredentialTypeEnum.API_KEY, @@ -81,7 +82,7 @@ const ApiKeyModal = ({ }) } else { - await addPluginToolCredential({ + await addPluginCredential({ credentials: transformedValues, type: CredentialTypeEnum.API_KEY, name: __name__ || '', @@ -93,8 +94,8 @@ const ApiKeyModal = ({ }) onClose?.() - invalidatePluginToolCredentialInfo() - }, [addPluginToolCredential, onClose, invalidatePluginToolCredentialInfo, updatePluginToolCredential, notify, t, editValues, formSchemas]) + invalidatePluginCredentialInfo() + }, [addPluginCredential, onClose, invalidatePluginCredentialInfo, updatePluginCredential, notify, t, editValues, formSchemas]) return ( { if (theme === 'secondary') { return { - provider, + pluginPayload, buttonVariant: 'secondary', buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Add API Key', } } return { - provider, + pluginPayload, buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Use API Key', buttonVariant: !canOAuth ? 'primary' : 'secondary-accent', } - }, [canOAuth, theme, provider]) + }, [canOAuth, theme, pluginPayload]) return ( <> diff --git a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx index 1bf0425695..96eb409829 100644 --- a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx +++ b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx @@ -1,26 +1,28 @@ import { memo, useCallback, - useMemo, useState, } from 'react' 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 } from './types' +import type { + Credential, + PluginPayload, +} from './types' import { Authorized, usePluginAuth, } from '.' type AuthorizedInNodeProps = { - provider: string + pluginPayload: PluginPayload onAuthorizationItemClick: (id: string) => void credentialId?: string } const AuthorizedInNode = ({ - provider = '', + pluginPayload, onAuthorizationItemClick, credentialId, }: AuthorizedInNodeProps) => { @@ -30,29 +32,40 @@ const AuthorizedInNode = ({ canOAuth, credentials, disabled, - } = usePluginAuth(provider, isOpen) - const label = useMemo(() => { - if (!credentialId) - return 'Workspace default' - const credential = credentials.find(c => c.id === credentialId) - - if (!credential) - return 'Auth removed' - - return credential.name - }, [credentials, credentialId]) + } = usePluginAuth(pluginPayload, isOpen || !!credentialId) const renderTrigger = useCallback((open?: boolean) => { + let label = '' + let removed = false + if (!credentialId) { + label = 'Workspace default' + } + else { + const credential = credentials.find(c => c.id === credentialId) + label = credential ? credential.name : 'Auth removed' + removed = !credential + } return ( ) - }, [label]) + }, [credentialId, credentials]) const extraAuthorizationItems: Credential[] = [ { id: '__workspace_default__', @@ -72,7 +85,7 @@ const AuthorizedInNode = ({ return ( credential.credential_type === CredentialTypeEnum.API_KEY) const pendingOperationCredentialId = useRef(null) const [deleteCredentialId, setDeleteCredentialId] = useState(null) - const { mutateAsync: deletePluginToolCredential } = useDeletePluginToolCredential(provider) - const invalidatePluginToolCredentialInfo = useInvalidPluginToolCredentialInfo(provider) + const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload) + const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) const openConfirm = useCallback((credentialId?: string) => { if (credentialId) pendingOperationCredentialId.current = credentialId @@ -98,15 +99,15 @@ const Authorized = ({ return } - await deletePluginToolCredential({ credential_id: pendingOperationCredentialId.current }) + await deletePluginCredential({ credential_id: pendingOperationCredentialId.current }) notify({ type: 'success', message: t('common.api.actionSuccess'), }) - invalidatePluginToolCredentialInfo() + invalidatePluginCredentialInfo() setDeleteCredentialId(null) pendingOperationCredentialId.current = null - }, [deletePluginToolCredential, invalidatePluginToolCredentialInfo, notify, t]) + }, [deletePluginCredential, invalidatePluginCredentialInfo, notify, t]) const [editValues, setEditValues] = useState | null>(null) const handleEdit = useCallback((id: string, values: Record) => { pendingOperationCredentialId.current = id @@ -115,15 +116,15 @@ const Authorized = ({ const handleRemove = useCallback(() => { setDeleteCredentialId(pendingOperationCredentialId.current) }, []) - const { mutateAsync: setPluginToolDefaultCredential } = useSetPluginToolDefaultCredential(provider) + const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload) const handleSetDefault = useCallback(async (id: string) => { - await setPluginToolDefaultCredential(id) + await setPluginDefaultCredential(id) notify({ type: 'success', message: t('common.api.actionSuccess'), }) - invalidatePluginToolCredentialInfo() - }, [setPluginToolDefaultCredential, invalidatePluginToolCredentialInfo, notify, t]) + invalidatePluginCredentialInfo() + }, [setPluginDefaultCredential, invalidatePluginCredentialInfo, notify, t]) return ( <> @@ -224,7 +225,7 @@ const Authorized = ({
{ setEditValues(null) 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..1fc9f0e012 --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts @@ -0,0 +1,53 @@ +import { + useAddPluginCredential, + useDeletePluginCredential, + useGetPluginCredentialInfo, + useGetPluginCredentialSchema, + useInvalidPluginCredentialInfo, + useSetPluginDefaultCredential, + 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) +} 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..8551332ddb --- /dev/null +++ b/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts @@ -0,0 +1,39 @@ +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`, + getCustomOAuthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, + } + } + + return { + getCredentialInfo: '', + setDefaultCredential: '', + getCredentials: '', + addCredential: '', + updateCredential: '', + deleteCredential: '', + getCredentialSchema: () => '', + getOauthUrl: '', + getOauthClientSchema: '', + setCustomOauthClient: '', + getCustomOAuthClient: '', + } +} diff --git a/web/app/components/plugins/plugin-auth/hooks.ts b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts similarity index 60% rename from web/app/components/plugins/plugin-auth/hooks.ts rename to web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts index 3c47997e4e..67d19e3de1 100644 --- a/web/app/components/plugins/plugin-auth/hooks.ts +++ b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts @@ -1,9 +1,10 @@ import { useAppContext } from '@/context/app-context' -import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth' -import { CredentialTypeEnum } from './types' +import { useGetPluginCredentialInfoHook } from './use-credential' +import { CredentialTypeEnum } from '../types' +import type { PluginPayload } from '../types' -export const usePluginAuth = (provider: string, enable?: boolean) => { - const { data } = useGetPluginToolCredentialInfo(enable ? provider : '') +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) @@ -14,7 +15,6 @@ export const usePluginAuth = (provider: string, enable?: boolean) => { canOAuth, canApiKey, credentials: data?.credentials || [], - provider, disabled: !isCurrentWorkspaceManager, } } diff --git a/web/app/components/plugins/plugin-auth/index.tsx b/web/app/components/plugins/plugin-auth/index.tsx index a3cde28c72..f5b2f0c457 100644 --- a/web/app/components/plugins/plugin-auth/index.tsx +++ b/web/app/components/plugins/plugin-auth/index.tsx @@ -1,4 +1,5 @@ export { default as PluginAuth } from './plugin-auth' export { default as Authorized } from './authorized' export { default as AuthorizedInNode } from './authorized-in-node' -export { usePluginAuth } from './hooks' +export { usePluginAuth } from './hooks/use-plugin-auth' +export * from './types' diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx index f02c66bc5f..6fdcdd1140 100644 --- a/web/app/components/plugins/plugin-auth/plugin-auth.tsx +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -1,44 +1,45 @@ import { memo } from 'react' import Authorize from './authorize' import Authorized from './authorized' -import { useAppContext } from '@/context/app-context' -import { useGetPluginToolCredentialInfo } from '@/service/use-plugins-auth' -import { CredentialTypeEnum } from './types' +import type { PluginPayload } from './types' +import { usePluginAuth } from './hooks/use-plugin-auth' type PluginAuthProps = { - provider?: string + pluginPayload: PluginPayload children?: React.ReactNode } const PluginAuth = ({ - provider = '', + pluginPayload, children, }: PluginAuthProps) => { - const { data } = useGetPluginToolCredentialInfo(provider) - 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 { + isAuthorized, + canOAuth, + canApiKey, + credentials, + disabled, + } = usePluginAuth(pluginPayload, true) return ( <> { !isAuthorized && ( ) } { isAuthorized && !children && ( ) } diff --git a/web/app/components/plugins/plugin-auth/types.ts b/web/app/components/plugins/plugin-auth/types.ts index f60324b7f7..ad41733bde 100644 --- a/web/app/components/plugins/plugin-auth/types.ts +++ b/web/app/components/plugins/plugin-auth/types.ts @@ -1,3 +1,14 @@ +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', 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 a1f14e6301..41e74b7d8a 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -37,6 +37,7 @@ 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' @@ -275,7 +276,10 @@ const DetailHeader = ({ { category === PluginType.tool && ( ) } diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 59c46b1e45..982dd4285a 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -63,6 +63,7 @@ import { AuthorizedInNode, PluginAuth, } from '@/app/components/plugins/plugin-auth' +import { AuthCategory } from '@/app/components/plugins/plugin-auth' import { canFindTool } from '@/utils' type BasePanelProps = { @@ -227,14 +228,14 @@ const BasePanel: FC = ({ const showPluginAuth = useMemo(() => { return data.type === BlockEnum.Tool && currCollection?.allow_delete && !currCollection.is_team_authorization }, [currCollection, data.type]) - const handleAuthorizationItemClick = useCallback((id: string) => { + const handleAuthorizationItemClick = useCallback((credential_id: string) => { handleNodeDataUpdate({ id, data: { - credential_id: id === '__workspace_default__' ? undefined : id, + credential_id: credential_id === '__workspace_default__' ? undefined : credential_id, }, }) - }, [handleNodeDataUpdate]) + }, [handleNodeDataUpdate, id]) if(logParams.showSpecialResultPanel) { return ( @@ -371,7 +372,10 @@ const BasePanel: FC = ({ { showPluginAuth && (
= ({ { currCollection?.allow_delete && ( diff --git a/web/service/use-plugins-auth.ts b/web/service/use-plugins-auth.ts index 000d6db723..90b1a20e8c 100644 --- a/web/service/use-plugins-auth.ts +++ b/web/service/use-plugins-auth.ts @@ -12,48 +12,48 @@ import type { FormSchema } from '@/app/components/base/form/types' const NAME_SPACE = 'plugins-auth' -export const useGetPluginToolCredentialInfo = ( - provider: string, +export const useGetPluginCredentialInfo = ( + url: string, ) => { return useQuery({ - enabled: !!provider, - queryKey: [NAME_SPACE, 'credential-info', provider], + enabled: !!url, + queryKey: [NAME_SPACE, 'credential-info', url], queryFn: () => get<{ supported_credential_types: string[] credentials: Credential[] is_oauth_custom_client_enabled: boolean - }>(`/workspaces/current/tool-provider/builtin/${provider}/credential/info`), + }>(url), staleTime: 0, }) } -export const useInvalidPluginToolCredentialInfo = ( - provider: string, +export const useInvalidPluginCredentialInfo = ( + url: string, ) => { - return useInvalid([NAME_SPACE, 'credential-info', provider]) + return useInvalid([NAME_SPACE, 'credential-info', url]) } -export const useSetPluginToolDefaultCredential = ( - provider: string, +export const useSetPluginDefaultCredential = ( + url: string, ) => { return useMutation({ mutationFn: (id: string) => { - return post(`/workspaces/current/tool-provider/builtin/${provider}/default-credential`, { body: { id } }) + return post(url, { body: { id } }) }, }) } -export const useGetPluginToolCredentialList = ( - provider: string, +export const useGetPluginCredentialList = ( + url: string, ) => { return useQuery({ - queryKey: [NAME_SPACE, 'credential-list', provider], - queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/credentials`), + queryKey: [NAME_SPACE, 'credential-list', url], + queryFn: () => get(url), }) } -export const useAddPluginToolCredential = ( - provider: string, +export const useAddPluginCredential = ( + url: string, ) => { return useMutation({ mutationFn: (params: { @@ -61,13 +61,13 @@ export const useAddPluginToolCredential = ( type: CredentialTypeEnum name?: string }) => { - return post(`/workspaces/current/tool-provider/builtin/${provider}/add`, { body: params }) + return post(url, { body: params }) }, }) } -export const useUpdatePluginToolCredential = ( - provider: string, +export const useUpdatePluginCredential = ( + url: string, ) => { return useMutation({ mutationFn: (params: { @@ -76,64 +76,63 @@ export const useUpdatePluginToolCredential = ( type: CredentialTypeEnum name?: string }) => { - return post(`/workspaces/current/tool-provider/builtin/${provider}/update`, { body: params }) + return post(url, { body: params }) }, }) } -export const useDeletePluginToolCredential = ( - provider: string, +export const useDeletePluginCredential = ( + url: string, ) => { return useMutation({ mutationFn: (params: { credential_id: string }) => { - return post(`/workspaces/current/tool-provider/builtin/${provider}/delete`, { body: params }) + return post(url, { body: params }) }, }) } -export const useGetPluginToolCredentialSchema = ( - provider: string, - credential_type: CredentialTypeEnum, +export const useGetPluginCredentialSchema = ( + url: string, ) => { return useQuery({ - queryKey: [NAME_SPACE, 'credential-schema', provider, credential_type], - queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/credential/schema/${credential_type}`), + queryKey: [NAME_SPACE, 'credential-schema', url], + queryFn: () => get(url), }) } -export const useGetPluginToolOAuthUrl = ( - provider: string, +export const useGetPluginOAuthUrl = ( + url: string, ) => { return useQuery({ - queryKey: [NAME_SPACE, 'oauth-url', provider], - queryFn: () => get(`oauth/plugin/${provider}/tool/authorization-url`), + queryKey: [NAME_SPACE, 'oauth-url', url], + queryFn: () => get(url), }) } -export const useGetPluginToolOAuthClientSchema = ( - provider: string, +export const useGetPluginOAuthClientSchema = ( + url: string, ) => { return useQuery({ - queryKey: [NAME_SPACE, 'oauth-client-schema', provider], - queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`), + queryKey: [NAME_SPACE, 'oauth-client-schema', url], + queryFn: () => get(url), }) } -export const useSetPluginToolOAuthCustomClient = ( - provider: string, +export const useSetPluginOAuthCustomClient = ( + url: string, ) => { return useMutation({ mutationFn: (params) => { - return post(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, { body: params }) + return post(url, { body: params }) }, }) } -export const useGetPluginToolOAuthCustomClientSchema = ( - provider: string, +export const useGetPluginOAuthCustomClientSchema = ( + url: string, ) => { return useQuery({ - queryKey: [NAME_SPACE, 'oauth-custom-client-schema', provider], - queryFn: () => get(`/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`), + queryKey: [NAME_SPACE, 'oauth-custom-client-schema', url], + queryFn: () => get(url), }) } From 18699f86714aa9701916b552929d7fb832b0c789 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Thu, 10 Jul 2025 17:12:48 +0800 Subject: [PATCH 05/25] tool oauth --- .../base/form/components/base/base-field.tsx | 53 +++ web/app/components/base/form/types.ts | 10 + web/app/components/base/select/pure.tsx | 10 +- .../plugin-auth/authorize/api-key-modal.tsx | 61 ++-- .../plugins/plugin-auth/authorize/index.tsx | 14 +- .../plugin-auth/authorized-in-node.tsx | 13 +- .../plugins/plugin-auth/authorized/index.tsx | 83 +++-- .../plugins/plugin-auth/authorized/item.tsx | 123 +++++-- .../components/plugins/plugin-auth/index.tsx | 1 + .../plugin-auth/plugin-auth-in-agent.tsx | 119 +++++++ .../tool-selector/index.tsx | 328 ++++++++---------- .../tool-selector/tool-item.tsx | 12 +- web/app/components/workflow/types.ts | 3 +- web/i18n/en-US/plugin.ts | 20 ++ web/i18n/zh-Hans/plugin.ts | 20 ++ web/service/use-plugins-auth.ts | 3 +- 16 files changed, 595 insertions(+), 278 deletions(-) create mode 100644 web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index f997297691..47a8cca419 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -7,6 +7,7 @@ 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' @@ -32,6 +33,9 @@ const BaseField = ({ const renderI18nObject = useRenderI18nObject() const { label, + required, + placeholder, + options, } = formSchema const memorizedLabel = useMemo(() => { @@ -44,12 +48,32 @@ const BaseField = ({ if (typeof label === 'object' && label !== null) return renderI18nObject(label as Record) }, [label, renderI18nObject]) + const memorizedPlaceholder = useMemo(() => { + if (typeof placeholder === 'string') + return placeholder + + if (typeof placeholder === 'object' && placeholder !== null) + return renderI18nObject(placeholder as Record) + }, [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]) return (
{memorizedLabel} + { + required && ( + * + ) + }
{ @@ -62,6 +86,7 @@ const BaseField = ({ onChange={e => field.handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} + placeholder={memorizedPlaceholder} /> ) } @@ -76,6 +101,34 @@ const BaseField = ({ onChange={e => field.handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} + placeholder={memorizedPlaceholder} + /> + ) + } + { + formSchema.type === FormTypeEnum.textNumber && ( + field.handleChange(e.target.value)} + onBlur={field.handleBlur} + disabled={disabled} + placeholder={memorizedPlaceholder} + /> + ) + } + { + formSchema.type === FormTypeEnum.select && ( + field.handleChange(v)} + disabled={disabled} + placeholder={memorizedPlaceholder} + options={memorizedOptions} + triggerPopupSameWidth /> ) } diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index 5156477a38..3e1001b54e 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -31,6 +31,13 @@ export enum FormTypeEnum { dynamicSelect = 'dynamic-select', } +export type FormOption = { + label: TypeWithI18N | string + value: string + show_on: FormShowOnObject[] + icon?: string +} + export type FormSchema = { type: FormTypeEnum name: string @@ -41,6 +48,9 @@ export type FormSchema = { show_on?: FormShowOnObject[] url?: string scope?: string + help?: string | TypeWithI18N + placeholder?: string | TypeWithI18N + options?: FormOption[] } export type FormValues = Record 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/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx index d7d4253089..572dfe125e 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -14,6 +14,7 @@ import AuthForm from '@/app/components/base/form/form-scenarios/auth' import type { FromRefObject } 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, @@ -21,6 +22,7 @@ import { useInvalidPluginCredentialInfoHook, useUpdatePluginCredentialHook, } from '../hooks/use-credential' +import { useRenderI18nObject } from '@/hooks/use-i18n' export type ApiKeyModalProps = { pluginPayload: PluginPayload @@ -38,18 +40,25 @@ const ApiKeyModal = ({ }: ApiKeyModalProps) => { const { t } = useTranslation() const { notify } = useToastContext() - const { data = [] } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY) + const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY) const formSchemas = useMemo(() => { return [ { type: FormTypeEnum.textInput, name: '__name__', - label: 'Authorization name', + label: t('plugin.auth.authorizationName'), required: false, }, ...data, ] - }, [data]) + }, [data, t]) + const defaultValues = formSchemas.reduce((acc, schema) => { + if (schema.default) + acc[schema.name] = schema.default + return acc + }, {} as Record) + const secretInput = formSchemas.find(schema => schema.type === FormTypeEnum.secretInput) + const renderI18nObject = useRenderI18nObject() const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload) const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) @@ -77,7 +86,6 @@ const ApiKeyModal = ({ await updatePluginCredential({ credentials: transformedValues, credential_id: __credential_id__, - type: CredentialTypeEnum.API_KEY, name: __name__ || '', }) } @@ -100,19 +108,21 @@ const ApiKeyModal = ({ return ( - Get your API Key from OpenAI - - + secretInput && ( + + {renderI18nObject(secretInput?.help as any)} + + + ) } bottomSlot={
@@ -133,12 +143,23 @@ const ApiKeyModal = ({ onExtraButtonClick={onRemove} disabled={disabled} > - + { + isLoading && ( +
+ +
+ ) + } + { + !isLoading && !!data.length && ( + + ) + } ) } diff --git a/web/app/components/plugins/plugin-auth/authorize/index.tsx b/web/app/components/plugins/plugin-auth/authorize/index.tsx index bb87578671..80f1656fb8 100644 --- a/web/app/components/plugins/plugin-auth/authorize/index.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/index.tsx @@ -2,6 +2,7 @@ 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' @@ -24,10 +25,11 @@ const Authorize = ({ canApiKey, disabled, }: AuthorizeProps) => { + const { t } = useTranslation() const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => { if (theme === 'secondary') { return { - buttonText: !canApiKey ? 'Add OAuth Authorization' : 'Add OAuth', + 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', @@ -38,25 +40,25 @@ const Authorize = ({ } return { - buttonText: !canApiKey ? 'Use OAuth Authorization' : 'Use OAuth', + buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'), pluginPayload, } - }, [canApiKey, theme, pluginPayload]) + }, [canApiKey, theme, pluginPayload, t]) const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => { if (theme === 'secondary') { return { pluginPayload, buttonVariant: 'secondary', - buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Add API Key', + buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'), } } return { pluginPayload, - buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Use API Key', + buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'), buttonVariant: !canOAuth ? 'primary' : 'secondary-accent', } - }, [canOAuth, theme, pluginPayload]) + }, [canOAuth, theme, pluginPayload, t]) return ( <> diff --git a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx index 96eb409829..55516e7fca 100644 --- a/web/app/components/plugins/plugin-auth/authorized-in-node.tsx +++ b/web/app/components/plugins/plugin-auth/authorized-in-node.tsx @@ -3,6 +3,7 @@ import { 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' @@ -26,6 +27,7 @@ const AuthorizedInNode = ({ onAuthorizationItemClick, credentialId, }: AuthorizedInNodeProps) => { + const { t } = useTranslation() const [isOpen, setIsOpen] = useState(false) const { canApiKey, @@ -37,11 +39,11 @@ const AuthorizedInNode = ({ let label = '' let removed = false if (!credentialId) { - label = 'Workspace default' + label = t('plugin.auth.workspaceDefault') } else { const credential = credentials.find(c => c.id === credentialId) - label = credential ? credential.name : 'Auth removed' + label = credential ? credential.name : t('plugin.auth.authRemoved') removed = !credential } return ( @@ -65,13 +67,13 @@ const AuthorizedInNode = ({ /> ) - }, [credentialId, credentials]) + }, [credentialId, credentials, t]) const extraAuthorizationItems: Credential[] = [ { id: '__workspace_default__', - name: 'Workspace default', + name: t('plugin.auth.workspaceDefault'), provider: '', - is_default: false, + is_default: !credentialId, isWorkspaceDefault: true, }, ] @@ -100,6 +102,7 @@ const AuthorizedInNode = ({ disableSetDefault onItemClick={handleAuthorizationItemClick} extraAuthorizationItems={extraAuthorizationItems} + showItemSelectedIcon /> ) } diff --git a/web/app/components/plugins/plugin-auth/authorized/index.tsx b/web/app/components/plugins/plugin-auth/authorized/index.tsx index 13fec807f0..ddd05787b6 100644 --- a/web/app/components/plugins/plugin-auth/authorized/index.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/index.tsx @@ -31,6 +31,7 @@ import { useDeletePluginCredentialHook, useInvalidPluginCredentialInfoHook, useSetPluginDefaultCredentialHook, + useUpdatePluginCredentialHook, } from '../hooks/use-credential' type AuthorizedProps = { @@ -49,6 +50,7 @@ type AuthorizedProps = { disableSetDefault?: boolean onItemClick?: (id: string) => void extraAuthorizationItems?: Credential[] + showItemSelectedIcon?: boolean } const Authorized = ({ pluginPayload, @@ -66,6 +68,7 @@ const Authorized = ({ disableSetDefault, onItemClick, extraAuthorizationItems, + showItemSelectedIcon, }: AuthorizedProps) => { const { t } = useTranslation() const { notify } = useToastContext() @@ -125,6 +128,17 @@ const Authorized = ({ }) invalidatePluginCredentialInfo() }, [setPluginDefaultCredential, invalidatePluginCredentialInfo, notify, t]) + const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) + const handleRename = useCallback(async (payload: { + credential_id: string + name: string + }) => { + await updatePluginCredential(payload) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + }, [updatePluginCredential, notify, t]) return ( <> @@ -149,7 +163,12 @@ const Authorized = ({ isOpen && 'bg-components-button-secondary-bg-hover', )}> - {credentials.length} Authorizations + {credentials.length}  + { + credentials.length > 1 + ? t('plugin.auth.authorizations') + : t('plugin.auth.authorization') + } ) @@ -160,31 +179,35 @@ const Authorized = ({ 'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg', popupClassName, )}> - { - !!extraAuthorizationItems?.length && ( -
- { - extraAuthorizationItems.map(credential => ( - - )) - } -
- ) - }
+ { + !!extraAuthorizationItems?.length && ( +
+ { + extraAuthorizationItems.map(credential => ( + + )) + } +
+ ) + } { !!oAuthCredentials.length && (
-
+
OAuth
{ @@ -192,6 +215,14 @@ const Authorized = ({ )) } @@ -201,7 +232,10 @@ const Authorized = ({ { !!apiKeyCredentials.length && (
-
+
API Keys
{ @@ -214,7 +248,10 @@ const Authorized = ({ onEdit={handleEdit} onSetDefault={handleSetDefault} disableSetDefault={disableSetDefault} + disableRename onItemClick={onItemClick} + onRename={handleRename} + showSelectedIcon={showItemSelectedIcon} /> )) } diff --git a/web/app/components/plugins/plugin-auth/authorized/item.tsx b/web/app/components/plugins/plugin-auth/authorized/item.tsx index f54ef5ac9b..42bce232ff 100644 --- a/web/app/components/plugins/plugin-auth/authorized/item.tsx +++ b/web/app/components/plugins/plugin-auth/authorized/item.tsx @@ -1,8 +1,11 @@ import { memo, useMemo, + useState, } from 'react' +import { useTranslation } from 'react-i18next' import { + RiCheckLine, RiDeleteBinLine, RiEditLine, RiEqualizer2Line, @@ -12,6 +15,8 @@ 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' @@ -21,11 +26,16 @@ type ItemProps = { 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 } const Item = ({ credential, @@ -33,12 +43,17 @@ const Item = ({ onDelete, onEdit, onSetDefault, + onRename, disableRename, disableEdit, disableDelete, disableSetDefault, onItemClick, + showSelectedIcon, }: 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) @@ -47,27 +62,77 @@ const Item = ({ return (
onItemClick?.(credential.id)} > -
- -
- {credential.name} -
- { - credential.is_default && ( - - Default - - ) - } -
{ - showAction && ( + renaming && ( +
+ setRenameValue(e.target.value)} + placeholder={t('common.placeholder.input')} + /> + + +
+ ) + } + { + !renaming && ( +
+ { + showSelectedIcon && ( +
+ { + credential.is_default && ( + + ) + } +
+ ) + } + +
+ {credential.name} +
+ { + credential.is_default && ( + + {t('plugin.auth.default')} + + ) + } +
+ ) + } + { + showAction && !renaming && (
{ !credential.is_default && !disableSetDefault && ( @@ -79,14 +144,21 @@ const Item = ({ onSetDefault?.(credential.id) }} > - Set as default + {t('plugin.auth.setDefault')} ) } { - isOAuth && !disableRename && ( - - + !disableRename && ( + + { + e.stopPropagation() + setRenaming(true) + setRenameValue(credential.name) + }} + > @@ -94,7 +166,7 @@ const Item = ({ } { !isOAuth && !disableEdit && ( - + { @@ -116,15 +188,16 @@ const Item = ({ } { !disableDelete && ( - + { e.stopPropagation() onDelete?.(credential.id) }} > - + ) diff --git a/web/app/components/plugins/plugin-auth/index.tsx b/web/app/components/plugins/plugin-auth/index.tsx index f5b2f0c457..e4f6ae8b2f 100644 --- a/web/app/components/plugins/plugin-auth/index.tsx +++ b/web/app/components/plugins/plugin-auth/index.tsx @@ -1,5 +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..bb5d8e1f3c --- /dev/null +++ b/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx @@ -0,0 +1,119 @@ +import { + memo, + useCallback, + useState, +} from 'react' +import { RiArrowDownSLine } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import Authorize from './authorize' +import Authorized from './authorized' +import type { + Credential, + PluginPayload, +} from './types' +import { usePluginAuth } from './hooks/use-plugin-auth' +import Button from '@/app/components/base/button' +import Indicator from '@/app/components/header/indicator' +import cn from '@/utils/classnames' + +type PluginAuthInAgentProps = { + pluginPayload: PluginPayload + credentialId?: string + onAuthorizationItemClick?: (id: string) => void +} +const PluginAuthInAgent = ({ + pluginPayload, + credentialId, + onAuthorizationItemClick, +}: PluginAuthInAgentProps) => { + const { t } = useTranslation() + const [isOpen, setIsOpen] = useState(false) + const { + isAuthorized, + canOAuth, + canApiKey, + credentials, + disabled, + } = usePluginAuth(pluginPayload, true) + + const extraAuthorizationItems: Credential[] = [ + { + id: '__workspace_default__', + name: t('plugin.auth.workspaceDefault'), + provider: '', + is_default: !credentialId, + isWorkspaceDefault: true, + }, + ] + + const handleAuthorizationItemClick = useCallback((id: string) => { + onAuthorizationItemClick?.(id) + setIsOpen(false) + }, [ + onAuthorizationItemClick, + setIsOpen, + ]) + + const renderTrigger = useCallback((isOpen?: boolean) => { + let label = '' + let removed = false + if (!credentialId) { + label = t('plugin.auth.workspaceDefault') + } + else { + const credential = credentials.find(c => c.id === credentialId) + label = credential ? credential.name : t('plugin.auth.authRemoved') + removed = !credential + } + return ( + + ) + }, [credentialId, credentials, t]) + + return ( + <> + { + !isAuthorized && ( + + ) + } + { + isAuthorized && ( + + ) + } + + ) +} + +export default memo(PluginAuthInAgent) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index 350fe50933..da4323713b 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -4,7 +4,6 @@ import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Link from 'next/link' import { - RiArrowLeftLine, RiArrowRightUpLine, } from '@remixicon/react' import { @@ -15,24 +14,17 @@ import { import ToolTrigger from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-trigger' import ToolItem from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-item' import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' -import Button from '@/app/components/base/button' -import Indicator from '@/app/components/header/indicator' -import ToolCredentialForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/tool-credentials-form' -import Toast from '@/app/components/base/toast' import Textarea from '@/app/components/base/textarea' import Divider from '@/app/components/base/divider' import TabSlider from '@/app/components/base/tab-slider-plain' import ReasoningConfigForm from '@/app/components/plugins/plugin-detail-panel/tool-selector/reasoning-config-form' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' - -import { useAppContext } from '@/context/app-context' import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllBuiltInTools, - useUpdateProviderCredentials, } from '@/service/use-tools' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { usePluginInstalledCheck } from '@/app/components/plugins/plugin-detail-panel/tool-selector/hooks' @@ -46,6 +38,10 @@ import { MARKETPLACE_API_PREFIX } from '@/config' import type { Node } from 'reactflow' import type { NodeOutPutVar } from '@/app/components/workflow/types' import cn from '@/utils/classnames' +import { + AuthCategory, + PluginAuthInAgent, +} from '@/app/components/plugins/plugin-auth' type Props = { disabled?: boolean @@ -191,23 +187,6 @@ const ToolSelector: FC = ({ } 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) @@ -221,6 +200,12 @@ const ToolSelector: FC = ({ invalidateAllBuiltinTools() invalidateInstalledPluginList() } + const handleAuthorizationItemClick = (id: string) => { + onSelect({ + ...value, + credential_id: id, + } as any) + } return ( <> @@ -257,7 +242,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} @@ -276,181 +260,141 @@ 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} - scope={scope} - selectedTools={selectedTools} +
+ <> +
{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} + scope={scope} + selectedTools={selectedTools} + /> +
+
+
{t('plugin.detailPanel.toolSelector.descriptionLabel')}
+