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