From 324437b3f1446a1c203213255c5cce28e5197f14 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 8 Nov 2024 16:11:50 +0800 Subject: [PATCH 01/16] feat: mutation permersions --- .../permission-setting-modal/modal.tsx | 2 +- .../plugins/plugin-page/context.tsx | 22 ++-------- .../components/plugins/plugin-page/index.tsx | 2 +- .../plugins/plugin-page/use-permission.ts | 42 ++++++++----------- web/service/plugins.ts | 9 ---- web/service/use-plugins.ts | 36 +++++++++++++++- 6 files changed, 58 insertions(+), 55 deletions(-) diff --git a/web/app/components/plugins/permission-setting-modal/modal.tsx b/web/app/components/plugins/permission-setting-modal/modal.tsx index c9e2d2da61..b5eaa4765c 100644 --- a/web/app/components/plugins/permission-setting-modal/modal.tsx +++ b/web/app/components/plugins/permission-setting-modal/modal.tsx @@ -34,7 +34,7 @@ const PluginSettingModal: FC = ({ const handleSave = useCallback(async () => { await onSave(tempPrivilege) onHide() - }, [tempPrivilege]) + }, [onHide, onSave, tempPrivilege]) return ( - permissions: Permissions - setPermissions: (permissions: PluginPageContextValue['permissions']) => void currentPluginDetail: PluginDetail | undefined setCurrentPluginDetail: (plugin: PluginDetail) => void filters: FilterState @@ -32,21 +29,16 @@ export type PluginPageContextValue = { export const PluginPageContext = createContext({ containerRef: { current: null }, - permissions: { - install_permission: PermissionType.noOne, - debug_permission: PermissionType.noOne, - }, - setPermissions: () => {}, currentPluginDetail: undefined, - setCurrentPluginDetail: () => {}, + setCurrentPluginDetail: () => { }, filters: { categories: [], tags: [], searchQuery: '', }, - setFilters: () => {}, + setFilters: () => { }, activeTab: '', - setActiveTab: () => {}, + setActiveTab: () => { }, options: [], }) @@ -63,10 +55,6 @@ export const PluginPageContextProvider = ({ }: PluginPageContextProviderProps) => { const { t } = useTranslation() const containerRef = useRef(null) - const [permissions, setPermissions] = useState({ - install_permission: PermissionType.noOne, - debug_permission: PermissionType.noOne, - }) const [filters, setFilters] = useState({ categories: [], tags: [], @@ -93,8 +81,6 @@ export const PluginPageContextProvider = ({ diff --git a/web/app/components/plugins/plugin-page/use-permission.ts b/web/app/components/plugins/plugin-page/use-permission.ts index 4a8cdc29c1..c4fc01f2c3 100644 --- a/web/app/components/plugins/plugin-page/use-permission.ts +++ b/web/app/components/plugins/plugin-page/use-permission.ts @@ -1,15 +1,12 @@ -import { useEffect } from 'react' -import type { Permissions } from '../types' import { PermissionType } from '../types' -import { - usePluginPageContext, -} from './context' import { useAppContext } from '@/context/app-context' -import { updatePermission as doUpdatePermission, fetchPermission } from '@/service/plugins' import Toast from '../../base/toast' import { useTranslation } from 'react-i18next' +import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins' -const hasPermission = (permission: PermissionType, isAdmin: boolean) => { +const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) => { + if (!permission) + return false if (permission === PermissionType.noOne) return false @@ -22,29 +19,26 @@ const hasPermission = (permission: PermissionType, isAdmin: boolean) => { const usePermission = () => { const { t } = useTranslation() const { isCurrentWorkspaceManager, isCurrentWorkspaceOwner } = useAppContext() - const [permissions, setPermissions] = usePluginPageContext(v => [v.permissions, v.setPermissions]) + const { data: permissions } = usePermissions() + const invalidatePermissions = useInvalidatePermissions() + const { mutate: updatePermission, isPending: isUpdatePending } = useMutationPermissions({ + onSuccess: () => { + invalidatePermissions() + Toast.notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + }, + }) const isAdmin = isCurrentWorkspaceManager || isCurrentWorkspaceOwner - const updatePermission = async (permission: Permissions) => { - await doUpdatePermission(permission) - setPermissions(permission) - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - } - useEffect(() => { - (async () => { - const permission = await fetchPermission() - setPermissions(permission) - })() - }, []) return { - canManagement: hasPermission(permissions.install_permission, isAdmin), - canDebugger: hasPermission(permissions.debug_permission, isAdmin), + canManagement: hasPermission(permissions?.install_permission, isAdmin), + canDebugger: hasPermission(permissions?.debug_permission, isAdmin), canSetPermissions: isAdmin, permissions, setPermissions: updatePermission, + isUpdatePending, } } diff --git a/web/service/plugins.ts b/web/service/plugins.ts index e9de724256..6d246e9239 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -15,7 +15,6 @@ import type { UpdateEndpointRequest, uploadGitHubResponse, } from '@/app/components/plugins/types' -import type { DebugInfo as DebugInfoTypes } from '@/app/components/plugins/types' import type { MarketplaceCollectionPluginsResponse, MarketplaceCollectionsResponse, @@ -51,10 +50,6 @@ export const disableEndpoint: Fetcher(url, { body: { endpoint_id: endpointID } }) } -export const fetchDebugKey = async () => { - return get('/workspaces/current/plugin/debugging-key') -} - export const uploadPackageFile = async (file: File) => { const formData = new FormData() formData.append('pkg', file) @@ -131,10 +126,6 @@ export const checkTaskStatus = async (taskId: string) => { return get(`/workspaces/current/plugin/tasks/${taskId}`) } -export const fetchPermission = async () => { - return get('/workspaces/current/plugin/permission/fetch') -} - export const updatePermission = async (permissions: Permissions) => { return post('/workspaces/current/plugin/permission/change', { body: permissions }) } diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 0f069f65cc..754cd1cc2d 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,6 +1,7 @@ -import type { DebugInfo as DebugInfoTypes, InstalledPluginListResponse } from '@/app/components/plugins/types' -import { get } from './base' +import type { DebugInfo as DebugInfoTypes, InstalledPluginListResponse, Permissions } from '@/app/components/plugins/types' +import { get, post } from './base' import { + useMutation, useQueryClient, } from '@tanstack/react-query' @@ -34,3 +35,34 @@ export const useDebugKey = () => { queryFn: () => get('/workspaces/current/plugin/debugging-key'), }) } + +const usePermissionsKey = [NAME_SPACE, 'permissions'] +export const usePermissions = () => { + return useQuery({ + queryKey: usePermissionsKey, + queryFn: () => get('/workspaces/current/plugin/permission/fetch'), + }) +} + +export const useInvalidatePermissions = () => { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries( + { + queryKey: usePermissionsKey, + }) + } +} + +export const useMutationPermissions = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationFn: (payload: Permissions) => { + return post('/workspaces/current/plugin/permission/change', { body: payload }) + }, + onSuccess, + }) +} From d13169934da1206fbebcf400384a74688dc02579 Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Fri, 8 Nov 2024 16:13:52 +0800 Subject: [PATCH 02/16] fix: marketplace i18n --- .../plugins/marketplace/description/index.tsx | 8 ++++---- .../plugins/marketplace/plugin-type-switch.tsx | 2 +- web/app/components/tools/marketplace/index.tsx | 10 ++++++---- web/i18n/en-US/marketplace.ts | 10 ---------- web/i18n/en-US/plugin.ts | 8 ++++++++ web/i18n/zh-Hans/marketplace.ts | 10 ---------- web/i18n/zh-Hans/plugin.ts | 8 ++++++++ 7 files changed, 27 insertions(+), 29 deletions(-) delete mode 100644 web/i18n/en-US/marketplace.ts delete mode 100644 web/i18n/zh-Hans/marketplace.ts diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index 3b0454c3c6..9e3f9774b5 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -15,10 +15,10 @@ const Description = async ({ return ( <>

- Empower your AI development + {t('marketplace.empower')}

- Discover + {t('marketplace.discover')} {t('category.models')} @@ -30,11 +30,11 @@ const Description = async ({ {t('category.extensions')} - and + {t('marketplace.and')} {t('category.bundles')} - in Dify Marketplace + {t('marketplace.inDifyMarketplace')}

) diff --git a/web/app/components/plugins/marketplace/plugin-type-switch.tsx b/web/app/components/plugins/marketplace/plugin-type-switch.tsx index 6a44524a0c..82758ad87d 100644 --- a/web/app/components/plugins/marketplace/plugin-type-switch.tsx +++ b/web/app/components/plugins/marketplace/plugin-type-switch.tsx @@ -30,7 +30,7 @@ const PluginTypeSwitch = ({ const options = [ { value: PLUGIN_TYPE_SEARCH_MAP.all, - text: 'All', + text: t('plugin.category.all'), icon: null, }, { diff --git a/web/app/components/tools/marketplace/index.tsx b/web/app/components/tools/marketplace/index.tsx index f2092227a0..c50b898362 100644 --- a/web/app/components/tools/marketplace/index.tsx +++ b/web/app/components/tools/marketplace/index.tsx @@ -31,9 +31,11 @@ const Marketplace = ({ onClick={() => onMarketplaceScroll()} />
-
More from Marketplace
+
+ {t('plugin.marketplace.moreFrom')} +
- Discover + {t('plugin.marketplace.discover')} {t('plugin.category.models')} @@ -45,11 +47,11 @@ const Marketplace = ({ {t('plugin.category.extensions')} - and + {t('plugin.marketplace.and')} {t('plugin.category.bundles')} - in Dify Marketplace + {t('plugin.marketplace.inDifyMarketplace')}
{ diff --git a/web/i18n/en-US/marketplace.ts b/web/i18n/en-US/marketplace.ts deleted file mode 100644 index 4a53d94e57..0000000000 --- a/web/i18n/en-US/marketplace.ts +++ /dev/null @@ -1,10 +0,0 @@ -const translation = { - plugins: { - title: 'Plugins', - }, - discover: { - title: 'Explore Marketplace', - }, -} - -export default translation diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 6d6e0d57c0..5c3bdc6f29 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -1,5 +1,6 @@ const translation = { category: { + all: 'All', models: 'models', tools: 'tools', extensions: 'extensions', @@ -116,6 +117,13 @@ const translation = { error: { inValidGitHubUrl: 'Invalid GitHub URL. Please enter a valid URL in the format: https://github.com/owner/repo', }, + marketplace: { + empower: 'Empower your AI development', + discover: 'Discover', + and: 'and', + inDifyMarketplace: 'in Dify Marketplace', + moreFrom: 'More from Marketplace', + }, } export default translation diff --git a/web/i18n/zh-Hans/marketplace.ts b/web/i18n/zh-Hans/marketplace.ts deleted file mode 100644 index ba3b2a192f..0000000000 --- a/web/i18n/zh-Hans/marketplace.ts +++ /dev/null @@ -1,10 +0,0 @@ -const translation = { - plugins: { - title: '插件', - }, - discover: { - title: '探索市场', - }, -} - -export default translation diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index 8b13e63aee..c1f7bb7600 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -1,5 +1,6 @@ const translation = { category: { + all: '全部', models: '模型', tools: '工具', extensions: '扩展', @@ -116,6 +117,13 @@ const translation = { error: { inValidGitHubUrl: '无效的 GitHub URL。请输入格式为 https://github.com/owner/repo 的有效 URL', }, + marketplace: { + empower: '助力您的 AI 开发', + discover: '探索', + and: '和', + inDifyMarketplace: '在 Dify 市场中', + moreFrom: '更多来自市场', + }, } export default translation From f498686c3a903583688922285757db12bd870aa6 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 8 Nov 2024 13:16:02 +0800 Subject: [PATCH 03/16] model list style fix --- web/app/components/plugins/plugin-detail-panel/model-list.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/plugins/plugin-detail-panel/model-list.tsx b/web/app/components/plugins/plugin-detail-panel/model-list.tsx index 0d79d73020..2704ea741f 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-list.tsx @@ -21,7 +21,7 @@ const ModelList = () => { return (
{t('plugin.detailPanel.modelNum', { num: res.data.length })}
-
+
{res.data.map(model => (
Date: Fri, 8 Nov 2024 16:11:19 +0800 Subject: [PATCH 04/16] endpoints api --- .../plugin-detail-panel/endpoint-card.tsx | 37 ++++-- .../plugin-detail-panel/endpoint-list.tsx | 28 +++-- .../plugins/plugin-detail-panel/index.tsx | 2 +- .../plugins/plugin-detail-panel/mock.ts | 105 ------------------ .../plugins/plugin-detail-panel/utils.ts | 21 ++++ web/app/components/tools/types.ts | 2 +- web/service/plugins.ts | 4 +- 7 files changed, 74 insertions(+), 125 deletions(-) delete mode 100644 web/app/components/plugins/plugin-detail-panel/mock.ts create mode 100644 web/app/components/plugins/plugin-detail-panel/utils.ts diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index 948cbdd225..1a984b4eda 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -4,12 +4,14 @@ import { useBoolean } from 'ahooks' import { RiDeleteBinLine, RiEditLine, RiLoginCircleLine } from '@remixicon/react' import type { EndpointListItem } from '../types' import EndpointModal from './endpoint-modal' +import { NAME_FIELD } from './utils' import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import ActionButton from '@/app/components/base/action-button' import CopyBtn from '@/app/components/base/copy-btn' import Confirm from '@/app/components/base/confirm' import Indicator from '@/app/components/header/indicator' import Switch from '@/app/components/base/switch' +import Toast from '@/app/components/base/toast' import { deleteEndpoint, disableEndpoint, @@ -19,10 +21,12 @@ import { type Props = { data: EndpointListItem + handleChange: () => void } const EndpointCard = ({ data, + handleChange, }: Props) => { const { t } = useTranslation() const [active, setActive] = useState(data.enabled) @@ -38,9 +42,11 @@ const EndpointCard = ({ url: '/workspaces/current/endpoints/enable', endpointID, }) + await handleChange() } - catch (error) { + catch (error: any) { console.error(error) + Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) setActive(false) } } @@ -50,9 +56,12 @@ const EndpointCard = ({ url: '/workspaces/current/endpoints/disable', endpointID, }) + await handleChange() + hideDisableConfirm() } catch (error) { console.error(error) + Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) setActive(true) } } @@ -77,9 +86,12 @@ const EndpointCard = ({ url: '/workspaces/current/endpoints/delete', endpointID, }) + await handleChange() + hideDeleteConfirm() } catch (error) { console.error(error) + Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } } @@ -89,25 +101,34 @@ const EndpointCard = ({ }] = useBoolean(false) const formSchemas = useMemo(() => { - return toolCredentialToFormSchemas(data.declaration.settings) + return toolCredentialToFormSchemas([NAME_FIELD, ...data.declaration.settings]) }, [data.declaration.settings]) const formValue = useMemo(() => { - return addDefaultValue(data.settings, formSchemas) - }, [data.settings, formSchemas]) + const formValue = { + name: data.name, + ...data.settings, + } + return addDefaultValue(formValue, formSchemas) + }, [data.name, data.settings, formSchemas]) - const handleUpdate = (state: any) => { + const handleUpdate = async (state: any) => { + const newName = state.name + delete state.name try { - updateEndpoint({ - url: '/workspaces/current/endpoints', + await updateEndpoint({ + url: '/workspaces/current/endpoints/update', body: { endpoint_id: data.id, settings: state, - name: state.name, + name: newName, }, }) + await handleChange() + hideEndpointModalConfirm() } catch (error) { console.error(error) + Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } } diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index 495d451f90..6323e42365 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -5,21 +5,27 @@ import { useBoolean } from 'ahooks' import { RiAddLine } from '@remixicon/react' import EndpointModal from './endpoint-modal' import EndpointCard from './endpoint-card' +import { NAME_FIELD } from './utils' import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import ActionButton from '@/app/components/base/action-button' import Tooltip from '@/app/components/base/tooltip' +import Toast from '@/app/components/base/toast' import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import { createEndpoint, fetchEndpointList, } from '@/service/plugins' +import cn from '@/utils/classnames' -const EndpointList = () => { +type Props = { + showTopBorder?: boolean +} +const EndpointList = ({ showTopBorder }: Props) => { const { t } = useTranslation() const pluginDetail = usePluginPageContext(v => v.currentPluginDetail) const pluginUniqueID = pluginDetail.plugin_unique_identifier const declaration = pluginDetail.declaration.endpoint - const { data } = useSWR( + const { data, mutate } = useSWR( { url: '/workspaces/current/endpoints/list/plugin', params: { @@ -36,22 +42,27 @@ const EndpointList = () => { }] = useBoolean(false) const formSchemas = useMemo(() => { - return toolCredentialToFormSchemas(declaration.settings) + return toolCredentialToFormSchemas([NAME_FIELD, ...declaration.settings]) }, [declaration.settings]) - const handleCreate = (state: any) => { + const handleCreate = async (state: any) => { + const newName = state.name + delete state.name try { - createEndpoint({ - url: '/workspaces/current/endpoints', + await createEndpoint({ + url: '/workspaces/current/endpoints/create', body: { plugin_unique_identifier: pluginUniqueID, settings: state, - name: state.name, + name: newName, }, }) + await mutate() + hideEndpointModal() } catch (error) { console.error(error) + Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) } } @@ -59,7 +70,7 @@ const EndpointList = () => { return null return ( -
+
{t('plugin.detailPanel.endpoints')} @@ -81,6 +92,7 @@ const EndpointList = () => { ))}
diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index 0dd85a4f87..7bd1849625 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -42,8 +42,8 @@ const PluginDetailPanel: FC = ({ onDelete={onDelete} />
- {!!pluginDetail.declaration.endpoint && } {!!pluginDetail.declaration.tool && } + {!!pluginDetail.declaration.endpoint && } {!!pluginDetail.declaration.model && }
diff --git a/web/app/components/plugins/plugin-detail-panel/mock.ts b/web/app/components/plugins/plugin-detail-panel/mock.ts deleted file mode 100644 index dffe753922..0000000000 --- a/web/app/components/plugins/plugin-detail-panel/mock.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { PluginSource, PluginType } from '../types' - -export const toolNotion = { - id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld', - created_at: '2024-10-16 16:05:33', - updated_at: '2024-10-16 16:05:33', - name: 'notion page search', - plugin_id: 'Notion/notion-page-search', - plugin_unique_identifier: 'Notion/notion-page-search:1.2.0@fldsjflkdsajfldsakajfkls', - declaration: { - version: '1.2.0', - author: 'Notion', - name: 'notion page search', - category: PluginType.tool, - icon: 'https://via.placeholder.com/150', - label: { - 'en-US': 'Notion Page Search', - 'zh-Hans': 'Notion 页面搜索', - }, - description: { - 'en-US': 'Description: Search Notion pages and open visited ones faster. No admin access required.More and more info...More and more info...More and more info...', - 'zh-Hans': '搜索 Notion 页面并更快地打开已访问的页面。无需管理员访问权限。More and more info...More and more info...More and more info...', - }, - created_at: '2024-10-16 16:05:33', - resource: {}, - plugins: {}, - endpoint: { - settings: [ - { - type: 'secret-input', - name: 'api-key', - required: true, - default: null, - options: null, - label: { - en_US: 'API-key', - zh_Hans: 'API-key', - }, - help: null, - url: null, - placeholder: { - en_US: 'Please input your API key', - zh_Hans: '请输入你的 API key', - }, - }, - ], - endpoints: [ - { path: '/duck/', method: 'GET' }, - { path: '/neko', method: 'GET' }, - ], - }, - tool: null, // TODO - verified: true, - }, - installation_id: 'jflkdsjoewingljlsadjgoijg-dkfjldajglkajglask-dlfkajdg', - tenant_id: 'jflkdsjoewingljlsadjgoijg', - endpoints_setups: 2, - endpoints_active: 1, - version: '1.2.0', - source: PluginSource.marketplace, - meta: null, -} - -export const toolNotionEndpoints = [ - { - id: 'dlfajkgjdga-dfjalksjfglkds-dfjakld', - created_at: '2024-10-16 16:05:33', - updated_at: '2024-10-16 16:05:33', - settings: { - 'api-key': '*******', - }, - tenant_id: 'jflkdsjoewingljlsadjgoijg', - plugin_id: 'Notion/notion-page-search', - expired_at: '2024-10-16 16:05:33', - declaration: { - settings: [ - { - type: 'secret-input', - name: 'api-key', - required: true, - default: null, - options: null, - label: { - en_US: 'API-key', - zh_Hans: 'API-key', - }, - help: null, - url: null, - placeholder: { - en_US: 'Please input your API key', - zh_Hans: '请输入你的 API key', - }, - }, - ], - endpoints: [ - { path: '/duck/', method: 'GET' }, - { path: '/neko', method: 'GET' }, - ], - }, - name: 'default', - enabled: true, - url: 'http://localhost:5002/e/45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8', - hook_id: '45rj9V4TRxAjL0I2wXRZgZdXjdHEKBh8', - }, -] diff --git a/web/app/components/plugins/plugin-detail-panel/utils.ts b/web/app/components/plugins/plugin-detail-panel/utils.ts new file mode 100644 index 0000000000..fd51142a38 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/utils.ts @@ -0,0 +1,21 @@ +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' + +export const NAME_FIELD = { + type: FormTypeEnum.textInput, + name: 'name', + label: { + en_US: 'Endpoint Name', + zh_Hans: '端点名称', + ja_JP: 'エンドポイント名', + pt_BR: 'Nome do ponto final', + }, + placeholder: { + en_US: 'Endpoint Name', + zh_Hans: '端点名称', + ja_JP: 'エンドポイント名', + pt_BR: 'Nome do ponto final', + }, + required: true, + default: '', + help: null, +} diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 9e899489bb..34cc491481 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -80,7 +80,7 @@ export type Tool = { export type ToolCredential = { name: string label: TypeWithI18N - help: TypeWithI18N + help: TypeWithI18N | null placeholder: TypeWithI18N type: string required: boolean diff --git a/web/service/plugins.ts b/web/service/plugins.ts index 6d246e9239..b7d5125f2e 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -1,5 +1,5 @@ import type { Fetcher } from 'swr' -import { del, get, getMarketplace, post, upload } from './base' +import { get, getMarketplace, post, upload } from './base' import type { CreateEndpointRequest, EndpointOperationResponse, @@ -32,7 +32,7 @@ export const fetchEndpointList: Fetcher = ({ url, endpointID }) => { // url = /workspaces/current/endpoints/delete - return del(url, { body: { endpoint_id: endpointID } }) + return post(url, { body: { endpoint_id: endpointID } }) } export const updateEndpoint: Fetcher = ({ url, body }) => { From 1f1c61541e821dd0a05a1d64f05dc83c9670cb57 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 8 Nov 2024 16:33:00 +0800 Subject: [PATCH 05/16] install from settings --- .../header/account-setting/model-provider-page/index.tsx | 2 +- web/app/components/plugins/provider-card.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 7faf3f3de7..f807bd7922 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -222,7 +222,7 @@ const ModelProviderPage = ({ searchText }: Props) => { {!collapse && !isPluginsLoading && (
{plugins.map(plugin => ( - + ))}
)} diff --git a/web/app/components/plugins/provider-card.tsx b/web/app/components/plugins/provider-card.tsx index 5c8ab1891e..e2a45fc24d 100644 --- a/web/app/components/plugins/provider-card.tsx +++ b/web/app/components/plugins/provider-card.tsx @@ -19,11 +19,13 @@ import { useBoolean } from 'ahooks' type Props = { className?: string payload: Plugin + onSuccess: () => void } const ProviderCard: FC = ({ className, payload, + onSuccess, }) => { const { t } = useTranslation() const [isShowInstallFromMarketplace, { @@ -84,7 +86,10 @@ const ProviderCard: FC = ({ manifest={payload as any} uniqueIdentifier={payload.latest_package_identifier} onClose={hideInstallFromMarketplace} - onSuccess={hideInstallFromMarketplace} + onSuccess={() => { + onSuccess() + hideInstallFromMarketplace() + }} /> ) } From ebdf72fffcbd4332974fbc77114597cdad9ba2ef Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 8 Nov 2024 17:01:49 +0800 Subject: [PATCH 06/16] check update --- .../plugin-detail-panel/detail-header.tsx | 60 ++++++++++++++++--- .../plugins/plugin-detail-panel/index.tsx | 6 +- .../operation-dropdown.tsx | 51 +++++++++------- .../plugins/plugin-page/plugins-panel.tsx | 2 +- 4 files changed, 88 insertions(+), 31 deletions(-) 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 ad9760feff..b40f264967 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header.tsx @@ -13,6 +13,8 @@ import Description from '../card/base/description' import Icon from '../card/base/card-icon' import Title from '../card/base/title' import OrgInfo from '../card/base/org-info' +import { useGitHubReleases } from '../install-plugin/hooks' +import { compareVersion, getLatestVersion } from '@/utils/semver' import OperationDropdown from './operation-dropdown' import PluginInfo from '@/app/components/plugins/plugin-page/plugin-info' import ActionButton from '@/app/components/base/action-button' @@ -20,10 +22,13 @@ import Button from '@/app/components/base/button' import Badge from '@/app/components/base/badge' import Confirm from '@/app/components/base/confirm' import Tooltip from '@/app/components/base/tooltip' +import Toast from '@/app/components/base/toast' import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin' import { Github } from '@/app/components/base/icons/src/public/common' import { uninstallPlugin } from '@/service/plugins' import { useGetLanguage } from '@/context/i18n' +import { useModalContext } from '@/context/modal-context' + import { API_PREFIX, MARKETPLACE_URL_PREFIX } from '@/config' import cn from '@/utils/classnames' @@ -32,16 +37,18 @@ const i18nPrefix = 'plugin.action' type Props = { detail: PluginDetail onHide: () => void - onDelete: () => void + onUpdate: () => void } const DetailHeader = ({ detail, onHide, - onDelete, + onUpdate, }: Props) => { const { t } = useTranslation() const locale = useGetLanguage() + const { fetchReleases } = useGitHubReleases() + const { setShowUpdatePluginModal } = useModalContext() const { installation_id, @@ -53,13 +60,51 @@ const DetailHeader = ({ } = detail const { author, name, label, description, icon, verified } = detail.declaration const isFromGitHub = source === PluginSource.github - // Only plugin installed from GitHub need to check if it's the new version + const hasNewVersion = useMemo(() => { return source === PluginSource.github && latest_version !== version }, [source, latest_version, version]) - // #plugin TODO# update plugin - const handleUpdate = () => { } + const handleUpdate = async () => { + try { + const fetchedReleases = await fetchReleases(author, name) + if (fetchedReleases.length === 0) + return + const versions = fetchedReleases.map(release => release.tag_name) + const latestVersion = getLatestVersion(versions) + if (compareVersion(latestVersion, version) === 1) { + setShowUpdatePluginModal({ + onSaveCallback: () => { + onUpdate() + }, + payload: { + type: PluginSource.github, + github: { + originalPackageInfo: { + id: installation_id, + repo: meta!.repo, + version: meta!.version, + package: meta!.package, + releases: fetchedReleases, + }, + }, + }, + }) + } + else { + Toast.notify({ + type: 'info', + message: 'No new version available', + }) + } + } + catch { + Toast.notify({ + type: 'error', + message: 'Failed to compare versions', + }) + } + } const [isShowPluginInfo, { setTrue: showPluginInfo, @@ -82,9 +127,9 @@ const DetailHeader = ({ hideDeleting() if (res.success) { hideDeleteConfirm() - onDelete() + onUpdate() } - }, [hideDeleteConfirm, hideDeleting, installation_id, showDeleting, onDelete]) + }, [hideDeleteConfirm, hideDeleting, installation_id, showDeleting, onUpdate]) // #plugin TODO# used in apps // const usedInApps = 3 @@ -141,6 +186,7 @@ const DetailHeader = ({
void + onUpdate: () => void } const PluginDetailPanel: FC = ({ - onDelete, + onUpdate, }) => { const pluginDetail = usePluginPageContext(v => v.currentPluginDetail) const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail) @@ -39,7 +39,7 @@ const PluginDetailPanel: FC = ({
{!!pluginDetail.declaration.tool && } diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx index b23b29d462..05988c181d 100644 --- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import React, { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { PluginSource } from '../types' import { RiArrowRightUpLine, RiMoreFill } from '@remixicon/react' import ActionButton from '@/app/components/base/action-button' // import Button from '@/app/components/base/button' @@ -13,6 +14,7 @@ import { import cn from '@/utils/classnames' type Props = { + source: PluginSource onInfo: () => void onCheckVersion: () => void onRemove: () => void @@ -20,10 +22,11 @@ type Props = { } const OperationDropdown: FC = ({ + source, + detailUrl, onInfo, onCheckVersion, onRemove, - detailUrl, }) => { const { t } = useTranslation() const [open, doSetOpen] = useState(false) @@ -56,25 +59,33 @@ const OperationDropdown: FC = ({
-
{ - onInfo() - handleTrigger() - }} - className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' - >{t('plugin.detailPanel.operation.info')}
-
{ - onCheckVersion() - handleTrigger() - }} - className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' - >{t('plugin.detailPanel.operation.checkUpdate')}
- - {t('plugin.detailPanel.operation.viewDetail')} - - -
+ {source === PluginSource.github && ( +
{ + onInfo() + handleTrigger() + }} + className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' + >{t('plugin.detailPanel.operation.info')}
+ )} + {source === PluginSource.github && ( +
{ + onCheckVersion() + handleTrigger() + }} + className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' + >{t('plugin.detailPanel.operation.checkUpdate')}
+ )} + {source === PluginSource.marketplace && ( + + {t('plugin.detailPanel.operation.viewDetail')} + + + )} + {(source === PluginSource.marketplace || source === PluginSource.github) && ( +
+ )}
{ onRemove() diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index 466df72066..198b418f7c 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -48,7 +48,7 @@ const PluginsPanel = () => { ) : ( )} - invalidateInstalledPluginList()}/> + invalidateInstalledPluginList()}/> ) } From d4f7ebfd2e82daf58dddd217a3a88366de824d5e Mon Sep 17 00:00:00 2001 From: AkaraChen Date: Fri, 8 Nov 2024 17:21:55 +0800 Subject: [PATCH 07/16] feat: refactor http client --- web/package.json | 3 +- web/pnpm-lock.yaml | 9 ++ web/service/base.ts | 191 +++---------------------------------------- web/service/fetch.ts | 187 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 181 deletions(-) create mode 100644 web/service/fetch.ts diff --git a/web/package.json b/web/package.json index 8d69bbc209..7cbe778790 100644 --- a/web/package.json +++ b/web/package.json @@ -64,6 +64,7 @@ "js-cookie": "^3.0.5", "jwt-decode": "^4.0.0", "katex": "^0.16.11", + "ky": "^1.7.2", "lamejs": "^1.2.1", "lexical": "^0.18.0", "lodash-es": "^4.17.21", @@ -84,9 +85,9 @@ "react-hook-form": "^7.53.1", "react-i18next": "^15.1.0", "react-infinite-scroll-component": "^6.1.0", + "react-markdown": "^9.0.1", "react-multi-email": "^1.0.25", "react-papaparse": "^4.4.0", - "react-markdown": "^9.0.1", "react-slider": "^2.0.6", "react-sortablejs": "^6.1.4", "react-syntax-highlighter": "^15.6.1", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 1a98266fdc..236c23c9c6 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: katex: specifier: ^0.16.11 version: 0.16.11 + ky: + specifier: ^1.7.2 + version: 1.7.2 lamejs: specifier: ^1.2.1 version: 1.2.1 @@ -5612,6 +5615,10 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + ky@1.7.2: + resolution: {integrity: sha512-OzIvbHKKDpi60TnF9t7UUVAF1B4mcqc02z5PIvrm08Wyb+yOcz63GRvEuVxNT18a9E1SrNouhB4W2NNLeD7Ykg==} + engines: {node: '>=18'} + lamejs@1.2.1: resolution: {integrity: sha512-s7bxvjvYthw6oPLCm5pFxvA84wUROODB8jEO2+CE1adhKgrIvVOlmMgY8zyugxGrvRaDHNJanOiS21/emty6dQ==} @@ -14744,6 +14751,8 @@ snapshots: kleur@4.1.5: {} + ky@1.7.2: {} + lamejs@1.2.1: dependencies: use-strict: 1.0.1 diff --git a/web/service/base.ts b/web/service/base.ts index e1a04217c7..0f9059e617 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -1,4 +1,4 @@ -import { API_PREFIX, IS_CE_EDITION, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config' +import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config' import { refreshAccessTokenOrRelogin } from './refresh-token' import Toast from '@/app/components/base/toast' import type { AnnotationReply, MessageEnd, MessageReplace, ThoughtItem } from '@/app/components/base/chat/chat/type' @@ -17,27 +17,10 @@ import type { WorkflowStartedResponse, } from '@/types/workflow' import { removeAccessToken } from '@/app/components/share/utils' +import type { FetchOptionType, ResponseError } from './fetch' +import { ContentType, base, baseOptions, getPublicToken } from './fetch' const TIME_OUT = 100000 -const ContentType = { - json: 'application/json', - stream: 'text/event-stream', - audio: 'audio/mpeg', - form: 'application/x-www-form-urlencoded; charset=UTF-8', - download: 'application/octet-stream', // for download - upload: 'multipart/form-data', // for upload -} - -const baseOptions = { - method: 'GET', - mode: 'cors', - credentials: 'include', // always send cookies、HTTP Basic authentication. - headers: new Headers({ - 'Content-Type': ContentType.json, - }), - redirect: 'follow', -} - export type IOnDataMoreInfo = { conversationId?: string taskId?: string @@ -100,17 +83,6 @@ export type IOtherOptions = { onTextReplace?: IOnTextReplace } -type ResponseError = { - code: string - message: string - status: number -} - -type FetchOptionType = Omit & { - params?: Record - body?: BodyInit | Record | null -} - function unicodeToChar(text: string) { if (!text) return '' @@ -277,153 +249,13 @@ const handleStream = ( read() } -const baseFetch = ( - url: string, - fetchOptions: FetchOptionType, - { - isPublicAPI = false, - isMarketplaceAPI = false, - bodyStringify = true, - needAllResponseContent, - deleteContentType, - getAbortController, - silent, - }: IOtherOptions, -): Promise => { - const options: typeof baseOptions & FetchOptionType = Object.assign({}, baseOptions, fetchOptions) - if (isMarketplaceAPI) - options.credentials = 'omit' - - if (getAbortController) { - const abortController = new AbortController() - getAbortController(abortController) - options.signal = abortController.signal - } - - if (isPublicAPI) { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] - const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) - let accessTokenJson = { [sharedToken]: '' } - try { - accessTokenJson = JSON.parse(accessToken) - } - catch (e) { - - } - options.headers.set('Authorization', `Bearer ${accessTokenJson[sharedToken]}`) - } - else if (!isMarketplaceAPI) { - const accessToken = localStorage.getItem('console_token') || '' - options.headers.set('Authorization', `Bearer ${accessToken}`) - } - - if (deleteContentType) { - options.headers.delete('Content-Type') - } - else { - const contentType = options.headers.get('Content-Type') - if (!contentType) - options.headers.set('Content-Type', ContentType.json) - } - - const urlPrefix = (() => { - if (isMarketplaceAPI) - return MARKETPLACE_API_PREFIX - if (isPublicAPI) - return PUBLIC_API_PREFIX - return API_PREFIX - })() - let urlWithPrefix = `${urlPrefix}${url.startsWith('/') ? url : `/${url}`}` - - const { method, params, body } = options - // handle query - if (method === 'GET' && params) { - const paramsArray: string[] = [] - Object.keys(params).forEach(key => - paramsArray.push(`${key}=${encodeURIComponent(params[key])}`), - ) - if (urlWithPrefix.search(/\?/) === -1) - urlWithPrefix += `?${paramsArray.join('&')}` - - else - urlWithPrefix += `&${paramsArray.join('&')}` - - delete options.params - } - - if (body && bodyStringify) - options.body = JSON.stringify(body) - - // Handle timeout - return Promise.race([ - new Promise((resolve, reject) => { - setTimeout(() => { - reject(new Error('request timeout')) - }, TIME_OUT) - }), - new Promise((resolve, reject) => { - globalThis.fetch(urlWithPrefix, options as RequestInit) - .then((res) => { - const resClone = res.clone() - // Error handler - if (!/^(2|3)\d{2}$/.test(String(res.status))) { - const bodyJson = res.json() - switch (res.status) { - case 401: - return Promise.reject(resClone) - case 403: - bodyJson.then((data: ResponseError) => { - if (!silent) - Toast.notify({ type: 'error', message: data.message }) - if (data.code === 'already_setup') - globalThis.location.href = `${globalThis.location.origin}/signin` - }) - break - // fall through - default: - bodyJson.then((data: ResponseError) => { - if (!silent) - Toast.notify({ type: 'error', message: data.message }) - }) - } - return Promise.reject(resClone) - } - - // handle delete api. Delete api not return content. - if (res.status === 204) { - resolve({ result: 'success' }) - return - } - - // return data - if (options.headers.get('Content-type') === ContentType.download || options.headers.get('Content-type') === ContentType.audio) - resolve(needAllResponseContent ? resClone : res.blob()) - - else resolve(needAllResponseContent ? resClone : res.json()) - }) - .catch((err) => { - if (!silent) - Toast.notify({ type: 'error', message: err }) - reject(err) - }) - }), - ]) as Promise -} +const baseFetch = base export const upload = (options: any, isPublicAPI?: boolean, url?: string, searchParams?: string): Promise => { const urlPrefix = isPublicAPI ? PUBLIC_API_PREFIX : API_PREFIX let token = '' if (isPublicAPI) { - const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] - const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) - let accessTokenJson = { [sharedToken]: '' } - try { - accessTokenJson = JSON.parse(accessToken) - } - catch (e) { - - } - token = accessTokenJson[sharedToken] + token = getPublicToken() } else { const accessToken = localStorage.getItem('console_token') || '' @@ -499,9 +331,9 @@ export const ssePost = ( signal: abortController.signal, }, fetchOptions) - const contentType = options.headers.get('Content-Type') + const contentType = (options.headers as Headers).get('Content-Type') if (!contentType) - options.headers.set('Content-Type', ContentType.json) + (options.headers as Headers).set('Content-Type', ContentType.json) getAbortController?.(abortController) @@ -559,18 +391,17 @@ export const ssePost = ( } // base request -export const request = (url: string, options = {}, otherOptions?: IOtherOptions) => { +export const request = (url: string, options = {}, otherOptions: IOtherOptions = {}) => { return new Promise((resolve, reject) => { - const otherOptionsForBaseFetch = otherOptions || {} - baseFetch(url, options, otherOptionsForBaseFetch).then(resolve).catch((errResp) => { + baseFetch(url, options, otherOptions).then(resolve).catch((errResp) => { if (errResp?.status === 401) { return refreshAccessTokenOrRelogin(TIME_OUT).then(() => { - baseFetch(url, options, otherOptionsForBaseFetch).then(resolve).catch(reject) + baseFetch(url, options, otherOptions).then(resolve).catch(reject) }).catch(() => { const { isPublicAPI = false, silent, - } = otherOptionsForBaseFetch + } = otherOptions const bodyJson = errResp.json() if (isPublicAPI) { return bodyJson.then((data: ResponseError) => { diff --git a/web/service/fetch.ts b/web/service/fetch.ts new file mode 100644 index 0000000000..46e0e6295d --- /dev/null +++ b/web/service/fetch.ts @@ -0,0 +1,187 @@ +import type { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Hooks } from 'ky' +import ky from 'ky' +import type { IOtherOptions } from './base' +import Toast from '@/app/components/base/toast' +import { API_PREFIX, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config' + +const TIME_OUT = 100000 + +export const ContentType = { + json: 'application/json', + stream: 'text/event-stream', + audio: 'audio/mpeg', + form: 'application/x-www-form-urlencoded; charset=UTF-8', + download: 'application/octet-stream', // for download + upload: 'multipart/form-data', // for upload +} + +export type FetchOptionType = Omit & { + params?: Record + body?: BodyInit | Record | null +} + +const afterResponse204: AfterResponseHook = async (_request, _options, response) => { + if (response.status === 204) return Response.json({ result: 'success' }) +} + +export type ResponseError = { + code: string + message: string + status: number +} + +const afterResponseErrorCode = (otherOptions: IOtherOptions): AfterResponseHook => { + return async (_request, _options, response) => { + if (!/^(2|3)\d{2}$/.test(String(response.status))) { + const bodyJson = response.json() as Promise + switch (response.status) { + case 401: + return Promise.reject(response) + case 403: + bodyJson.then((data: ResponseError) => { + if (!otherOptions.silent) + Toast.notify({ type: 'error', message: data.message }) + if (data.code === 'already_setup') + globalThis.location.href = `${globalThis.location.origin}/signin` + }) + break + // fall through + default: + bodyJson.then((data: ResponseError) => { + if (!otherOptions.silent) + Toast.notify({ type: 'error', message: data.message }) + }) + } + throw response + } + } +} + +const beforeErrorToast = (otherOptions: IOtherOptions): BeforeErrorHook => { + return (error) => { + if (!otherOptions.silent) + Toast.notify({ type: 'error', message: error.message }) + return error + } +} + +export const getPublicToken = () => { + let token = '' + const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0] + const accessToken = localStorage.getItem('token') || JSON.stringify({ [sharedToken]: '' }) + let accessTokenJson = { [sharedToken]: '' } + try { + accessTokenJson = JSON.parse(accessToken) + } + catch {} + token = accessTokenJson[sharedToken] + return token || '' +} + +const beforeRequestPublicAuthorization: BeforeRequestHook = (request) => { + const token = getPublicToken() + request.headers.set('Authorization', `Bearer ${token}`) +} + +const beforeRequestAuthorization: BeforeRequestHook = (request) => { + const accessToken = localStorage.getItem('console_token') || '' + request.headers.set('Authorization', `Bearer ${accessToken}`) +} + +const beforeRequestDeleteContentType: BeforeRequestHook = (request) => { + request.headers.delete('Content-Type') +} + +const baseHooks: Hooks = { + afterResponse: [ + afterResponse204, + ], +} + +const client = ky.create({ + hooks: baseHooks, + timeout: TIME_OUT, +}) + +export const baseOptions: RequestInit = { + method: 'GET', + mode: 'cors', + credentials: 'include', // always send cookies、HTTP Basic authentication. + headers: new Headers({ + 'Content-Type': ContentType.json, + }), + redirect: 'follow', +} + +async function base(url: string, options: FetchOptionType = {}, otherOptions: IOtherOptions = {}): Promise { + const { params, body, ...init } = Object.assign({}, baseOptions, options) + const { + isPublicAPI = false, + isMarketplaceAPI = false, + bodyStringify = true, + needAllResponseContent, + deleteContentType, + getAbortController, + } = otherOptions + + const base + = isMarketplaceAPI + ? MARKETPLACE_API_PREFIX + : isPublicAPI + ? PUBLIC_API_PREFIX + : API_PREFIX + + if (getAbortController) { + const abortController = new AbortController() + getAbortController(abortController) + options.signal = abortController.signal + } + + const fetchPathname = `${base}${url.startsWith('/') ? url : `/${url}`}` + + const res = await client.extend({ + hooks: { + ...baseHooks, + beforeError: [ + ...baseHooks.beforeError || [], + beforeErrorToast(otherOptions), + ], + beforeRequest: [ + ...baseHooks.beforeRequest || [], + isPublicAPI && beforeRequestPublicAuthorization, + !isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization, + deleteContentType && beforeRequestDeleteContentType, + ].filter(i => !!i), + afterResponse: [ + ...baseHooks.afterResponse || [], + afterResponseErrorCode(otherOptions), + ], + }, + })(fetchPathname, { + credentials: isMarketplaceAPI + ? 'omit' + : (options.credentials || 'include'), + ...init, + retry: { + methods: [], + }, + ...(bodyStringify ? { json: body } : { body: body as BodyInit }), + searchParams: params, + }) + + if (needAllResponseContent) + return res as T + const contentType = res.headers.get('content-type') + if ( + contentType + && [ContentType.download, ContentType.audio].includes(contentType) + ) + return await res.blob() as T + + return await res.json() as T +} + +export { + client, + base, +} From 33349191e9e5cd88c24bfe7ef0d53c28e2d3c5c7 Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Fri, 8 Nov 2024 18:21:39 +0800 Subject: [PATCH 08/16] marketplace usemutation --- web/app/(commonLayout)/layout.tsx | 26 ++++++------ .../plugins/marketplace/context.tsx | 20 ++++----- .../components/plugins/marketplace/hooks.ts | 35 +++++++++------- .../components/plugins/marketplace/index.tsx | 27 ++++++------ web/app/components/plugins/types.ts | 4 ++ web/app/components/tools/marketplace/hooks.ts | 6 +-- web/service/base.ts | 5 +++ web/service/use-plugins.ts | 42 ++++++++++++++++--- 8 files changed, 105 insertions(+), 60 deletions(-) diff --git a/web/app/(commonLayout)/layout.tsx b/web/app/(commonLayout)/layout.tsx index ef07732997..f0f7e0321d 100644 --- a/web/app/(commonLayout)/layout.tsx +++ b/web/app/(commonLayout)/layout.tsx @@ -15,20 +15,20 @@ const Layout = ({ children }: { children: ReactNode }) => { <> - - - - - -
- - + + + + + + +
+ {children} - - - - - + + + + + ) diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index 0c87e32919..4c5752d45b 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -34,7 +34,7 @@ export type MarketplaceContextValue = { activePluginType: string handleActivePluginTypeChange: (type: string) => void plugins?: Plugin[] - setPlugins: (plugins: Plugin[]) => void + resetPlugins: () => void sort: PluginsSort handleSortChange: (sort: PluginsSort) => void marketplaceCollectionsFromClient?: MarketplaceCollection[] @@ -53,7 +53,7 @@ export const MarketplaceContext = createContext({ activePluginType: PLUGIN_TYPE_SEARCH_MAP.all, handleActivePluginTypeChange: () => {}, plugins: undefined, - setPlugins: () => {}, + resetPlugins: () => {}, sort: DEFAULT_SORT, handleSortChange: () => {}, marketplaceCollectionsFromClient: [], @@ -91,7 +91,7 @@ export const MarketplaceContextProvider = ({ } = useMarketplaceCollectionsAndPlugins() const { plugins, - setPlugins, + resetPlugins, queryPlugins, queryPluginsWithDebounced, } = useMarketplacePlugins() @@ -104,7 +104,7 @@ export const MarketplaceContextProvider = ({ queryMarketplaceCollectionsAndPlugins({ category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, }) - setPlugins(undefined) + resetPlugins() return } @@ -116,7 +116,7 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, }) - }, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, setPlugins]) + }, [queryPluginsWithDebounced, queryMarketplaceCollectionsAndPlugins, resetPlugins]) const handleFilterPluginTagsChange = useCallback((tags: string[]) => { setFilterPluginTags(tags) @@ -126,7 +126,7 @@ export const MarketplaceContextProvider = ({ queryMarketplaceCollectionsAndPlugins({ category: activePluginTypeRef.current === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : activePluginTypeRef.current, }) - setPlugins(undefined) + resetPlugins() return } @@ -138,7 +138,7 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, }) - }, [queryPlugins, setPlugins, queryMarketplaceCollectionsAndPlugins]) + }, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins]) const handleActivePluginTypeChange = useCallback((type: string) => { setActivePluginType(type) @@ -148,7 +148,7 @@ export const MarketplaceContextProvider = ({ queryMarketplaceCollectionsAndPlugins({ category: type === PLUGIN_TYPE_SEARCH_MAP.all ? undefined : type, }) - setPlugins(undefined) + resetPlugins() return } @@ -160,7 +160,7 @@ export const MarketplaceContextProvider = ({ sortBy: sortRef.current.sortBy, sortOrder: sortRef.current.sortOrder, }) - }, [queryPlugins, setPlugins, queryMarketplaceCollectionsAndPlugins]) + }, [queryPlugins, resetPlugins, queryMarketplaceCollectionsAndPlugins]) const handleSortChange = useCallback((sort: PluginsSort) => { setSort(sort) @@ -187,7 +187,7 @@ export const MarketplaceContextProvider = ({ activePluginType, handleActivePluginTypeChange, plugins, - setPlugins, + resetPlugins, sort, handleSortChange, marketplaceCollectionsFromClient, diff --git a/web/app/components/plugins/marketplace/hooks.ts b/web/app/components/plugins/marketplace/hooks.ts index 47ad603276..1cbc035972 100644 --- a/web/app/components/plugins/marketplace/hooks.ts +++ b/web/app/components/plugins/marketplace/hooks.ts @@ -4,7 +4,9 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' -import type { Plugin } from '../types' +import type { + Plugin, +} from '../types' import type { CollectionsAndPluginsSearchParams, MarketplaceCollection, @@ -12,9 +14,9 @@ import type { } from './types' import { getMarketplaceCollectionsAndPlugins, - getMarketplacePlugins, } from './utils' import i18n from '@/i18n/i18next-config' +import { useMutationPluginsFromMarketplace } from '@/service/use-plugins' export const useMarketplaceCollectionsAndPlugins = () => { const [isLoading, setIsLoading] = useState(false) @@ -41,28 +43,29 @@ export const useMarketplaceCollectionsAndPlugins = () => { } export const useMarketplacePlugins = () => { - const [isLoading, setIsLoading] = useState(false) - const [plugins, setPlugins] = useState() + const { + data, + mutate, + reset, + isPending, + } = useMutationPluginsFromMarketplace() - const queryPlugins = useCallback(async (query: PluginsSearchParams) => { - setIsLoading(true) - const { marketplacePlugins } = await getMarketplacePlugins(query) - setIsLoading(false) + const queryPlugins = useCallback((pluginsSearchParams: PluginsSearchParams) => { + mutate(pluginsSearchParams) + }, [mutate]) - setPlugins(marketplacePlugins) - }, []) - - const { run: queryPluginsWithDebounced } = useDebounceFn(queryPlugins, { + const { run: queryPluginsWithDebounced } = useDebounceFn((pluginsSearchParams) => { + mutate(pluginsSearchParams) + }, { wait: 500, }) return { - plugins, - setPlugins, + plugins: data?.data?.plugins, + resetPlugins: reset, queryPlugins, queryPluginsWithDebounced, - isLoading, - setIsLoading, + isLoading: isPending, } } diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 742df86ea0..10e623710e 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -5,6 +5,7 @@ import SearchBoxWrapper from './search-box/search-box-wrapper' import PluginTypeSwitch from './plugin-type-switch' import ListWrapper from './list/list-wrapper' import { getMarketplaceCollectionsAndPlugins } from './utils' +import { TanstackQueryIniter } from '@/context/query-client' type MarketplaceProps = { locale?: string @@ -17,18 +18,20 @@ const Marketplace = async ({ const { marketplaceCollections, marketplaceCollectionPluginsMap } = await getMarketplaceCollectionsAndPlugins() return ( - - - - - - - + + + + + + + + + ) } diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 95b255de62..629b9b7582 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -301,3 +301,7 @@ export type InstalledPluginListResponse = { export type UninstallPluginResponse = { success: boolean } + +export type PluginsFromMarketplaceResponse = { + plugins: Plugin[] +} diff --git a/web/app/components/tools/marketplace/hooks.ts b/web/app/components/tools/marketplace/hooks.ts index 82f019ef14..5d27e10043 100644 --- a/web/app/components/tools/marketplace/hooks.ts +++ b/web/app/components/tools/marketplace/hooks.ts @@ -15,7 +15,7 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin } = useMarketplaceCollectionsAndPlugins() const { plugins, - setPlugins, + resetPlugins, queryPlugins, queryPluginsWithDebounced, isLoading: isPluginsLoading, @@ -37,9 +37,9 @@ export const useMarketplace = (searchPluginText: string, filterPluginTags: strin } else { queryMarketplaceCollectionsAndPlugins() - setPlugins(undefined) + resetPlugins() } - }, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, setPlugins]) + }, [searchPluginText, filterPluginTags, queryPlugins, queryMarketplaceCollectionsAndPlugins, queryPluginsWithDebounced, resetPlugins]) return { isLoading: isLoading || isPluginsLoading, diff --git a/web/service/base.ts b/web/service/base.ts index 0f9059e617..061e8dcd2c 100644 --- a/web/service/base.ts +++ b/web/service/base.ts @@ -460,6 +460,11 @@ export const post = (url: string, options = {}, otherOptions?: IOtherOptions) return request(url, Object.assign({}, options, { method: 'POST' }), otherOptions) } +// For Marketplace API +export const postMarketplace = (url: string, options = {}, otherOptions?: IOtherOptions) => { + return post(url, options, { ...otherOptions, isMarketplaceAPI: true }) +} + export const postPublic = (url: string, options = {}, otherOptions?: IOtherOptions) => { return post(url, options, { ...otherOptions, isPublicAPI: true }) } diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 754cd1cc2d..251e3a40cd 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,12 +1,17 @@ -import type { DebugInfo as DebugInfoTypes, InstalledPluginListResponse, Permissions } from '@/app/components/plugins/types' -import { get, post } from './base' +import type { + DebugInfo as DebugInfoTypes, + InstalledPluginListResponse, + Permissions, + PluginsFromMarketplaceResponse, +} from '@/app/components/plugins/types' +import type { + PluginsSearchParams, +} from '@/app/components/plugins/marketplace/types' +import { get, post, postMarketplace } from './base' import { useMutation, - useQueryClient, -} from '@tanstack/react-query' - -import { useQuery, + useQueryClient, } from '@tanstack/react-query' const NAME_SPACE = 'plugins' @@ -66,3 +71,28 @@ export const useMutationPermissions = ({ onSuccess, }) } + +export const useMutationPluginsFromMarketplace = () => { + return useMutation({ + mutationFn: (pluginsSearchParams: PluginsSearchParams) => { + const { + query, + sortBy, + sortOrder, + category, + tags, + } = pluginsSearchParams + return postMarketplace<{ data: PluginsFromMarketplaceResponse }>('/plugins/search/basic', { + body: { + page: 1, + page_size: 10, + query, + sort_by: sortBy, + sort_order: sortOrder, + category: category !== 'all' ? category : '', + tags, + }, + }) + }, + }) +} From c77b38b97db4ca7c7e5d2703090ebd76c7ca9ae5 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 8 Nov 2024 18:25:15 +0800 Subject: [PATCH 09/16] chore: install from marketplace --- .../install-from-marketplace/steps/install.tsx | 3 ++- web/service/plugins.ts | 6 ------ web/service/use-plugins.ts | 9 +++++++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx index bc32e642a5..27ae871d97 100644 --- a/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-marketplace/steps/install.tsx @@ -9,7 +9,7 @@ import Button from '@/app/components/base/button' import { useTranslation } from 'react-i18next' import { RiLoader2Line } from '@remixicon/react' import Badge, { BadgeState } from '@/app/components/base/badge/index' -import { installPackageFromMarketPlace } from '@/service/plugins' +import { useInstallPackageFromMarketPlace } from '@/service/use-plugins' import checkTaskStatus from '../../base/check-task-status' const i18nPrefix = 'plugin.installModal' @@ -32,6 +32,7 @@ const Installed: FC = ({ onFailed, }) => { const { t } = useTranslation() + const { mutateAsync: installPackageFromMarketPlace } = useInstallPackageFromMarketPlace() const [isInstalling, setIsInstalling] = React.useState(false) const { check, diff --git a/web/service/plugins.ts b/web/service/plugins.ts index b7d5125f2e..3e5d872bf2 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -104,12 +104,6 @@ export const fetchManifestFromMarketPlace = async (uniqueIdentifier: string) => return getMarketplace<{ data: { plugin: PluginManifestInMarket } }>(`/plugins/identifier?unique_identifier=${uniqueIdentifier}`) } -export const installPackageFromMarketPlace = async (uniqueIdentifier: string) => { - return post('/workspaces/current/plugin/install/marketplace', { - body: { plugin_unique_identifiers: [uniqueIdentifier] }, - }) -} - export const fetchMarketplaceCollections: Fetcher = ({ url }) => { return get(url) } diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 251e3a40cd..2274250b58 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -1,5 +1,6 @@ import type { DebugInfo as DebugInfoTypes, + InstallPackageResponse, InstalledPluginListResponse, Permissions, PluginsFromMarketplaceResponse, @@ -34,6 +35,14 @@ export const useInvalidateInstalledPluginList = () => { } } +export const useInstallPackageFromMarketPlace = () => { + return useMutation({ + mutationFn: (uniqueIdentifier: string) => { + return post('/workspaces/current/plugin/install/marketplace', { body: { plugin_unique_identifiers: [uniqueIdentifier] } }) + }, + }) +} + export const useDebugKey = () => { return useQuery({ queryKey: [NAME_SPACE, 'debugKey'], From f2bf2e4470bb0f26af93c469f5a2fb3c8e4367e9 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 8 Nov 2024 21:31:44 +0800 Subject: [PATCH 10/16] fix style of provider added card --- .../model-provider-page/provider-added-card/model-list.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx index 5e70a0def1..b4b331c4bc 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list.tsx @@ -50,15 +50,15 @@ const ModelList: FC = ({ return (
-
+
- + {t('common.modelProvider.modelsNum', { num: models.length })} onCollapse()} > {t('common.modelProvider.modelsNum', { num: models.length })} From 59a9235041f9e36b63e932dfb7ac64ac66a088dd Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sat, 9 Nov 2024 12:12:10 +0800 Subject: [PATCH 11/16] useQuery for model list --- .../plugins/plugin-detail-panel/model-list.tsx | 9 ++------- web/service/use-models.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 web/service/use-models.ts diff --git a/web/app/components/plugins/plugin-detail-panel/model-list.tsx b/web/app/components/plugins/plugin-detail-panel/model-list.tsx index 2704ea741f..7980920119 100644 --- a/web/app/components/plugins/plugin-detail-panel/model-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/model-list.tsx @@ -1,19 +1,14 @@ import React from 'react' -import useSWR from 'swr' import { useTranslation } from 'react-i18next' import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import ModelIcon from '@/app/components/header/account-setting/model-provider-page/model-icon' import ModelName from '@/app/components/header/account-setting/model-provider-page/model-name' -import { fetchModelProviderModelList } from '@/service/common' +import { useModelProviderModelList } from '@/service/use-models' const ModelList = () => { const { t } = useTranslation() const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) - - const { data: res } = useSWR( - `/workspaces/current/model-providers/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}/models`, - fetchModelProviderModelList, - ) + const { data: res } = useModelProviderModelList(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`) if (!res) return null diff --git a/web/service/use-models.ts b/web/service/use-models.ts new file mode 100644 index 0000000000..84122cdd1f --- /dev/null +++ b/web/service/use-models.ts @@ -0,0 +1,17 @@ +import { get } from './base' +import type { + ModelItem, +} from '@/app/components/header/account-setting/model-provider-page/declarations' +import { + useQuery, + // useQueryClient, +} from '@tanstack/react-query' + +const NAME_SPACE = 'models' + +export const useModelProviderModelList = (provider: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'model-list', provider], + queryFn: () => get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`), + }) +} From 1e62768eed1b792f7cd7d654b73a7976ba622371 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sat, 9 Nov 2024 12:51:10 +0800 Subject: [PATCH 12/16] useQuery in action list --- .../plugin-detail-panel/action-list.tsx | 63 +++++++++--------- web/service/use-tools.ts | 65 ++++++++++++++++--- 2 files changed, 87 insertions(+), 41 deletions(-) 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 c90f1ffb0d..609e7d1306 100644 --- a/web/app/components/plugins/plugin-detail-panel/action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/action-list.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react' -import useSWR from 'swr' import { useTranslation } from 'react-i18next' import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import { useAppContext } from '@/context/app-context' @@ -9,28 +8,39 @@ 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 { - fetchBuiltInToolList, - fetchCollectionDetail, - removeBuiltInToolCredential, - updateBuiltInToolCredential, -} from '@/service/tools' + useBuiltinProviderInfo, + useBuiltinTools, + useInvalidateBuiltinProviderInfo, + useRemoveProviderCredentials, + useUpdateProviderCredentials, +} from '@/service/use-tools' const ActionList = () => { const { t } = useTranslation() const { isCurrentWorkspaceManager } = useAppContext() const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail) - const { data: provider } = useSWR( - `builtin/${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`, - fetchCollectionDetail, - ) - const { data } = useSWR( - `${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`, - fetchBuiltInToolList, - ) + const { data: provider } = useBuiltinProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`) + const invalidateProviderInfo = useInvalidateBuiltinProviderInfo() + const { data } = useBuiltinTools(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`) const [showSettingAuth, setShowSettingAuth] = useState(false) - const handleCredentialSettingUpdate = () => {} + const handleCredentialSettingUpdate = () => { + invalidateProviderInfo(`${currentPluginDetail.plugin_id}/${currentPluginDetail.name}`) + Toast.notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + setShowSettingAuth(false) + } + + const { mutate: updatePermission } = useUpdateProviderCredentials({ + onSuccess: handleCredentialSettingUpdate, + }) + + const { mutate: removePermission } = useRemoveProviderCredentials({ + onSuccess: handleCredentialSettingUpdate, + }) if (!data || !provider) return null @@ -77,24 +87,11 @@ const ActionList = () => { setShowSettingAuth(false)} - onSaved={async (value) => { - await updateBuiltInToolCredential(provider.name, value) - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - handleCredentialSettingUpdate() - setShowSettingAuth(false) - }} - onRemove={async () => { - await removeBuiltInToolCredential(provider.name) - Toast.notify({ - type: 'success', - message: t('common.api.actionSuccess'), - }) - handleCredentialSettingUpdate() - setShowSettingAuth(false) - }} + onSaved={async value => updatePermission({ + providerName: provider.name, + credentials: value, + })} + onRemove={async () => removePermission(provider.name)} /> )}
diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index fb01888e7f..0d0f816b3e 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -1,14 +1,13 @@ -import { get } from './base' +import { get, post } from './base' import type { + Collection, Tool, } from '@/app/components/tools/types' import type { ToolWithProvider } from '@/app/components/workflow/types' import { - useQueryClient, -} from '@tanstack/react-query' - -import { + useMutation, useQuery, + useQueryClient, } from '@tanstack/react-query' const NAME_SPACE = 'tools' @@ -45,9 +44,59 @@ export const useAllWorkflowTools = () => { }) } -export const useBuiltInTools = (collectionName: string) => { +export const useBuiltinProviderInfo = (providerName: string) => { return useQuery({ - queryKey: [NAME_SPACE, 'builtIn', collectionName], - queryFn: () => get(`/workspaces/current/tool-provider/builtin/${collectionName}/tools`), + queryKey: [NAME_SPACE, 'builtin-provider-info', providerName], + queryFn: () => get(`/workspaces/current/tool-provider/builtin/${providerName}/info`), + }) +} + +export const useInvalidateBuiltinProviderInfo = () => { + const queryClient = useQueryClient() + return (providerName: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'builtin-provider-info', providerName], + }) + } +} + +export const useBuiltinTools = (providerName: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'builtin-provider-tools', providerName], + queryFn: () => get(`/workspaces/current/tool-provider/builtin/${providerName}/tools`), + }) +} + +export const useUpdateProviderCredentials = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationFn: (payload: { providerName: string, credentials: Record }) => { + const { providerName, credentials } = payload + return post(`/workspaces/current/tool-provider/builtin/${providerName}/update`, { + body: { + credentials, + }, + }) + }, + onSuccess, + }) +} + +export const useRemoveProviderCredentials = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationFn: (providerName: string) => { + return post(`/workspaces/current/tool-provider/builtin/${providerName}/delete`, { + body: {}, + }) + }, + onSuccess, }) } From 5e81150b22786fc6b7c7c3b9ff4c8e3dc0536d07 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Sat, 9 Nov 2024 14:44:48 +0800 Subject: [PATCH 13/16] useQuery for endpoints --- .../plugin-detail-panel/endpoint-card.tsx | 101 +++++------- .../plugin-detail-panel/endpoint-list.tsx | 54 +++---- web/app/components/plugins/types.ts | 11 +- web/service/plugins.ts | 35 ---- web/service/use-endpoints.ts | 149 ++++++++++++++++++ web/service/use-tools.ts | 2 + 6 files changed, 212 insertions(+), 140 deletions(-) create mode 100644 web/service/use-endpoints.ts diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx index 1a984b4eda..14e9abef9b 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-card.tsx @@ -13,11 +13,11 @@ import Indicator from '@/app/components/header/indicator' import Switch from '@/app/components/base/switch' import Toast from '@/app/components/base/toast' import { - deleteEndpoint, - disableEndpoint, - enableEndpoint, - updateEndpoint, -} from '@/service/plugins' + useDeleteEndpoint, + useDisableEndpoint, + useEnableEndpoint, + useUpdateEndpoint, +} from '@/service/use-endpoints' type Props = { data: EndpointListItem @@ -32,43 +32,34 @@ const EndpointCard = ({ const [active, setActive] = useState(data.enabled) const endpointID = data.id + // switch const [isShowDisableConfirm, { setTrue: showDisableConfirm, setFalse: hideDisableConfirm, }] = useBoolean(false) - const activeEndpoint = async () => { - try { - await enableEndpoint({ - url: '/workspaces/current/endpoints/enable', - endpointID, - }) + const { mutate: enableEndpoint } = useEnableEndpoint({ + onSuccess: async () => { await handleChange() - } - catch (error: any) { - console.error(error) + }, + onError: () => { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) setActive(false) - } - } - const inactiveEndpoint = async () => { - try { - await disableEndpoint({ - url: '/workspaces/current/endpoints/disable', - endpointID, - }) + }, + }) + const { mutate: disableEndpoint } = useDisableEndpoint({ + onSuccess: async () => { await handleChange() hideDisableConfirm() - } - catch (error) { - console.error(error) + }, + onError: () => { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) - setActive(true) - } - } + setActive(false) + }, + }) const handleSwitch = (state: boolean) => { if (state) { setActive(true) - activeEndpoint() + enableEndpoint(endpointID) } else { setActive(false) @@ -76,30 +67,26 @@ const EndpointCard = ({ } } + // delete const [isShowDeleteConfirm, { setTrue: showDeleteConfirm, setFalse: hideDeleteConfirm, }] = useBoolean(false) - const handleDelete = async () => { - try { - await deleteEndpoint({ - url: '/workspaces/current/endpoints/delete', - endpointID, - }) + const { mutate: deleteEndpoint } = useDeleteEndpoint({ + onSuccess: async () => { await handleChange() hideDeleteConfirm() - } - catch (error) { - console.error(error) + }, + onError: () => { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) - } - } + }, + }) + // update const [isShowEndpointModal, { setTrue: showEndpointModalConfirm, setFalse: hideEndpointModalConfirm, }] = useBoolean(false) - const formSchemas = useMemo(() => { return toolCredentialToFormSchemas([NAME_FIELD, ...data.declaration.settings]) }, [data.declaration.settings]) @@ -110,27 +97,19 @@ const EndpointCard = ({ } return addDefaultValue(formValue, formSchemas) }, [data.name, data.settings, formSchemas]) - - const handleUpdate = async (state: any) => { - const newName = state.name - delete state.name - try { - await updateEndpoint({ - url: '/workspaces/current/endpoints/update', - body: { - endpoint_id: data.id, - settings: state, - name: newName, - }, - }) + const { mutate: updateEndpoint } = useUpdateEndpoint({ + onSuccess: async () => { await handleChange() hideEndpointModalConfirm() - } - catch (error) { - console.error(error) + }, + onError: () => { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) - } - } + }, + }) + const handleUpdate = (state: any) => updateEndpoint({ + endpointID, + state, + }) return (
@@ -192,7 +171,7 @@ const EndpointCard = ({ hideDisableConfirm() setActive(true) }} - onConfirm={inactiveEndpoint} + onConfirm={() => disableEndpoint(endpointID)} /> )} {isShowDeleteConfirm && ( @@ -201,7 +180,7 @@ const EndpointCard = ({ title={t('plugin.detailPanel.endpointDeleteTip')} content={
{t('plugin.detailPanel.endpointDeleteContent', { name: data.name })}
} onCancel={hideDeleteConfirm} - onConfirm={handleDelete} + onConfirm={() => deleteEndpoint(endpointID)} /> )} {isShowEndpointModal && ( diff --git a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx index 6323e42365..b5f5d2768e 100644 --- a/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx @@ -1,6 +1,5 @@ import React, { useMemo } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import { useBoolean } from 'ahooks' import { RiAddLine } from '@remixicon/react' import EndpointModal from './endpoint-modal' @@ -12,9 +11,10 @@ import Tooltip from '@/app/components/base/tooltip' import Toast from '@/app/components/base/toast' import { usePluginPageContext } from '@/app/components/plugins/plugin-page/context' import { - createEndpoint, - fetchEndpointList, -} from '@/service/plugins' + useCreateEndpoint, + useEndpointList, + useInvalidateEndpointList, +} from '@/service/use-endpoints' import cn from '@/utils/classnames' type Props = { @@ -25,17 +25,9 @@ const EndpointList = ({ showTopBorder }: Props) => { const pluginDetail = usePluginPageContext(v => v.currentPluginDetail) const pluginUniqueID = pluginDetail.plugin_unique_identifier const declaration = pluginDetail.declaration.endpoint - const { data, mutate } = useSWR( - { - url: '/workspaces/current/endpoints/list/plugin', - params: { - plugin_id: pluginDetail.plugin_id, - page: 1, - page_size: 100, - }, - }, - fetchEndpointList, - ) + const { data } = useEndpointList(pluginDetail.plugin_id) + const invalidateEndpointList = useInvalidateEndpointList() + const [isShowEndpointModal, { setTrue: showEndpointModal, setFalse: hideEndpointModal, @@ -45,26 +37,20 @@ const EndpointList = ({ showTopBorder }: Props) => { return toolCredentialToFormSchemas([NAME_FIELD, ...declaration.settings]) }, [declaration.settings]) - const handleCreate = async (state: any) => { - const newName = state.name - delete state.name - try { - await createEndpoint({ - url: '/workspaces/current/endpoints/create', - body: { - plugin_unique_identifier: pluginUniqueID, - settings: state, - name: newName, - }, - }) - await mutate() + const { mutate: createEndpoint } = useCreateEndpoint({ + onSuccess: async () => { + await invalidateEndpointList(pluginDetail.plugin_id) hideEndpointModal() - } - catch (error) { - console.error(error) + }, + onError: () => { Toast.notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') }) - } - } + }, + }) + + const handleCreate = (state: any) => createEndpoint({ + pluginUniqueID, + state, + }) if (!data) return null @@ -92,7 +78,7 @@ const EndpointList = ({ showTopBorder }: Props) => { invalidateEndpointList(pluginDetail.plugin_id)} /> ))}
diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 629b9b7582..40627f67a3 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -194,19 +194,10 @@ export type GitHubUrlInfo = { } // endpoint -export type CreateEndpointRequest = { - plugin_unique_identifier: string - settings: Record - name: string -} export type EndpointOperationResponse = { result: 'success' | 'error' } -export type EndpointsRequest = { - page_size: number - page: number - plugin_id: string -} + export type EndpointsResponse = { endpoints: EndpointListItem[] has_more: boolean diff --git a/web/service/plugins.ts b/web/service/plugins.ts index 3e5d872bf2..999ed4f1b9 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -1,10 +1,6 @@ import type { Fetcher } from 'swr' import { get, getMarketplace, post, upload } from './base' import type { - CreateEndpointRequest, - EndpointOperationResponse, - EndpointsRequest, - EndpointsResponse, InstallPackageResponse, Permissions, PluginDeclaration, @@ -12,7 +8,6 @@ import type { PluginTasksResponse, TaskStatusResponse, UninstallPluginResponse, - UpdateEndpointRequest, uploadGitHubResponse, } from '@/app/components/plugins/types' import type { @@ -20,36 +15,6 @@ import type { MarketplaceCollectionsResponse, } from '@/app/components/plugins/marketplace/types' -export const createEndpoint: Fetcher = ({ url, body }) => { - // url = /workspaces/current/endpoints/create - return post(url, { body }) -} - -export const fetchEndpointList: Fetcher = ({ url, params }) => { - // url = /workspaces/current/endpoints/list/plugin?plugin_id=xxx - return get(url, { params }) -} - -export const deleteEndpoint: Fetcher = ({ url, endpointID }) => { - // url = /workspaces/current/endpoints/delete - return post(url, { body: { endpoint_id: endpointID } }) -} - -export const updateEndpoint: Fetcher = ({ url, body }) => { - // url = /workspaces/current/endpoints/update - return post(url, { body }) -} - -export const enableEndpoint: Fetcher = ({ url, endpointID }) => { - // url = /workspaces/current/endpoints/enable - return post(url, { body: { endpoint_id: endpointID } }) -} - -export const disableEndpoint: Fetcher = ({ url, endpointID }) => { - // url = /workspaces/current/endpoints/disable - return post(url, { body: { endpoint_id: endpointID } }) -} - export const uploadPackageFile = async (file: File) => { const formData = new FormData() formData.append('pkg', file) diff --git a/web/service/use-endpoints.ts b/web/service/use-endpoints.ts new file mode 100644 index 0000000000..43a82480b9 --- /dev/null +++ b/web/service/use-endpoints.ts @@ -0,0 +1,149 @@ +import { get, post } from './base' +import type { + EndpointsResponse, +} from '@/app/components/plugins/types' +import { + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query' + +const NAME_SPACE = 'endpoints' + +export const useEndpointList = (pluginID: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'list', pluginID], + queryFn: () => get('/workspaces/current/endpoints/list/plugin', { + params: { + plugin_id: pluginID, + page: 1, + page_size: 100, + }, + }), + }) +} + +export const useInvalidateEndpointList = () => { + const queryClient = useQueryClient() + return (pluginID: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'list', pluginID], + }) + } +} + +export const useCreateEndpoint = ({ + onSuccess, + onError, +}: { + onSuccess?: () => void + onError?: (error: any) => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'create'], + mutationFn: (payload: { pluginUniqueID: string, state: Record }) => { + const { pluginUniqueID, state } = payload + const newName = state.name + delete state.name + return post('/workspaces/current/endpoints/create', { + body: { + plugin_unique_identifier: pluginUniqueID, + settings: state, + name: newName, + }, + }) + }, + onSuccess, + onError, + }) +} + +export const useUpdateEndpoint = ({ + onSuccess, + onError, +}: { + onSuccess?: () => void + onError?: (error: any) => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update'], + mutationFn: (payload: { endpointID: string, state: Record }) => { + const { endpointID, state } = payload + const newName = state.name + delete state.name + return post('/workspaces/current/endpoints/update', { + body: { + endpoint_id: endpointID, + settings: state, + name: newName, + }, + }) + }, + onSuccess, + onError, + }) +} + +export const useDeleteEndpoint = ({ + onSuccess, + onError, +}: { + onSuccess?: () => void + onError?: (error: any) => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete'], + mutationFn: (endpointID: string) => { + return post('/workspaces/current/endpoints/delete', { + body: { + endpoint_id: endpointID, + }, + }) + }, + onSuccess, + onError, + }) +} + +export const useEnableEndpoint = ({ + onSuccess, + onError, +}: { + onSuccess?: () => void + onError?: (error: any) => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'enable'], + mutationFn: (endpointID: string) => { + return post('/workspaces/current/endpoints/enable', { + body: { + endpoint_id: endpointID, + }, + }) + }, + onSuccess, + onError, + }) +} + +export const useDisableEndpoint = ({ + onSuccess, + onError, +}: { + onSuccess?: () => void + onError?: (error: any) => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'disable'], + mutationFn: (endpointID: string) => { + return post('/workspaces/current/endpoints/disable', { + body: { + endpoint_id: endpointID, + }, + }) + }, + onSuccess, + onError, + }) +} diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index 0d0f816b3e..3c34de3be9 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -74,6 +74,7 @@ export const useUpdateProviderCredentials = ({ onSuccess?: () => void }) => { return useMutation({ + mutationKey: [NAME_SPACE, 'update-provider-credentials'], mutationFn: (payload: { providerName: string, credentials: Record }) => { const { providerName, credentials } = payload return post(`/workspaces/current/tool-provider/builtin/${providerName}/update`, { @@ -92,6 +93,7 @@ export const useRemoveProviderCredentials = ({ onSuccess?: () => void }) => { return useMutation({ + mutationKey: [NAME_SPACE, 'remove-provider-credentials'], mutationFn: (providerName: string) => { return post(`/workspaces/current/tool-provider/builtin/${providerName}/delete`, { body: {}, From 66b08e653e8ef42b33b6583251ec015e913e1d6c Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Mon, 11 Nov 2024 12:27:13 +0800 Subject: [PATCH 14/16] fix: credentials: --- web/service/fetch.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/service/fetch.ts b/web/service/fetch.ts index 46e0e6295d..b70a13c6ca 100644 --- a/web/service/fetch.ts +++ b/web/service/fetch.ts @@ -158,10 +158,10 @@ async function base(url: string, options: FetchOptionType = {}, otherOptions: ], }, })(fetchPathname, { - credentials: isMarketplaceAPI - ? 'omit' - : (options.credentials || 'include'), ...init, + credentials: isMarketplaceAPI + ? undefined + : (options.credentials || 'include'), retry: { methods: [], }, From 822c18cb766bf5273f57a00cca68e1241a56db4e Mon Sep 17 00:00:00 2001 From: StyleZhang Date: Mon, 11 Nov 2024 12:27:58 +0800 Subject: [PATCH 15/16] fix: credentials: --- web/service/fetch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/service/fetch.ts b/web/service/fetch.ts index b70a13c6ca..6551fa5793 100644 --- a/web/service/fetch.ts +++ b/web/service/fetch.ts @@ -160,7 +160,7 @@ async function base(url: string, options: FetchOptionType = {}, otherOptions: })(fetchPathname, { ...init, credentials: isMarketplaceAPI - ? undefined + ? 'omit' : (options.credentials || 'include'), retry: { methods: [], From f47b5ce63a633dbe181a7ac23468d61e1460855c Mon Sep 17 00:00:00 2001 From: Joel Date: Mon, 11 Nov 2024 13:00:41 +0800 Subject: [PATCH 16/16] chore: install plugin by local use use query --- .../install-from-local-package/steps/install.tsx | 4 +++- web/service/plugins.ts | 6 ------ web/service/use-plugins.ts | 10 ++++++++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx index da5357d87d..b48922bdb9 100644 --- a/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx +++ b/web/app/components/plugins/install-plugin/install-from-local-package/steps/install.tsx @@ -8,7 +8,7 @@ import Button from '@/app/components/base/button' import { Trans, useTranslation } from 'react-i18next' import { RiLoader2Line } from '@remixicon/react' import Badge, { BadgeState } from '@/app/components/base/badge/index' -import { installPackageFromLocal } from '@/service/plugins' +import { useInstallPackageFromLocal } from '@/service/use-plugins' import checkTaskStatus from '../../base/check-task-status' import { usePluginTasksStore } from '@/app/components/plugins/plugin-page/store' @@ -33,6 +33,8 @@ const Installed: FC = ({ }) => { const { t } = useTranslation() const [isInstalling, setIsInstalling] = React.useState(false) + const { mutateAsync: installPackageFromLocal } = useInstallPackageFromLocal() + const { check, stop, diff --git a/web/service/plugins.ts b/web/service/plugins.ts index 999ed4f1b9..0b10196096 100644 --- a/web/service/plugins.ts +++ b/web/service/plugins.ts @@ -24,12 +24,6 @@ export const uploadPackageFile = async (file: File) => { }, false, '/workspaces/current/plugin/upload/pkg') } -export const installPackageFromLocal = async (uniqueIdentifier: string) => { - return post('/workspaces/current/plugin/install/pkg', { - body: { plugin_unique_identifiers: [uniqueIdentifier] }, - }) -} - export const updateFromMarketPlace = async (body: Record) => { return post('/workspaces/current/plugin/upgrade/marketplace', { body, diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 2274250b58..3b72bee919 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -43,6 +43,16 @@ export const useInstallPackageFromMarketPlace = () => { }) } +export const useInstallPackageFromLocal = () => { + return useMutation({ + mutationFn: (uniqueIdentifier: string) => { + return post('/workspaces/current/plugin/install/pkg', { + body: { plugin_unique_identifiers: [uniqueIdentifier] }, + }) + }, + }) +} + export const useDebugKey = () => { return useQuery({ queryKey: [NAME_SPACE, 'debugKey'],