| @@ -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): | |||
| @@ -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 | |||
| @@ -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,12 +32,21 @@ class PluginInstaller(BasePluginClient): | |||
| ) | |||
| def list_plugins(self, tenant_id: str) -> list[PluginEntity]: | |||
| return self._request_with_plugin_daemon_response( | |||
| result = self._request_with_plugin_daemon_response( | |||
| "GET", | |||
| f"plugin/{tenant_id}/management/list", | |||
| list[PluginEntity], | |||
| 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", | |||
| PluginListResponse, | |||
| params={"page": page, "page_size": page_size}, | |||
| ) | |||
| def upload_pkg( | |||
| self, | |||
| @@ -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]: | |||
| """ | |||
| @@ -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 = () => { | |||
| /> | |||
| </div> | |||
| {isPluginListLoading ? <Loading type='app' /> : (filteredList?.length ?? 0) > 0 ? ( | |||
| <div className='flex grow flex-wrap content-start items-start gap-2 self-stretch px-12'> | |||
| <div className='flex grow flex-wrap content-start items-start justify-center gap-2 self-stretch px-12'> | |||
| <div className='w-full'> | |||
| <List pluginList={filteredList || []} /> | |||
| </div> | |||
| {!isLastPage && !isFetching && ( | |||
| <Button onClick={loadNextPage}> | |||
| {t('workflow.common.loadMore')} | |||
| </Button> | |||
| )} | |||
| {isFetching && <div className='system-md-semibold text-text-secondary'>{t('appLog.detail.loading')}</div>} | |||
| </div> | |||
| ) : ( | |||
| <Empty /> | |||
| @@ -325,6 +325,11 @@ export type InstalledPluginListResponse = { | |||
| plugins: PluginDetail[] | |||
| } | |||
| export type InstalledPluginListWithTotalResponse = { | |||
| plugins: PluginDetail[] | |||
| total: number | |||
| } | |||
| export type InstalledLatestVersionResponse = { | |||
| versions: { | |||
| [plugin_id: string]: { | |||
| @@ -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<InstalledPluginListWithTotalResponse>( | |||
| `/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<InstalledLatestVersionResponse>({ | |||
| queryKey: [NAME_SPACE, 'installedLatestVersion', pluginIds], | |||