diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 1ee4b0d5fb..54ecd59889 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -27,6 +27,7 @@ import { useTheme } from "@/composables/useTheme"; import { canDownloadAndInstallUpdate, useAppUpdater } from "@/composables/useAppUpdater"; import { useMcpUpdateBadge } from "@/composables/useMcpUpdateBadge"; import { useComponentUpdates, type ComponentUpdateCategory } from "@/composables/useComponentUpdates"; +import type { PluginUpdateBlock } from "@/composables/useComponentUpdates"; import { COMPONENT_UPDATES_CHANGED_EVENT, notifyComponentPluginsUpdated, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents"; import { driverStoreUpdateBadgeCount, showMcpUpdateBadge, showToolbarUpdateAction } from "@/lib/updates/updateBadges"; import { @@ -1271,6 +1272,12 @@ function handleComponentUpdatesChanged() { }); } +function pluginUpdateBlockMessage(block: PluginUpdateBlock): string { + if (block.reason === "connections") return `${block.pluginName}: ${t("pluginPlatform.updateBlockedByConnections", { labels: block.connections })}`; + const key = block.reason === "operations" ? "pluginPlatform.updateBlockedByOperations" : "pluginPlatform.updateInProgress"; + return `${block.pluginName}: ${t(key)}`; +} + function reportComponentUpdateResult(result: Awaited>) { const updatedComponents = [result.drivers > 0 ? t("settings.updateDrivers") : "", result.jdbc ? t("settings.updateJdbc") : "", result.mcp ? t("settings.updateMcp") : "", result.plugins > 0 ? t("settings.updatePlugins") : ""].filter(Boolean); // Only a clean refresh is authoritative; a failed registry check must not clear stale toolbar state. @@ -1278,7 +1285,9 @@ function reportComponentUpdateResult(result: Awaited 0) notifyComponentPluginsUpdated(); if (updatedComponents.length) toast(t("updates.componentsAutoUpdated", { components: updatedComponents.join(t("updates.componentListSeparator")) })); if (result.skippedDrivers > 0) toast(t("updates.componentsAutoUpdateSkipped"), 6000); - if (result.failed.length) toast(t("updates.componentsAutoUpdateFailed", { count: result.failed.length }), 6000); + const otherFailureCount = result.failed.length - result.blockedPlugins.length; + const failureMessages = [result.blockedPlugins.map(pluginUpdateBlockMessage).join("\n"), otherFailureCount > 0 ? t("updates.componentsAutoUpdateFailed", { count: otherFailureCount }) : ""].filter(Boolean); + if (failureMessages.length) toast(failureMessages.join("\n"), 8000); if ( shouldCloseUpdateCenterAfterComponentUpdate({ diff --git a/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts b/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts index 7a2ba25789..cfa6ce2c46 100644 --- a/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts +++ b/apps/desktop/src/composables/__tests__/useComponentUpdates.spec.ts @@ -131,7 +131,7 @@ describe("useComponentUpdates", () => { expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce(); expect(mocks.installMcpServer).toHaveBeenCalledOnce(); expect(mocks.installMarketplacePlugin).toHaveBeenCalledWith({ repositoryId: "official", pluginId: "example", version: "1.1.0" }); - expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] }); + expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [], blockedPlugins: [] }); }); it("shares one update operation between automatic and manual callers", async () => { @@ -172,7 +172,7 @@ describe("useComponentUpdates", () => { expect(mocks.installJdbcPlugin).not.toHaveBeenCalled(); expect(mocks.installMcpServer).not.toHaveBeenCalled(); expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled(); - expect(result).toEqual({ drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] }); + expect(result).toEqual({ drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [], blockedPlugins: [] }); }); it("installs every manually selected category when no DBX update exists and automatic updates are disabled", async () => { @@ -190,7 +190,7 @@ describe("useComponentUpdates", () => { expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce(); expect(mocks.installMcpServer).toHaveBeenCalledOnce(); expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce(); - expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] }); + expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [], blockedPlugins: [] }); }); it("resumes a persisted manual update-all plan after restart while automatic updates are disabled", async () => { @@ -213,7 +213,7 @@ describe("useComponentUpdates", () => { expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce(); expect(mocks.installMcpServer).toHaveBeenCalledOnce(); expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce(); - expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] }); + expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [], blockedPlugins: [] }); }); it("keeps the toolbar update action hidden while a persisted restart plan is still installing", async () => { @@ -302,6 +302,32 @@ describe("useComponentUpdates", () => { const result = await updates.installCategory("jdbc"); expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce(); - expect(result).toEqual({ drivers: 0, jdbc: true, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] }); + expect(result).toEqual({ drivers: 0, jdbc: true, mcp: false, plugins: 0, skippedDrivers: 0, failed: [], blockedPlugins: [] }); + }); + + it("reports the blocking connections when a K8S plugin update cannot start", async () => { + mocks.buildMarketplacePluginListings.mockReturnValue([ + { + ...pluginUpdate, + plugin: { id: "io.dbx.k8s", latestVersion: "0.1.3" }, + name: "Kubernetes 管理", + }, + ]); + mocks.installMarketplacePlugin.mockRejectedValue(new Error("Plugin update blocked by active connections: market测试环境")); + const updates = useComponentUpdates({ isDesktop: true }); + + const result = await updates.installCategory("plugins"); + + expect(result.blockedPlugins).toEqual([{ pluginName: "Kubernetes 管理", reason: "connections", connections: "market测试环境" }]); + expect(result.failed).toEqual(["Kubernetes 管理: Plugin update blocked by active connections: market测试环境"]); + }); + + it("reports active operations separately from connections", async () => { + mocks.installMarketplacePlugin.mockRejectedValue(new Error("Plugin update blocked by active operations. Please wait for them to finish.")); + const updates = useComponentUpdates({ isDesktop: true }); + + const result = await updates.installCategory("plugins"); + + expect(result.blockedPlugins).toEqual([{ pluginName: "Example plugin", reason: "operations" }]); }); }); diff --git a/apps/desktop/src/composables/useComponentUpdates.ts b/apps/desktop/src/composables/useComponentUpdates.ts index b46ec6d41d..6121278eba 100644 --- a/apps/desktop/src/composables/useComponentUpdates.ts +++ b/apps/desktop/src/composables/useComponentUpdates.ts @@ -11,6 +11,8 @@ import type { JdbcPluginStatus } from "@/types/database"; export type { ComponentUpdateCategory } from "@/lib/updates/componentUpdateOrchestration"; +export type PluginUpdateBlock = { pluginName: string; reason: "connections"; connections: string } | { pluginName: string; reason: "operations" | "inProgress" }; + export interface ComponentUpdateResult { drivers: number; jdbc: boolean; @@ -18,6 +20,7 @@ export interface ComponentUpdateResult { plugins: number; skippedDrivers: number; failed: string[]; + blockedPlugins: PluginUpdateBlock[]; } interface ComponentUpdateRefreshOptions { @@ -25,7 +28,21 @@ interface ComponentUpdateRefreshOptions { } function emptyResult(): ComponentUpdateResult { - return { drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] }; + return { drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [], blockedPlugins: [] }; +} + +function pluginUpdateBlock(pluginName: string, message: string): PluginUpdateBlock | null { + const connectionsPrefix = "Plugin update blocked by active connections: "; + if (message.startsWith(connectionsPrefix)) { + return { pluginName, reason: "connections", connections: message.slice(connectionsPrefix.length) }; + } + if (message === "Plugin update blocked by active operations. Please wait for them to finish.") { + return { pluginName, reason: "operations" }; + } + if (message === "Plugin update is in progress. Please try again after it finishes.") { + return { pluginName, reason: "inProgress" }; + } + return null; } export function useComponentUpdates(options: { isDesktop: boolean }) { @@ -167,7 +184,10 @@ export function useComponentUpdates(options: { isDesktop: boolean }) { }); result.plugins += 1; } catch (error) { - result.failed.push(`${listing.name}: ${error instanceof Error ? error.message : String(error)}`); + const message = error instanceof Error ? error.message : String(error); + const block = pluginUpdateBlock(listing.name, message); + if (block) result.blockedPlugins.push(block); + result.failed.push(`${listing.name}: ${message}`); } } }