diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index 3700f007f..302bc3090 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -49,6 +49,23 @@ class PluginListApi(Resource): return jsonable_encoder({"plugins": plugins}) +class PluginListLatestVersionsApi(Resource): + @setup_required + @login_required + @account_initialization_required + def post(self): + req = reqparse.RequestParser() + req.add_argument("plugin_ids", type=list, required=True, location="json") + args = req.parse_args() + + try: + versions = PluginService.list_latest_versions(args["plugin_ids"]) + except PluginDaemonClientSideError as e: + raise ValueError(e) + + return jsonable_encoder({"versions": versions}) + + class PluginListInstallationsFromIdsApi(Resource): @setup_required @login_required @@ -453,6 +470,7 @@ class PluginFetchPermissionApi(Resource): api.add_resource(PluginDebuggingKeyApi, "/workspaces/current/plugin/debugging-key") api.add_resource(PluginListApi, "/workspaces/current/plugin/list") +api.add_resource(PluginListLatestVersionsApi, "/workspaces/current/plugin/list/latest-versions") api.add_resource(PluginListInstallationsFromIdsApi, "/workspaces/current/plugin/list/installations/ids") api.add_resource(PluginIconApi, "/workspaces/current/plugin/icon") api.add_resource(PluginUploadFromPkgApi, "/workspaces/current/plugin/upload/pkg") diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py index 61f8a6591..421c16093 100644 --- a/api/core/plugin/entities/plugin.py +++ b/api/core/plugin/entities/plugin.py @@ -120,8 +120,6 @@ class PluginEntity(PluginInstallation): name: str installation_id: str version: str - latest_version: Optional[str] = None - latest_unique_identifier: Optional[str] = None @model_validator(mode="after") def set_plugin_id(self): diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index 749bb1a5b..25d192410 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -94,6 +94,13 @@ class PluginService: manager = PluginDebuggingManager() return manager.get_debugging_key(tenant_id) + @staticmethod + def list_latest_versions(plugin_ids: Sequence[str]) -> Mapping[str, Optional[LatestPluginCache]]: + """ + List the latest versions of the plugins + """ + return PluginService.fetch_latest_plugin_version(plugin_ids) + @staticmethod def list(tenant_id: str) -> list[PluginEntity]: """ @@ -101,22 +108,6 @@ class PluginService: """ manager = PluginInstallationManager() plugins = manager.list_plugins(tenant_id) - plugin_ids = [plugin.plugin_id for plugin in plugins if plugin.source == PluginInstallationSource.Marketplace] - try: - manifests = PluginService.fetch_latest_plugin_version(plugin_ids) - except Exception: - manifests = {} - logger.exception("failed to fetch plugin manifests") - - for plugin in plugins: - if plugin.source == PluginInstallationSource.Marketplace: - if plugin.plugin_id in manifests: - latest_plugin_cache = manifests[plugin.plugin_id] - if latest_plugin_cache: - # set latest_version - plugin.latest_version = latest_plugin_cache.version - plugin.latest_unique_identifier = latest_plugin_cache.unique_identifier - return plugins @staticmethod diff --git a/web/app/components/plugins/plugin-page/plugins-panel.tsx b/web/app/components/plugins/plugin-page/plugins-panel.tsx index 063cec872..125e6f0a7 100644 --- a/web/app/components/plugins/plugin-page/plugins-panel.tsx +++ b/web/app/components/plugins/plugin-page/plugins-panel.tsx @@ -3,17 +3,23 @@ import { useMemo } from 'react' import type { FilterState } from './filter-management' import FilterManagement from './filter-management' import List from './list' -import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' +import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' import { usePluginPageContext } from './context' import { useDebounceFn } from 'ahooks' import Empty from './empty' import Loading from '../../base/loading' +import { PluginSource } from '../types' const PluginsPanel = () => { const filters = usePluginPageContext(v => v.filters) as FilterState const setFilters = usePluginPageContext(v => v.setFilters) const { data: pluginList, isLoading: isPluginListLoading } = useInstalledPluginList() + const { data: installedLatestVersion } = useInstalledLatestVersion( + pluginList?.plugins + .filter(plugin => plugin.source === PluginSource.marketplace) + .map(plugin => plugin.plugin_id) ?? [], + ) const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const currentPluginID = usePluginPageContext(v => v.currentPluginID) const setCurrentPluginID = usePluginPageContext(v => v.setCurrentPluginID) @@ -22,9 +28,17 @@ const PluginsPanel = () => { setFilters(filters) }, { wait: 500 }) + const pluginListWithLatestVersion = useMemo(() => { + return pluginList?.plugins.map(plugin => ({ + ...plugin, + latest_version: installedLatestVersion?.versions[plugin.plugin_id]?.version ?? '', + latest_unique_identifier: installedLatestVersion?.versions[plugin.plugin_id]?.unique_identifier ?? '', + })) || [] + }, [pluginList, installedLatestVersion]) + const filteredList = useMemo(() => { const { categories, searchQuery, tags } = filters - const filteredList = pluginList?.plugins.filter((plugin) => { + const filteredList = pluginListWithLatestVersion.filter((plugin) => { return ( (categories.length === 0 || categories.includes(plugin.declaration.category)) && (tags.length === 0 || tags.some(tag => plugin.declaration.tags.includes(tag))) @@ -32,12 +46,12 @@ const PluginsPanel = () => { ) }) return filteredList - }, [pluginList, filters]) + }, [pluginListWithLatestVersion, filters]) const currentPluginDetail = useMemo(() => { - const detail = pluginList?.plugins.find(plugin => plugin.plugin_id === currentPluginID) + const detail = pluginListWithLatestVersion.find(plugin => plugin.plugin_id === currentPluginID) return detail - }, [currentPluginID, pluginList?.plugins]) + }, [currentPluginID, pluginListWithLatestVersion]) const handleHide = () => setCurrentPluginID(undefined) diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 1ed379511..64f15a08a 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -318,6 +318,15 @@ export type InstalledPluginListResponse = { plugins: PluginDetail[] } +export type InstalledLatestVersionResponse = { + versions: { + [plugin_id: string]: { + unique_identifier: string + version: string + } | null + } +} + export type UninstallPluginResponse = { success: boolean } diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 9b5bab587..c5f60b4a1 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -9,6 +9,7 @@ import type { Dependency, GitHubItemAndMarketPlaceDependency, InstallPackageResponse, + InstalledLatestVersionResponse, InstalledPluginListResponse, PackageDependency, Permissions, @@ -72,6 +73,19 @@ export const useInstalledPluginList = (disable?: boolean) => { }) } +export const useInstalledLatestVersion = (pluginIds: string[]) => { + return useQuery({ + queryKey: [NAME_SPACE, 'installedLatestVersion', pluginIds], + queryFn: () => post('/workspaces/current/plugin/list/latest-versions', { + body: { + plugin_ids: pluginIds, + }, + }), + enabled: !!pluginIds.length, + initialData: pluginIds.length ? undefined : { versions: {} }, + }) +} + export const useInvalidateInstalledPluginList = () => { const queryClient = useQueryClient() const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools()