| @@ -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") | |||
| @@ -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): | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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 | |||
| } | |||
| @@ -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<InstalledLatestVersionResponse>({ | |||
| queryKey: [NAME_SPACE, 'installedLatestVersion', pluginIds], | |||
| queryFn: () => post<InstalledLatestVersionResponse>('/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() | |||