mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(updates): restore driver download progress and batch counter
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user