fix(updates): sync update center state after manual component changes

This commit is contained in:
zipg authored and GitHub committed 2026-09-20 18:43:02 +08:00
1 parent 1ef865008a
commit 4d150fc909
13 files changed
+143 -11

No files matched your search

@@ -40,6 +40,7 @@ import { translateBackendError } from "@/i18n/backend-errors";
import { runAgentOfflineExportAction } from "@/lib/driverStore/agentOfflineExportFlow";
import { DRIVER_CATEGORIES, getCategoryForAgentDriver, assertAgentDriverCategoriesComplete } from "@/lib/connection/driver-category-definitions";
import { hasAnyUpdatableDriverMatching, countInstalledDrivers, countAvailableDrivers, partitionDriversByInstallStatus, upgradeAllDriverTypes, upgradeAllMatchesFullUpdateSet, type DriverInstallStatusFilter } from "@/lib/connection/driverListFilter";
import { notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
const { t } = useI18n();
const { toast } = useToast();
@@ -497,6 +498,7 @@ async function runDriverInstall(dbType: string) {
if (managedResult.drivers) jdbcDrivers.value = managedResult.drivers;
jdbcMavenBundles.value = managedResult.bundles;
void loadDriverStoreUsage();
notifyComponentUpdatesChanged();
toast(t("driverStore.driverInstallSuccess", { label }));
return;
}
@@ -509,6 +511,7 @@ async function runDriverInstall(dbType: string) {
activeAgentOperationId.value = operationId;
cancellableAgentInstallDbType.value = dbType;
await api.installAgent(dbType, operationId);
notifyComponentUpdatesChanged();
await refreshAgents();
toast(t("driverStore.driverInstallSuccess", { label }));
} catch (e: any) {
@@ -557,6 +560,7 @@ async function upgradeAll() {
queuedDriverInstalls.value = [];
resetAgentInstallProgress();
const result = await api.upgradeAllAgents(activeAgentOperationId.value);
notifyComponentUpdatesChanged();
await refreshAgents();
if (result.cancelled > 0 && result.failed.length === 0) {
toast(t("driverStore.upgradeAllCancelled", { count: result.cancelled }));
@@ -613,6 +617,7 @@ async function uninstallDriver(dbType: string) {
if (managedResult.drivers) jdbcDrivers.value = managedResult.drivers;
jdbcMavenBundles.value = managedResult.bundles;
void loadDriverStoreUsage();
notifyComponentUpdatesChanged();
toast(t("driverStore.driverUninstallSuccess", { label }));
return;
}
@@ -622,6 +627,7 @@ async function uninstallDriver(dbType: string) {
return;
}
await api.uninstallAgent(dbType);
notifyComponentUpdatesChanged();
await refreshAgents();
toast(t("driverStore.driverUninstallSuccess", { label }));
} catch (e: any) {
@@ -744,6 +750,7 @@ async function importOfflineZip() {
resetAgentInstallProgress();
try {
const result = await api.importAgentsFromZip(selected, activeAgentOperationId.value);
notifyComponentUpdatesChanged();
await Promise.all([refreshAgents(), loadJdbcDrivers(), loadJdbcPluginStatus()]);
toastOfflineImportResult(result);
} catch (e: any) {
@@ -796,6 +803,7 @@ async function importDriverFile(driver: AgentDriverInfo) {
resetAgentInstallProgress();
try {
const result = await api.importAgentsFromZip(selected, activeAgentOperationId.value);
notifyComponentUpdatesChanged();
await Promise.all([refreshAgents(), loadJdbcDrivers(), loadJdbcPluginStatus()]);
toastOfflineImportResult(result);
} finally {
@@ -805,6 +813,7 @@ async function importDriverFile(driver: AgentDriverInfo) {
} else {
await api.importAgentDriver(dbType, selected);
await refreshAgents();
notifyComponentUpdatesChanged();
toast(t("driverStore.driverImportSuccess", { label }));
}
};
@@ -1288,6 +1297,7 @@ async function installJdbcPlugin() {
try {
jdbcPluginStatus.value = await api.installJdbcPlugin();
emitDriverUpdateCount();
notifyComponentUpdatesChanged();
toast(t("settings.jdbcPluginInstallSuccess"));
await loadJdbcDrivers();
} catch (e: any) {
@@ -1318,6 +1328,7 @@ async function installJdbcPluginLocal() {
try {
jdbcPluginStatus.value = await api.installJdbcPluginLocal(selected);
emitDriverUpdateCount();
notifyComponentUpdatesChanged();
toast(t("settings.jdbcPluginInstallSuccess"));
await loadJdbcDrivers();
} catch (e: any) {
@@ -1334,6 +1345,7 @@ async function uninstallJdbcPlugin() {
try {
jdbcPluginStatus.value = await api.uninstallJdbcPlugin();
emitDriverUpdateCount();
notifyComponentUpdatesChanged();
toast(t("settings.jdbcPluginUninstallSuccess"));
await loadJdbcDrivers();
} catch (e: any) {
@@ -92,6 +92,7 @@ import { configuredDatabaseProductName, connectionConfigFingerprint, databaseInf
import { agentDriverInstallKey, appendAgentDriverUpdateHint, connectionUsesSsh, hasAgentDriverUpdate, showAgentDriverInstallHint, type AgentDriverInstallState, type DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
import { prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriver";
import { JDBCX_DEFAULT_URL, JDBCX_DRIVER_PROFILE, JDBCX_JDBC_DRIVER_CLASS, ensureJdbcxRuntimeDrivers, isJdbcxRuntimeBundle, isJdbcxRuntimePath, jdbcxHighPrivilegeExtensionsEnabled, setJdbcxHighPrivilegeExtensionsEnabled } from "@/lib/database/jdbcxBuiltinDriver";
import { notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection";
import { connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs } from "@/lib/connection/connectionAttemptTimeout";
import { consulAgentAddressesMatch } from "@/lib/consul/agentTarget";
@@ -1964,6 +1965,7 @@ async function ensureRequiredAgentDriverInstalled(config: ConnectionConfig): Pro
const operationId = beginAgentDriverInstall(driverKey, label);
try {
await api.installAgent(driverKey, operationId);
notifyComponentUpdatesChanged();
await refreshLocalAgentDrivers();
// A stale promise (cancelled then retried) must not close the retry's dialog.
if (agentInstallOperationId.value === operationId) finishAgentDriverInstall(operationId);
@@ -1999,6 +2001,7 @@ async function ensureRequiredJdbcxDriverInstalled(config: ConnectionConfig): Pro
testResult.value = { ok: true, message: "Installing JDBC plugin..." };
});
if (!result) return;
notifyComponentUpdatesChanged();
jdbcMavenBundles.value = result.bundles;
addJdbcDriverPaths(result.paths);
@@ -2012,6 +2015,7 @@ async function ensureRequiredJdbcxDriverInstalled(config: ConnectionConfig): Pro
async function ensureRequiredJdbcProductRuntimeInstalled(config: ConnectionConfig): Promise<void> {
const result = await ensureRegisteredJdbcProductRuntimeDrivers(config, api);
if (!result) return;
notifyComponentUpdatesChanged();
jdbcMavenBundles.value = result.bundles;
jdbcDriverPathsInput.value = result.paths.join("\n");
@@ -2031,6 +2035,7 @@ async function ensureRequiredGaussdbMJdbcRuntime(config: ConnectionConfig): Prom
if (status.installed && status.compatible) return;
testResult.value = { ok: true, message: t("connection.gaussdbMJdbcPluginInstalling") };
await api.installJdbcPlugin();
notifyComponentUpdatesChanged();
}
async function installSqlServerLegacyCompatibilityComponentIfNeeded(): Promise<boolean> {
@@ -2040,6 +2045,7 @@ async function installSqlServerLegacyCompatibilityComponentIfNeeded(): Promise<b
const operationId = beginAgentDriverInstall(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, label);
try {
await api.installAgent(SQLSERVER_LEGACY_COMPATIBILITY_DRIVER_KEY, operationId);
notifyComponentUpdatesChanged();
await refreshLocalAgentDrivers();
// A stale promise (cancelled then retried) must not close the retry's dialog.
if (agentInstallOperationId.value === operationId) finishAgentDriverInstall(operationId);
@@ -205,6 +205,7 @@ import {
type McpLaunchConfig,
} from "@/lib/mcp/mcpConfigTemplates";
import { beginMcpStatusRequest, mcpUpdateAvailability } from "@/lib/mcp/mcpUpdateStatus";
import { notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COLUMNS, MCP_TOOL_OPTIONS, mcpExecutionModeFromPolicy, mcpPolicyFieldsForExecutionMode, toggleMcpAllowedToolName, type McpExecutionMode } from "@/lib/mcp/mcpPolicySelection";
import { isMacOS, isWindows } from "@/lib/backend/platform";
import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState";
@@ -3793,6 +3794,7 @@ async function installMcp() {
mcpInstallError.value = false;
// 安装成功后刷新状态
await refreshMcpStatus();
notifyComponentUpdatesChanged();
} catch (e: any) {
mcpInstallMessage.value = e?.message || String(e);
mcpInstallError.value = true;
@@ -3815,6 +3817,7 @@ async function uninstallMcp() {
try {
mcpInstallMessage.value = await uninstallMcpServer();
await refreshMcpStatus();
notifyComponentUpdatesChanged();
} catch (e: any) {
mcpInstallMessage.value = t("settings.mcpUninstallFailed", { error: e?.message || String(e) });
mcpInstallError.value = true;
@@ -19,7 +19,7 @@ import { physicalDropPositionInsideRect } from "@/lib/ai/aiAttachments";
import { createFrontendPluginRegistry, pluginConnectionProviderIcon } from "@/lib/plugins/frontendPlugin";
import { beaconPluginInstall, buildMarketplacePluginListings, filterMarketplacePluginListings, listingRepositoryCanVerify, marketplaceHomepageUrl, type MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
import { isBatchSelectableListing, runBatch } from "@/lib/plugins/pluginBatch";
import { COMPONENT_PLUGINS_UPDATED_EVENT } from "@/lib/updates/componentUpdateEvents";
import { COMPONENT_PLUGINS_UPDATED_EVENT, notifyComponentUpdatesChanged } from "@/lib/updates/componentUpdateEvents";
import { formatBytes } from "@/lib/database/serverMetrics";
import type { PluginCenterFocus } from "@/lib/plugins/pluginCenterNavigation";
import { useConnectionStore } from "@/stores/connectionStore";
@@ -216,6 +216,7 @@ async function installMarketplaceListing(listing: MarketplacePluginListing) {
const result = await installListing(listing);
toast(t(listing.status === "update" ? "pluginPlatform.updateSuccess" : "pluginPlatform.installSuccess", { name: result.plugin.manifest.name, version: result.plugin.manifest.version }));
installedPlugins.value = await api.listPlugins();
notifyComponentUpdatesChanged();
selectPlugin(result.plugin.manifest.id);
} catch (cause) {
toast(translateBackendError(t, cause), 8000);
@@ -223,6 +224,7 @@ async function installMarketplaceListing(listing: MarketplacePluginListing) {
// instance), so re-read the installed list instead of leaving the card on state it may no longer
// describe.
installedPlugins.value = await api.listPlugins().catch(() => installedPlugins.value);
notifyComponentUpdatesChanged();
} finally {
marketplaceInstallingKey.value = "";
}
@@ -325,6 +327,7 @@ async function runBatchInstallUpdate() {
clearBatchSelection();
reportBatchSummary(outcome);
await refreshAfterBatch();
notifyComponentUpdatesChanged();
} finally {
batchRunning.value = false;
}
@@ -348,6 +351,7 @@ async function runBatchUninstall() {
clearBatchSelection();
reportBatchSummary(outcome);
await refreshAfterBatch();
notifyComponentUpdatesChanged();
} finally {
batchRunning.value = false;
}
@@ -543,6 +547,7 @@ async function finishInstall(result: PluginInstallResult) {
toast(t("pluginPlatform.installSuccess", { name: result.plugin.manifest.name, version: result.plugin.manifest.version }));
clearPluginIconCache();
installedPlugins.value = await api.listPlugins();
notifyComponentUpdatesChanged();
selectPlugin(result.plugin.manifest.id);
activeSection.value = "installed";
}
@@ -689,6 +694,7 @@ async function rollbackSelectedPlugin() {
toast(t("pluginPlatform.rollbackSuccess", { version: result.plugin.manifest.version }));
clearPluginIconCache();
installedPlugins.value = await api.listPlugins();
notifyComponentUpdatesChanged();
selectPlugin(result.plugin.manifest.id);
} catch (cause) {
toast(translateBackendError(t, cause), 8000);
@@ -703,6 +709,7 @@ async function uninstallSelectedPlugin() {
operating.value = true;
try {
installedPlugins.value = await api.uninstallPlugin(definition.plugin.manifest.id);
notifyComponentUpdatesChanged();
clearPluginIconCache();
toast(t("pluginPlatform.uninstallSuccess", { name: definition.plugin.manifest.name }));
selectFirstProvider();
@@ -5,7 +5,7 @@ import { readFileSync } from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { InstalledPlugin, PluginRepositoryCatalogResult } from "@/types/database";
import type { MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
import { COMPONENT_PLUGINS_UPDATED_EVENT } from "@/lib/updates/componentUpdateEvents";
import { COMPONENT_PLUGINS_UPDATED_EVENT, COMPONENT_UPDATES_CHANGED_EVENT } from "@/lib/updates/componentUpdateEvents";
const mocks = vi.hoisted(() => ({
listPlugins: vi.fn(),
@@ -246,6 +246,24 @@ describe("PluginContributionsPanel batch source validation", () => {
});
});
describe("PluginContributionsPanel update center synchronization", () => {
it("notifies after a single marketplace update without duplicating per batch item", async () => {
const listener = vi.fn();
window.addEventListener(COMPONENT_UPDATES_CHANGED_EVENT, listener);
try {
await state.installMarketplaceListing(state.marketplaceListings[0]);
expect(listener).toHaveBeenCalledOnce();
listener.mockClear();
state.selectAllUpdatable();
await state.runBatchInstallUpdate();
expect(listener).toHaveBeenCalledOnce();
} finally {
window.removeEventListener(COMPONENT_UPDATES_CHANGED_EVENT, listener);
}
});
});
describe("PluginContributionsPanel installed plugin pin controls", () => {
it("renders selection and pin actions as sibling native buttons", async () => {
state.batchMode = false;