feat(updates): restore driver download progress and batch counter

This commit is contained in:
lxk955
2026-10-19 03:21:03 +08:00
committed by GitHub
parent 7b40a6ffca
commit d6cc18b2a9
8 changed files with 233 additions and 15 deletions
+3
View File
@@ -5731,6 +5731,9 @@ onUnmounted(() => {
:is-ignoring-update="isIgnoringUpdate"
:active-task-count="activeUpdateTaskCount"
:driver-updates="componentUpdates.driverUpdates.value"
:driver-batch-completed="componentUpdates.driverBatchCompleted.value"
:driver-batch-total="componentUpdates.driverBatchTotal.value"
:driver-progress-map="componentUpdates.driverProgressMap.value"
:jdbc-update="componentUpdates.jdbcPluginStatus.value"
:mcp-update="componentUpdates.mcpStatus.value"
:plugin-updates="componentUpdates.pluginUpdates.value"
@@ -24,6 +24,7 @@ import {
addDriverInstallQueue,
driverInstallProgressChannel,
driverInstallProgressPercent,
formatDriverInstallProgressText,
isDriverInstallCanceledError,
isDriverInstallCancellationTarget,
isDriverInstallProgressForOperation,
@@ -244,15 +245,7 @@ const installedJres = computed(() => {
});
function formatProgressText(p: DriverInstallProgress | null | undefined): string {
if (!p) return "";
if (p.step === "jre-extract") return t("driverStore.progressJreExtract");
if (p.step === "jdbc-plugin-extract") return t("driverStore.progressJdbcPluginExtract");
const label = p.step === "jre" ? t("driverStore.progressDownloadJre") : p.step === "jdbc-plugin" ? t("driverStore.progressDownloadJdbcPlugin") : t("driverStore.progressDownloadDriver");
if (!p.total) return `${label}...`;
const pct = Math.round(((p.downloaded ?? 0) / p.total) * 100);
const dl = formatSize(p.downloaded ?? 0);
const total = formatSize(p.total);
return `${label} ${dl} / ${total} (${pct}%)`;
return formatDriverInstallProgressText(p, t);
}
function getAgentProgressText(dbType: string): string {
+42
View File
@@ -691,4 +691,46 @@ describe("UpdateDialog aggregate update center", () => {
expect(rowFor("moved plugin")?.textContent).toContain(hint);
expect(rowFor("same plugin")?.textContent).not.toContain(hint);
});
it("renders driver install progress circle and batch counter during driver update", async () => {
const driver = {
db_type: "mysql",
label: "MySQL",
version: "0.1.50",
installed: true,
installed_version: "0.1.40",
update_available: true,
};
await mountDialog(0, {}, undefined, {
updateInfo: null,
driverUpdates: [driver],
componentUpdatesUpdating: true,
updatingComponent: "drivers",
driverBatchCompleted: 1,
driverBatchTotal: 3,
driverProgressMap: {
mysql: {
step: "driver",
db_type: "mysql",
downloaded: 45 * 1024 * 1024,
total: 100 * 1024 * 1024,
},
},
});
const driversTab = document.body.querySelector<HTMLButtonElement>('[data-update-tab="drivers"]');
driversTab?.click();
await flushDialog();
// Driver row shows progress circle with percentage 45 and status role
const progressStatus = document.body.querySelector('[role="status"]');
expect(progressStatus).not.toBeNull();
expect(progressStatus?.textContent).toContain("45");
expect(progressStatus?.getAttribute("title")).toContain("45%");
// Footer button shows batch progress Upgrading (1/3) or Updating (1/3)
const footer = document.body.querySelector("[data-update-footer]");
expect(footer?.textContent).toMatch(/1\/3/);
});
});
@@ -16,6 +16,8 @@ import { canDownloadAndInstallUpdate, isNewerRemoteVersion, resolveReleaseTagUrl
import type { ComponentUpdateCategory } from "@/composables/useComponentUpdates";
import { currentLocale } from "@/i18n";
import { changelogLangFromLocale, createLatestRequestGuard, fetchChangelog, type ChangelogRelease } from "@/lib/app/changelog";
import DriverInstallProgressCircle from "@/components/config/DriverInstallProgressCircle.vue";
import { driverInstallProgressPercent, formatDriverInstallProgressText, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
type UpdateTab = "app" | ComponentUpdateCategory;
@@ -37,6 +39,9 @@ const props = withDefaults(
updateCheckFailed: boolean;
updateDownloadSource: UpdateDownloadSource;
driverUpdates?: AgentDriverInfo[];
driverBatchCompleted?: number;
driverBatchTotal?: number;
driverProgressMap?: Record<string, DriverInstallProgress | null | undefined>;
jdbcUpdate?: JdbcPluginStatus | null;
mcpUpdate?: McpServerStatus | null;
pluginUpdates?: MarketplacePluginListing[];
@@ -48,6 +53,9 @@ const props = withDefaults(
}>(),
{
driverUpdates: () => [],
driverBatchCompleted: 0,
driverBatchTotal: 0,
driverProgressMap: () => ({}),
jdbcUpdate: null,
mcpUpdate: null,
pluginUpdates: () => [],
@@ -122,6 +130,33 @@ const selectedCategoryHasUpdate = computed(() => {
return false;
});
const isAnyComponentUpdating = computed(() => props.componentUpdatesUpdating || props.updatingComponent !== null);
function getDriverProgress(dbType: string): DriverInstallProgress | null {
return props.driverProgressMap?.[dbType] ?? null;
}
function isDriverProgressActive(dbType: string): boolean {
const p = getDriverProgress(dbType);
return Boolean(p && p.step !== "done" && p.step !== "all-done");
}
function getDriverProgressPercent(dbType: string): number | null {
return driverInstallProgressPercent(getDriverProgress(dbType));
}
function getDriverProgressTitle(dbType: string): string {
return formatDriverInstallProgressText(getDriverProgress(dbType), t) || t("driverStore.updating");
}
const updatingButtonText = computed(() => {
if (props.driverBatchTotal > 0) {
return t("driverStore.upgradingProgress", {
current: props.driverBatchCompleted,
total: props.driverBatchTotal,
});
}
return t("updates.updating");
});
// Component update tasks are owned by the app-level composable. Closing the
// dialog only leaves them running in the background; replacing an app update
// is the only operation that must keep the modal open.
@@ -407,7 +442,8 @@ watch(
<span class="min-w-0 truncate font-medium">{{ driver.label }}</span>
<span class="flex shrink-0 items-center gap-3">
<span class="text-xs tabular-nums text-muted-foreground">{{ driver.installed_version || driver.version }} → {{ driver.version }}</span>
<span class="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">{{ t("settings.updateAvailable") }}</span>
<DriverInstallProgressCircle v-if="isDriverProgressActive(driver.db_type)" :percent="getDriverProgressPercent(driver.db_type)" :title="getDriverProgressTitle(driver.db_type)" />
<span v-else class="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary">{{ t("settings.updateAvailable") }}</span>
</span>
</div>
</div>
@@ -512,7 +548,7 @@ watch(
<Button variant="ghost" class="shrink-0" @click="handleOpenChange(false)">{{ t("updates.updateInBackground") }}</Button>
<Button class="shrink-0" disabled>
<Loader2 class="h-4 w-4 animate-spin" />
{{ t("updates.updating") }}
{{ updatingButtonText }}
</Button>
</template>
<Button v-else-if="selectedCategory" :disabled="!selectedCategoryHasUpdate || isInstallingUpdate || isUpdatingAll" @click="emit('install-component-updates', selectedCategory)">
@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
fetchPluginMarketplaceCatalogs: vi.fn(),
checkAgentUpdateBlockers: vi.fn(),
upgradeAllAgents: vi.fn(),
listenAgentInstallProgress: vi.fn(),
installJdbcPlugin: vi.fn(),
installMcpServer: vi.fn(),
installMarketplacePlugin: vi.fn(),
@@ -74,6 +75,7 @@ beforeEach(() => {
mocks.buildMarketplacePluginListings.mockReturnValue([pluginUpdate]);
mocks.checkAgentUpdateBlockers.mockResolvedValue([]);
mocks.upgradeAllAgents.mockResolvedValue({ upgraded: 1, failed: [], cancelled: 0 });
mocks.listenAgentInstallProgress.mockResolvedValue(vi.fn());
mocks.installJdbcPlugin.mockResolvedValue({ installed: true, update_available: false });
mocks.installMcpServer.mockResolvedValue("updated");
mocks.installMarketplacePlugin.mockResolvedValue({});
@@ -435,4 +437,58 @@ describe("useComponentUpdates", () => {
expect(result.blockedPlugins).toEqual([{ pluginName: "Example plugin", reason: "operations" }]);
});
it("tracks driver install progress and batch counts during driver update", async () => {
let capturedHandler: ((payload: unknown) => void) | null = null;
mocks.listenAgentInstallProgress.mockImplementation(async (handler: (payload: unknown) => void) => {
capturedHandler = handler;
return () => {};
});
const upgradeDeferred = deferred<{ upgraded: number; failed: any[]; cancelled: number }>();
mocks.upgradeAllAgents.mockReturnValue(upgradeDeferred.promise);
const updates = useComponentUpdates({ isDesktop: true });
const updatePromise = updates.installCategory("drivers");
await vi.waitFor(() => expect(mocks.listenAgentInstallProgress).toHaveBeenCalledOnce());
expect(capturedHandler).not.toBeNull();
// Initial batch total is set from updatable drivers count
expect(updates.driverBatchTotal.value).toBe(1);
expect(updates.driverBatchCompleted.value).toBe(0);
// Simulate progress event for downloading mysql driver
capturedHandler!({
step: "driver",
db_type: "mysql",
downloaded: 40,
total: 100,
total_drivers: 1,
});
expect(updates.driverProgressMap.value.mysql).toMatchObject({
step: "driver",
db_type: "mysql",
downloaded: 40,
total: 100,
});
// Simulate completion of mysql driver
capturedHandler!({
step: "done",
db_type: "mysql",
});
expect(updates.driverBatchCompleted.value).toBe(1);
expect(updates.driverProgressMap.value.mysql).toBeNull();
upgradeDeferred.resolve({ upgraded: 1, failed: [], cancelled: 0 });
await updatePromise;
// After finish, progress map and counters reset
expect(updates.driverProgressMap.value).toEqual({});
expect(updates.driverBatchCompleted.value).toBe(0);
expect(updates.driverBatchTotal.value).toBe(0);
});
});
@@ -8,6 +8,8 @@ import { isUpdatePreviewMockEnabled, previewDriverUpdates, previewJdbcUpdate, pr
import type { ComponentUpdateCategory } from "@/lib/updates/componentUpdateOrchestration";
import type { AgentDriverInfo, AgentUpdateBlocker, McpServerStatus } from "@/lib/backend/tauri";
import type { JdbcPluginStatus } from "@/types/database";
import { uuid } from "@/lib/common/utils";
import { isDriverInstallProgressForOperation, updatePerDriverProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
export type { ComponentUpdateCategory } from "@/lib/updates/componentUpdateOrchestration";
@@ -56,6 +58,9 @@ export function useComponentUpdates(options: { isDesktop: boolean }) {
const updating = ref(false);
const updatingCategory = ref<ComponentUpdateCategory | null>(null);
const lastError = ref("");
const driverBatchCompleted = ref(0);
const driverBatchTotal = ref(0);
const driverProgressMap = ref<Record<string, DriverInstallProgress>>({});
const driverUpdates = computed(() => drivers.value.filter((driver) => driver.update_available));
const pluginUpdates = computed(() => pluginListings.value.filter((listing) => listing.status === "update"));
@@ -143,9 +148,36 @@ export function useComponentUpdates(options: { isDesktop: boolean }) {
result.blockedDrivers = blockers;
return;
}
const upgraded = await api.upgradeAllAgents();
result.drivers += upgraded.upgraded;
if (upgraded.failed.length) result.failed.push(...upgraded.failed.map((item) => `${item.db_type}: ${item.error}`));
const operationId = uuid();
driverBatchCompleted.value = 0;
driverBatchTotal.value = updatable.length;
driverProgressMap.value = {};
let unlisten: (() => void) | null = null;
try {
if (typeof api.listenAgentInstallProgress === "function") {
unlisten = await api.listenAgentInstallProgress((payload) => {
const incoming = payload as DriverInstallProgress;
if (!isDriverInstallProgressForOperation(incoming, operationId)) return;
if (incoming.total_drivers) {
driverBatchTotal.value = incoming.total_drivers;
}
if (incoming.step === "done" && incoming.db_type) {
driverBatchCompleted.value++;
}
updatePerDriverProgress(driverProgressMap.value, incoming);
driverProgressMap.value = { ...driverProgressMap.value };
});
}
const upgraded = await api.upgradeAllAgents(operationId);
result.drivers += upgraded.upgraded;
if (upgraded.failed.length) result.failed.push(...upgraded.failed.map((item) => `${item.db_type}: ${item.error}`));
} finally {
unlisten?.();
driverProgressMap.value = {};
driverBatchCompleted.value = 0;
driverBatchTotal.value = 0;
}
}
async function updateJdbc(result: ComponentUpdateResult) {
@@ -270,6 +302,9 @@ export function useComponentUpdates(options: { isDesktop: boolean }) {
updatingCategory,
lastError,
driverUpdateCount,
driverBatchCompleted,
driverBatchTotal,
driverProgressMap,
jdbcUpdateAvailable,
mcpUpdateAvailable,
pluginUpdateCount,
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { driverInstallProgressChannel, isDriverInstallProgressForOperation, isDriverInstallProgressTarget, updateDriverInstallProgress, updatePerDriverProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
import { formatDriverInstallProgressText, driverInstallProgressChannel, isDriverInstallProgressForOperation, isDriverInstallProgressTarget, updateDriverInstallProgress, updatePerDriverProgress, type DriverInstallProgress } from "@/lib/connection/driverInstallProgressUi";
describe("driver install progress channels", () => {
it("classifies agent and JDBC plugin progress independently", () => {
@@ -74,4 +74,39 @@ describe("driver install progress channels", () => {
expect(progressByDbType.mysql).toBeNull();
expect(progressByDbType.oracle).toBeNull();
});
it("formats progress text with byte sizes, percentage, and localized labels", () => {
const t = (key: string) => {
const dict: Record<string, string> = {
"driverStore.progressDownloadDriver": "Downloading driver",
"driverStore.progressDownloadJre": "Downloading JRE",
"driverStore.progressJreExtract": "Extracting JRE...",
};
return dict[key] ?? key;
};
expect(formatDriverInstallProgressText(null, t)).toBe("");
expect(formatDriverInstallProgressText({ step: "jre-extract" }, t)).toBe("Extracting JRE...");
expect(formatDriverInstallProgressText({ step: "driver" }, t)).toBe("Downloading driver...");
expect(
formatDriverInstallProgressText(
{
step: "driver",
downloaded: 10 * 1024 * 1024,
total: 20 * 1024 * 1024,
},
t,
),
).toBe("Downloading driver 10.0 MB / 20.0 MB (50%)");
expect(
formatDriverInstallProgressText(
{
step: "driver",
downloaded: 512 * 1024,
total: 2 * 1024 * 1024,
},
t,
),
).toBe("Downloading driver 512 KB / 2.0 MB (25%)");
});
});
@@ -122,6 +122,24 @@ export function driverInstallProgressPercent(progress: DriverInstallProgress | n
return Math.min(100, Math.max(0, percent));
}
export function formatDriverInstallProgressSize(bytes?: number): string {
if (!bytes || bytes <= 0) return "0 MB";
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
export function formatDriverInstallProgressText(p: DriverInstallProgress | null | undefined, t: (key: string) => string): string {
if (!p) return "";
if (p.step === "jre-extract") return t("driverStore.progressJreExtract");
if (p.step === "jdbc-plugin-extract") return t("driverStore.progressJdbcPluginExtract");
const label = p.step === "jre" ? t("driverStore.progressDownloadJre") : p.step === "jdbc-plugin" ? t("driverStore.progressDownloadJdbcPlugin") : t("driverStore.progressDownloadDriver");
if (!p.total) return `${label}...`;
const pct = Math.round(((p.downloaded ?? 0) / p.total) * 100);
const dl = formatDriverInstallProgressSize(p.downloaded);
const total = formatDriverInstallProgressSize(p.total);
return `${label} ${dl} / ${total} (${pct}%)`;
}
export function isDriverInstallProgressTarget(dbType: string, state: DriverInstallProgressTargetState): boolean {
if (state.installing === dbType) return true;
if (!state.upgradingAll) return false;