fix(updates): show why a plugin update was blocked

This commit is contained in:
zipg authored and GitHub committed 2026-09-22 15:41:25 +08:00
1 parent 72d25273df
commit 6b06522ced
3 files changed
+63 -8

No files matched your search

+10 -1
View File
@@ -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<ReturnType<typeof componentUpdates.installCategory>>) {
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<ReturnType<typeof component
if (result.plugins > 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({
@@ -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" }]);
});
});
@@ -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}`);
}
}
}