feat(agents): concurrent all-agent-driver upgrade
This commit is contained in:
1 parent
2a99f91099
commit
95db01e359
15 files changed
+1083
-282
No files matched your search
@@ -95,25 +95,28 @@ export const clearDriverDownloadCache = forward("clearDriverDownloadCache");
|
||||
export const getDriverRuntimeSummary = forward("getDriverRuntimeSummary");
|
||||
export const stopDriverRuntime = forward("stopDriverRuntime");
|
||||
export const restartDriverRuntime = forward("restartDriverRuntime");
|
||||
export async function installAgent(dbType: string) {
|
||||
export async function installAgent(dbType: string, operationId?: string) {
|
||||
const backend = await getBackend();
|
||||
return backend.installAgent(dbType, useSettingsStore().editorSettings.updateDownloadSource);
|
||||
return backend.installAgent(dbType, useSettingsStore().editorSettings.updateDownloadSource, operationId);
|
||||
}
|
||||
export async function upgradeAllAgents() {
|
||||
export async function upgradeAllAgents(operationId?: string) {
|
||||
const backend = await getBackend();
|
||||
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource);
|
||||
return backend.upgradeAllAgents(useSettingsStore().editorSettings.updateDownloadSource, operationId);
|
||||
}
|
||||
export const checkAgentUpdateBlockers = forward("checkAgentUpdateBlockers");
|
||||
export const uninstallAgent = forward("uninstallAgent");
|
||||
export const getAgentJavaRuntimeConfig = forward("getAgentJavaRuntimeConfig");
|
||||
export const setAgentJavaRuntimeConfig = forward("setAgentJavaRuntimeConfig");
|
||||
export const invalidateAgentRegistryCache = forward("invalidateAgentRegistryCache");
|
||||
export const importAgentsFromZip = forward("importAgentsFromZip");
|
||||
export async function importAgentsFromZip(fileOrPath: string | File, operationId?: string) {
|
||||
const backend = await getBackend();
|
||||
return backend.importAgentsFromZip(fileOrPath, operationId);
|
||||
}
|
||||
export const importAgentDriver = forward("importAgentDriver");
|
||||
export const importAgentJar = importAgentDriver;
|
||||
export async function reinstallJre(jreKey?: string) {
|
||||
export async function reinstallJre(jreKey?: string, operationId?: string) {
|
||||
const backend = await getBackend();
|
||||
return backend.reinstallJre(jreKey, useSettingsStore().editorSettings.updateDownloadSource);
|
||||
return backend.reinstallJre(jreKey, useSettingsStore().editorSettings.updateDownloadSource, operationId);
|
||||
}
|
||||
export const uninstallJre = forward("uninstallJre");
|
||||
export const listenAgentInstallProgress = forward("listenAgentInstallProgress");
|
||||
|
||||
@@ -437,12 +437,12 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
|
||||
await post("/api/agents/runtime/restart", { runtimeId });
|
||||
}
|
||||
|
||||
export async function installAgent(dbType: string, _source?: UpdateDownloadSource): Promise<void> {
|
||||
await post("/api/agents/install", { dbType });
|
||||
export async function installAgent(dbType: string, _source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||
await post("/api/agents/install", { dbType, operationId });
|
||||
}
|
||||
|
||||
export async function upgradeAllAgents(_source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
|
||||
return post("/api/agents/upgrade-all", {});
|
||||
export async function upgradeAllAgents(_source?: UpdateDownloadSource, operationId?: string): Promise<UpgradeAllAgentDriversResult> {
|
||||
return post("/api/agents/upgrade-all", { operationId });
|
||||
}
|
||||
|
||||
export async function checkAgentUpdateBlockers(_dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
|
||||
@@ -465,11 +465,12 @@ export async function invalidateAgentRegistryCache(): Promise<void> {
|
||||
await post("/api/agents/invalidate-registry-cache", {});
|
||||
}
|
||||
|
||||
export async function importAgentsFromZip(fileOrPath: string | File): Promise<number> {
|
||||
export async function importAgentsFromZip(fileOrPath: string | File, operationId?: string): Promise<number> {
|
||||
if (typeof fileOrPath === "string") {
|
||||
throw new Error("Offline ZIP import in web mode requires a File object, not a file path");
|
||||
}
|
||||
const formData = new FormData();
|
||||
if (operationId) formData.append("operationId", operationId);
|
||||
formData.append("file", fileOrPath);
|
||||
const res = await fetch(apiUrl("/api/agents/import-offline"), { method: "POST", body: formData });
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
@@ -496,8 +497,8 @@ export async function importAgentDriver(dbType: string, pathOrFile: string | Fil
|
||||
|
||||
export const importAgentJar = importAgentDriver;
|
||||
|
||||
export async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource): Promise<void> {
|
||||
await post("/api/agents/reinstall-jre", { jreKey });
|
||||
export async function reinstallJre(jreKey?: string, _source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||
await post("/api/agents/reinstall-jre", { jreKey, operationId });
|
||||
}
|
||||
|
||||
export async function uninstallJre(jreKey: string): Promise<void> {
|
||||
|
||||
@@ -318,6 +318,7 @@ export interface DroppedFilePreviewSqlOptions {
|
||||
export type XlsxCellValue = string | number | boolean | null;
|
||||
|
||||
export interface DriverInstallProgress {
|
||||
operation_id?: string;
|
||||
step: string;
|
||||
downloaded?: number;
|
||||
total?: number;
|
||||
@@ -1364,12 +1365,12 @@ export async function restartDriverRuntime(runtimeId: string): Promise<void> {
|
||||
return invoke("restart_driver_runtime", { runtimeId });
|
||||
}
|
||||
|
||||
export async function installAgent(dbType: string, source?: UpdateDownloadSource): Promise<void> {
|
||||
return invoke("install_agent", { dbType, source });
|
||||
export async function installAgent(dbType: string, source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||
return invoke("install_agent", { dbType, source, operationId });
|
||||
}
|
||||
|
||||
export async function upgradeAllAgents(source?: UpdateDownloadSource): Promise<UpgradeAllAgentDriversResult> {
|
||||
return invoke("upgrade_all_agents", { source });
|
||||
export async function upgradeAllAgents(source?: UpdateDownloadSource, operationId?: string): Promise<UpgradeAllAgentDriversResult> {
|
||||
return invoke("upgrade_all_agents", { source, operationId });
|
||||
}
|
||||
|
||||
export async function checkAgentUpdateBlockers(dbTypes: string[]): Promise<AgentUpdateBlocker[]> {
|
||||
@@ -1392,11 +1393,11 @@ export async function invalidateAgentRegistryCache(): Promise<void> {
|
||||
return invoke("invalidate_agent_registry_cache");
|
||||
}
|
||||
|
||||
export async function importAgentsFromZip(path: string | File): Promise<number> {
|
||||
export async function importAgentsFromZip(path: string | File, operationId?: string): Promise<number> {
|
||||
if (typeof path !== "string") {
|
||||
throw new Error("Desktop offline ZIP import requires a local file path");
|
||||
}
|
||||
return invoke("import_agents_from_zip", { path });
|
||||
return invoke("import_agents_from_zip", { path, operationId });
|
||||
}
|
||||
|
||||
export async function importAgentDriver(dbType: string, path: string | File): Promise<void> {
|
||||
@@ -1408,8 +1409,8 @@ export async function importAgentDriver(dbType: string, path: string | File): Pr
|
||||
|
||||
export const importAgentJar = importAgentDriver;
|
||||
|
||||
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource): Promise<void> {
|
||||
return invoke("reinstall_jre", { jreKey, source });
|
||||
export async function reinstallJre(jreKey?: string, source?: UpdateDownloadSource, operationId?: string): Promise<void> {
|
||||
return invoke("reinstall_jre", { jreKey, source, operationId });
|
||||
}
|
||||
|
||||
export async function uninstallJre(jreKey: string): Promise<void> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { driverInstallProgressChannel, updateDriverInstallProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||
import { driverInstallProgressChannel, isDriverInstallProgressForOperation, isDriverInstallProgressTarget, updateDriverInstallProgress, updatePerDriverProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
|
||||
|
||||
describe("driver install progress channels", () => {
|
||||
it("classifies agent and JDBC plugin progress independently", () => {
|
||||
@@ -17,6 +17,16 @@ describe("driver install progress channels", () => {
|
||||
expect(updateDriverInstallProgress(jdbcProgress, agentProgress, "jdbc-plugin")).toBe(jdbcProgress);
|
||||
});
|
||||
|
||||
it("rejects progress and terminal events from another operation", () => {
|
||||
expect(isDriverInstallProgressForOperation({ operation_id: "upgrade-b", step: "driver", db_type: "mysql" }, "upgrade-a")).toBe(false);
|
||||
expect(isDriverInstallProgressForOperation({ operation_id: "upgrade-b", step: "all-done" }, "upgrade-a")).toBe(false);
|
||||
expect(isDriverInstallProgressForOperation({ operation_id: "upgrade-a", step: "all-done" }, "upgrade-a")).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps legacy progress compatible when no operation id is emitted", () => {
|
||||
expect(isDriverInstallProgressForOperation({ step: "driver", db_type: "mysql" }, "upgrade-a")).toBe(true);
|
||||
});
|
||||
|
||||
it("allows a built-in driver backed by JDBC to consume JDBC progress explicitly", () => {
|
||||
const jdbcProgress: DriverInstallProgress = { step: "jdbc-plugin", downloaded: 40, total: 100 };
|
||||
|
||||
@@ -41,4 +51,27 @@ describe("driver install progress channels", () => {
|
||||
expect(updateDriverInstallProgress(agentProgress, ambiguousDone, "agent")).toBe(agentProgress);
|
||||
expect(updateDriverInstallProgress(jdbcProgress, ambiguousDone, "jdbc-plugin")).toBe(jdbcProgress);
|
||||
});
|
||||
|
||||
it("keeps other drivers visible when one concurrent update completes", () => {
|
||||
const progressByDbType: Record<string, DriverInstallProgress | null | undefined> = {};
|
||||
updatePerDriverProgress(progressByDbType, { step: "driver", db_type: "mysql", downloaded: 50, total: 100 });
|
||||
updatePerDriverProgress(progressByDbType, { step: "driver", db_type: "oracle", downloaded: 25, total: 100 });
|
||||
updatePerDriverProgress(progressByDbType, { step: "done", db_type: "mysql" });
|
||||
|
||||
expect(progressByDbType.mysql).toBeNull();
|
||||
expect(progressByDbType.oracle).toMatchObject({ step: "driver", downloaded: 25 });
|
||||
expect(isDriverInstallProgressTarget("oracle", { installing: null, upgradingAll: true, progressMap: progressByDbType })).toBe(true);
|
||||
});
|
||||
|
||||
it("clears all tracked progress on a batch completion event", () => {
|
||||
const progressByDbType: Record<string, DriverInstallProgress | null | undefined> = {
|
||||
mysql: { step: "driver", db_type: "mysql", downloaded: 50, total: 100 },
|
||||
oracle: { step: "driver", db_type: "oracle", downloaded: 25, total: 100 },
|
||||
};
|
||||
|
||||
updatePerDriverProgress(progressByDbType, { step: "all-done" });
|
||||
|
||||
expect(progressByDbType.mysql).toBeNull();
|
||||
expect(progressByDbType.oracle).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,31 @@
|
||||
export interface DriverInstallProgress {
|
||||
operation_id?: string;
|
||||
step: string;
|
||||
downloaded?: number;
|
||||
total?: number;
|
||||
db_type?: string;
|
||||
/** Number of drivers completed so far during a batch upgrade (1-based). */
|
||||
completed?: number;
|
||||
/** Total drivers in the batch. */
|
||||
total_drivers?: number;
|
||||
}
|
||||
|
||||
export interface DriverInstallProgressTargetState {
|
||||
installing: string | null;
|
||||
upgradingAll: boolean;
|
||||
progress: DriverInstallProgress | null;
|
||||
progressMap: Record<string, DriverInstallProgress | null | undefined>;
|
||||
}
|
||||
|
||||
export type DriverInstallProgressChannel = "agent" | "jdbc-plugin";
|
||||
|
||||
const AGENT_PROGRESS_STEPS = new Set(["driver", "jre", "jre-extract", "all-done"]);
|
||||
|
||||
export function isDriverInstallProgressForOperation(progress: DriverInstallProgress, operationId: string | null): boolean {
|
||||
// Keep accepting legacy unscoped events, while preventing another active
|
||||
// install's terminal/progress event from mutating this dialog.
|
||||
return !progress.operation_id || progress.operation_id === operationId;
|
||||
}
|
||||
|
||||
export function driverInstallProgressChannel(progress: DriverInstallProgress): DriverInstallProgressChannel | null {
|
||||
if (progress.step === "jdbc-plugin" || progress.step === "jdbc-plugin-extract") return "jdbc-plugin";
|
||||
if (progress.db_type || AGENT_PROGRESS_STEPS.has(progress.step)) return "agent";
|
||||
@@ -28,7 +39,26 @@ export function updateDriverInstallProgress(current: DriverInstallProgress | nul
|
||||
return incoming;
|
||||
}
|
||||
|
||||
export function driverInstallProgressPercent(progress: DriverInstallProgress | null): number | null {
|
||||
/**
|
||||
* Update a per-driver progress map with an incoming event.
|
||||
* Returns the updated map (mutated in-place — returned for convenience).
|
||||
*/
|
||||
export function updatePerDriverProgress(progressMap: Record<string, DriverInstallProgress | null | undefined>, incoming: DriverInstallProgress): Record<string, DriverInstallProgress | null | undefined> {
|
||||
if (incoming.step === "all-done") {
|
||||
// Batch completion has no db_type, so it must be handled before routing.
|
||||
for (const key of Object.keys(progressMap)) {
|
||||
progressMap[key] = null;
|
||||
}
|
||||
return progressMap;
|
||||
}
|
||||
|
||||
const dbType = incoming.db_type;
|
||||
if (!dbType) return progressMap;
|
||||
progressMap[dbType] = incoming.step === "done" ? null : incoming;
|
||||
return progressMap;
|
||||
}
|
||||
|
||||
export function driverInstallProgressPercent(progress: DriverInstallProgress | null | undefined): number | null {
|
||||
if (!progress?.total || progress.total <= 0) return null;
|
||||
const percent = Math.round(((progress.downloaded ?? 0) / progress.total) * 100);
|
||||
return Math.min(100, Math.max(0, percent));
|
||||
@@ -36,7 +66,11 @@ export function driverInstallProgressPercent(progress: DriverInstallProgress | n
|
||||
|
||||
export function isDriverInstallProgressTarget(dbType: string, state: DriverInstallProgressTargetState): boolean {
|
||||
if (state.installing === dbType) return true;
|
||||
return state.upgradingAll && state.progress?.db_type === dbType;
|
||||
if (!state.upgradingAll) return false;
|
||||
// During batch upgrade, check the per-driver map — the driver is "active"
|
||||
// if it has a (non-null, non-"done") progress entry.
|
||||
const progress = state.progressMap[dbType];
|
||||
return progress !== null && progress !== undefined;
|
||||
}
|
||||
|
||||
export function addDriverInstallQueue(queue: string[], dbType: string, activeDbType: string | null): string[] {
|
||||
|
||||
Reference in new issue
Block a user