diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index fda5a7d3bb..9bddbb4b4b 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -41,12 +41,16 @@ class PluginListApi(Resource): @account_initialization_required def get(self): tenant_id = current_user.current_tenant_id + parser = reqparse.RequestParser() + parser.add_argument("page", type=int, required=False, location="args", default=1) + parser.add_argument("page_size", type=int, required=False, location="args", default=256) + args = parser.parse_args() try: - plugins = PluginService.list(tenant_id) + plugins_with_total = PluginService.list_with_total(tenant_id, args["page"], args["page_size"]) except PluginDaemonClientSideError as e: raise ValueError(e) - return jsonable_encoder({"plugins": plugins}) + return jsonable_encoder({"plugins": plugins_with_total.list, "total": plugins_with_total.total}) class PluginListLatestVersionsApi(Resource): diff --git a/api/core/plugin/entities/plugin_daemon.py b/api/core/plugin/entities/plugin_daemon.py index 2bea07bea0..e9275c31cc 100644 --- a/api/core/plugin/entities/plugin_daemon.py +++ b/api/core/plugin/entities/plugin_daemon.py @@ -9,7 +9,7 @@ from core.agent.plugin_entities import AgentProviderEntityWithPlugin from core.model_runtime.entities.model_entities import AIModelEntity from core.model_runtime.entities.provider_entities import ProviderEntity from core.plugin.entities.base import BasePluginEntity -from core.plugin.entities.plugin import PluginDeclaration +from core.plugin.entities.plugin import PluginDeclaration, PluginEntity from core.tools.entities.common_entities import I18nObject from core.tools.entities.tool_entities import ToolProviderEntityWithPlugin @@ -167,3 +167,8 @@ class PluginOAuthAuthorizationUrlResponse(BaseModel): class PluginOAuthCredentialsResponse(BaseModel): credentials: Mapping[str, Any] = Field(description="The credentials of the OAuth.") + + +class PluginListResponse(BaseModel): + list: list[PluginEntity] + total: int diff --git a/api/core/plugin/impl/plugin.py b/api/core/plugin/impl/plugin.py index 3349463ce5..1cd2dc1be7 100644 --- a/api/core/plugin/impl/plugin.py +++ b/api/core/plugin/impl/plugin.py @@ -9,7 +9,12 @@ from core.plugin.entities.plugin import ( PluginInstallation, PluginInstallationSource, ) -from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginInstallTaskStartResponse, PluginUploadResponse +from core.plugin.entities.plugin_daemon import ( + PluginInstallTask, + PluginInstallTaskStartResponse, + PluginListResponse, + PluginUploadResponse, +) from core.plugin.impl.base import BasePluginClient @@ -27,11 +32,20 @@ class PluginInstaller(BasePluginClient): ) def list_plugins(self, tenant_id: str) -> list[PluginEntity]: + result = self._request_with_plugin_daemon_response( + "GET", + f"plugin/{tenant_id}/management/list", + PluginListResponse, + params={"page": 1, "page_size": 256}, + ) + return result.list + + def list_plugins_with_total(self, tenant_id: str, page: int, page_size: int) -> PluginListResponse: return self._request_with_plugin_daemon_response( "GET", f"plugin/{tenant_id}/management/list", - list[PluginEntity], - params={"page": 1, "page_size": 256}, + PluginListResponse, + params={"page": page, "page_size": page_size}, ) def upload_pkg( diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index be722a59ad..a8b64f27db 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -17,7 +17,7 @@ from core.plugin.entities.plugin import ( PluginInstallation, PluginInstallationSource, ) -from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginUploadResponse +from core.plugin.entities.plugin_daemon import PluginInstallTask, PluginListResponse, PluginUploadResponse from core.plugin.impl.asset import PluginAssetManager from core.plugin.impl.debugging import PluginDebuggingClient from core.plugin.impl.plugin import PluginInstaller @@ -110,6 +110,15 @@ class PluginService: plugins = manager.list_plugins(tenant_id) return plugins + @staticmethod + def list_with_total(tenant_id: str, page: int, page_size: int) -> PluginListResponse: + """ + list all plugins of the tenant + """ + manager = PluginInstaller() + plugins = manager.list_plugins_with_total(tenant_id, page, page_size) + return plugins + @staticmethod def list_installations_from_ids(tenant_id: str, ids: Sequence[str]) -> Sequence[PluginInstallation]: """ diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index 125e6f0a70..513641f4b9 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -1,20 +1,23 @@ 'use client' import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' import type { FilterState } from './filter-management' import FilterManagement from './filter-management' import List from './list' -import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' +import { useInstalledLatestVersion, useInstalledPluginListWithPagination, useInvalidateInstalledPluginList } from '@/service/use-plugins' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' import { usePluginPageContext } from './context' import { useDebounceFn } from 'ahooks' +import Button from '@/app/components/base/button' import Empty from './empty' import Loading from '../../base/loading' import { PluginSource } from '../types' const PluginsPanel = () => { + const { t } = useTranslation() const filters = usePluginPageContext(v => v.filters) as FilterState const setFilters = usePluginPageContext(v => v.setFilters) - const { data: pluginList, isLoading: isPluginListLoading } = useInstalledPluginList() + const { data: pluginList, isLoading: isPluginListLoading, isFetching, isLastPage, loadNextPage } = useInstalledPluginListWithPagination() const { data: installedLatestVersion } = useInstalledLatestVersion( pluginList?.plugins .filter(plugin => plugin.source === PluginSource.marketplace) @@ -64,10 +67,16 @@ const PluginsPanel = () => { /> {isPluginListLoading ? : (filteredList?.length ?? 0) > 0 ? ( -
+
+ {!isLastPage && !isFetching && ( + + )} + {isFetching &&
{t('appLog.detail.loading')}
}
) : ( diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index f552d7c17a..231763aaab 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -325,6 +325,11 @@ export type InstalledPluginListResponse = { plugins: PluginDetail[] } +export type InstalledPluginListWithTotalResponse = { + plugins: PluginDetail[] + total: number +} + export type InstalledLatestVersionResponse = { versions: { [plugin_id: string]: { diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 13a494b50d..871b6f0649 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -11,6 +11,7 @@ import type { InstallPackageResponse, InstalledLatestVersionResponse, InstalledPluginListResponse, + InstalledPluginListWithTotalResponse, PackageDependency, Permissions, Plugin, @@ -33,6 +34,7 @@ import type { import { get, getMarketplace, post, postMarketplace } from './base' import type { MutateOptions, QueryOptions } from '@tanstack/react-query' import { + useInfiniteQuery, useMutation, useQuery, useQueryClient, @@ -74,6 +76,53 @@ export const useInstalledPluginList = (disable?: boolean) => { }) } +export const useInstalledPluginListWithPagination = (pageSize = 100) => { + const fetchPlugins = async ({ pageParam = 1 }) => { + const response = await get( + `/workspaces/current/plugin/list?page=${pageParam}&page_size=${pageSize}`, + ) + return response + } + + const { + data, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + } = useInfiniteQuery({ + queryKey: ['installed-plugins', pageSize], + queryFn: fetchPlugins, + getNextPageParam: (lastPage, pages) => { + const totalItems = lastPage.total + const currentPage = pages.length + const itemsLoaded = currentPage * pageSize + + if (itemsLoaded >= totalItems) + return + + return currentPage + 1 + }, + initialPageParam: 1, + }) + + const plugins = data?.pages.flatMap(page => page.plugins) ?? [] + + return { + data: { + plugins, + }, + isLastPage: !hasNextPage, + loadNextPage: () => { + fetchNextPage() + }, + isLoading, + isFetching: isFetchingNextPage, + error, + } +} + export const useInstalledLatestVersion = (pluginIds: string[]) => { return useQuery({ queryKey: [NAME_SPACE, 'installedLatestVersion', pluginIds],