diff --git a/src/App.vue b/src/App.vue index e7fa58c282..5997e9797f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -49,17 +49,11 @@ import { isPreviewTab } from "@/lib/tabPresentation"; import { supportsSqlFileExecution } from "@/lib/databaseCapabilities"; import { classifyAiSqlExecution } from "@/lib/aiSqlExecutionPolicy"; import { buildHistoryAiAnalysisPrompt } from "@/lib/historyAiAnalysis"; -import { - agentDriverUpdateIgnoreKey, - findAgentDriverUpdatePrompt, - type AgentDriverUpdatePrompt, - type AgentDriverUpdateState, -} from "@/lib/agentDriverUpdatePrompt"; +import { countAvailableAgentDriverUpdates, type AgentDriverUpdateBadgeState } from "@/lib/agentDriverUpdateBadge"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import type { ConnectionConfig } from "@/types/database"; import type { HistoryEntry } from "@/lib/tauri"; const { t } = useI18n(); @@ -88,8 +82,6 @@ const { setupFileDrop } = useFileDrop(); const isDesktop = isTauriRuntime(); const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000; -const AGENT_DRIVER_UPDATE_IGNORES_STORAGE_KEY = "dbx-agent-driver-update-ignores"; -type AgentDriverUpdateChoice = "update" | "skip" | "ignore"; let updateCheckTimer: ReturnType | undefined; const needsAuth = ref(!isDesktop); const authenticated = ref(isDesktop); @@ -98,9 +90,7 @@ const setupRequired = ref(false); const showConnectionDialog = ref(false); const showSettingsDialog = ref(false); const showDriverStore = ref(false); -const showAgentDriverUpdateDialog = ref(false); -const agentDriverUpdatePrompt = ref(null); -const isInstallingAgentDriverUpdate = ref(false); +const agentDriverUpdateCount = ref(0); const showHistory = ref(false); const showAiPanel = ref(localStorage.getItem("dbx-ai-panel-open") !== "false"); const { sidebarWidth, aiPanelWidth, historyWidth, startSidebarResize, startAiPanelResize, startHistoryResize } = @@ -125,70 +115,20 @@ const activeConnection = computed(() => { return tab ? connectionStore.getConfig(tab.connectionId) : undefined; }); -let resolveAgentDriverUpdateChoice: ((choice: AgentDriverUpdateChoice) => void) | null = null; - -function loadIgnoredAgentDriverUpdates(): Set { - try { - const value = JSON.parse(localStorage.getItem(AGENT_DRIVER_UPDATE_IGNORES_STORAGE_KEY) || "[]"); - return new Set(Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []); - } catch { - return new Set(); - } +function updateAgentDriverUpdateCount(count: number) { + agentDriverUpdateCount.value = count; } -function saveIgnoredAgentDriverUpdate(prompt: AgentDriverUpdatePrompt) { - const ignored = loadIgnoredAgentDriverUpdates(); - ignored.add(agentDriverUpdateIgnoreKey(prompt.dbType, prompt.latestVersion)); - localStorage.setItem(AGENT_DRIVER_UPDATE_IGNORES_STORAGE_KEY, JSON.stringify([...ignored])); -} - -function requestAgentDriverUpdateChoice(prompt: AgentDriverUpdatePrompt): Promise { - agentDriverUpdatePrompt.value = prompt; - showAgentDriverUpdateDialog.value = true; - return new Promise((resolve) => { - resolveAgentDriverUpdateChoice = resolve; - }); -} - -function chooseAgentDriverUpdate(choice: AgentDriverUpdateChoice) { - showAgentDriverUpdateDialog.value = false; - resolveAgentDriverUpdateChoice?.(choice); - resolveAgentDriverUpdateChoice = null; -} - -function onAgentDriverUpdateOpenChange(value: boolean) { - if (value) { - showAgentDriverUpdateDialog.value = true; - return; - } - if (isInstallingAgentDriverUpdate.value) return; - chooseAgentDriverUpdate("skip"); -} - -async function checkAgentDriverUpdateBeforeConnect(config: ConnectionConfig) { +async function refreshAgentDriverUpdateCount() { if (!isDesktop) return; - const drivers = await invoke("list_installed_agents").catch(() => []); - const prompt = findAgentDriverUpdatePrompt(config, drivers, loadIgnoredAgentDriverUpdates()); - if (!prompt) return; - - const choice = await requestAgentDriverUpdateChoice(prompt); - if (choice === "ignore") { - saveIgnoredAgentDriverUpdate(prompt); - return; - } - if (choice === "skip") return; - - isInstallingAgentDriverUpdate.value = true; try { - await invoke("install_agent", { dbType: prompt.dbType }); - toast(`${prompt.label} 驱动已更新到 v${prompt.latestVersion}`); - } finally { - isInstallingAgentDriverUpdate.value = false; + const drivers = await invoke("list_installed_agents"); + updateAgentDriverUpdateCount(countAvailableAgentDriverUpdates(drivers)); + } catch { + // Driver update availability is only a badge hint; keep the existing count if the registry cannot be reached. } } -connectionStore.setBeforeConnectHandler(checkAgentDriverUpdateBeforeConnect); - function restoreHistorySql(sql: string, entry: HistoryEntry) { const tab = activeTab.value; if (tab?.mode === "query") { @@ -709,9 +649,14 @@ onMounted(async () => { } initApp(); setupFileDrop().catch(() => {}); + void refreshAgentDriverUpdateCount(); setTimeout(() => { checkUpdates({ silent: true }); - updateCheckTimer = setInterval(() => checkUpdates({ silent: true }), UPDATE_CHECK_INTERVAL_MS); + void refreshAgentDriverUpdateCount(); + updateCheckTimer = setInterval(() => { + checkUpdates({ silent: true }); + void refreshAgentDriverUpdateCount(); + }, UPDATE_CHECK_INTERVAL_MS); }, 10_000); api .getAppVersion() @@ -725,7 +670,6 @@ onMounted(async () => { }); onUnmounted(() => { - connectionStore.setBeforeConnectHandler(null); cleanupTauriListeners(); if (updateCheckTimer) { clearInterval(updateCheckTimer); @@ -752,6 +696,7 @@ onUnmounted(() => { :show-driver-store="showDriverStore" :checking-updates="checkingUpdates" :has-update-available="hasUpdateAvailable" + :agent-driver-update-count="agentDriverUpdateCount" :has-connections="connectionStore.connections.length > 0" :has-sql-file-connections="hasSqlFileConnections" @new-connection="showConnectionDialog = true" @@ -795,10 +740,15 @@ onUnmounted(() => {
- +
{ @download-and-install="downloadAndInstallUpdate" @restart="restartApp" /> - - - - 驱动有新版本 - -
-

- {{ agentDriverUpdatePrompt.label }} 驱动可从 v{{ agentDriverUpdatePrompt.currentVersion }} 更新到 v{{ - agentDriverUpdatePrompt.latestVersion - }}。 -

-

可以先更新驱动再连接,也可以本次直接连接。

-
- - - - - -
-
-
(); + // ──────────── Agent drivers ──────────── interface AgentDriverInfo { @@ -98,8 +103,13 @@ const progressPercent = computed(() => { const updatableCount = computed(() => drivers.value.filter((d) => d.update_available).length); +function updateAgentDrivers(nextDrivers: AgentDriverInfo[]) { + drivers.value = nextDrivers; + emit("update-count-change", countAvailableAgentDriverUpdates(nextDrivers)); +} + async function refreshAgents() { - drivers.value = await invoke("list_installed_agents"); + updateAgentDrivers(await invoke("list_installed_agents")); } async function forceRefresh() { @@ -363,11 +373,11 @@ async function deleteJdbcDriver(path: string) { // ──────────── Lifecycle ──────────── onMounted(async () => { - drivers.value = await invoke("list_installed_agents_local"); + updateAgentDrivers(await invoke("list_installed_agents_local")); void loadJavaRuntimeConfig(); invoke("list_installed_agents").then((result) => { - drivers.value = result; + updateAgentDrivers(result); }); unlisten = await listen("agent-install-progress", (event) => { diff --git a/src/components/layout/AppTabBar.vue b/src/components/layout/AppTabBar.vue index 393e93f80b..7bb4cc52b9 100644 --- a/src/components/layout/AppTabBar.vue +++ b/src/components/layout/AppTabBar.vue @@ -19,6 +19,7 @@ import type { QueryTab } from "@/types/database"; const props = defineProps<{ showDriverStore?: boolean; + agentDriverUpdateCount?: number; }>(); const emit = defineEmits<{ @@ -234,6 +235,13 @@ function tabIconClass(tab: QueryTab) { 驱动管理 + + {{ (agentDriverUpdateCount ?? 0) > 99 ? "99+" : agentDriverUpdateCount }} + diff --git a/src/components/layout/AppToolbar.vue b/src/components/layout/AppToolbar.vue index 1cae5bf14a..f6cb145b90 100644 --- a/src/components/layout/AppToolbar.vue +++ b/src/components/layout/AppToolbar.vue @@ -46,6 +46,7 @@ defineProps<{ showDriverStore: boolean; checkingUpdates: boolean; hasUpdateAvailable: boolean; + agentDriverUpdateCount: number; hasConnections: boolean; hasSqlFileConnections: boolean; }>(); @@ -154,6 +155,13 @@ function onToolbarDblClick(e: MouseEvent) { > 驱动管理 + + {{ agentDriverUpdateCount > 99 ? "99+" : agentDriverUpdateCount }} +
diff --git a/src/lib/agentDriverUpdateBadge.ts b/src/lib/agentDriverUpdateBadge.ts new file mode 100644 index 0000000000..401cb58e20 --- /dev/null +++ b/src/lib/agentDriverUpdateBadge.ts @@ -0,0 +1,7 @@ +export interface AgentDriverUpdateBadgeState { + update_available: boolean; +} + +export function countAvailableAgentDriverUpdates(drivers: readonly AgentDriverUpdateBadgeState[]): number { + return drivers.filter((driver) => driver.update_available).length; +} diff --git a/src/lib/agentDriverUpdatePrompt.ts b/src/lib/agentDriverUpdatePrompt.ts deleted file mode 100644 index 19f276d178..0000000000 --- a/src/lib/agentDriverUpdatePrompt.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { ConnectionConfig, DatabaseType } from "@/types/database"; -import { supportsDriverManagement } from "@/lib/databaseCapabilities"; - -export interface AgentDriverUpdateState { - db_type: string; - label: string; - installed: boolean; - installed_version: string | null; - version: string; - update_available: boolean; -} - -export interface AgentDriverUpdatePrompt { - dbType: string; - label: string; - currentVersion: string; - latestVersion: string; -} - -export function agentDriverUpdateKeyForConnection( - connection: Pick, -): string | null { - if (connection.db_type === "oracle" && connection.driver_profile === "oracle-10g") return "oracle-10g"; - if (!supportsDriverManagement(connection.db_type as DatabaseType)) return null; - return connection.db_type; -} - -export function agentDriverUpdateIgnoreKey(dbType: string, version: string): string { - return `${dbType}@${version}`; -} - -export function findAgentDriverUpdatePrompt( - connection: Pick, - drivers: readonly AgentDriverUpdateState[], - ignoredVersions: ReadonlySet = new Set(), -): AgentDriverUpdatePrompt | null { - const dbType = agentDriverUpdateKeyForConnection(connection); - if (!dbType) return null; - - const driver = drivers.find((item) => item.db_type === dbType); - if (!driver?.installed || !driver.update_available || !driver.version || !driver.installed_version) return null; - if (ignoredVersions.has(agentDriverUpdateIgnoreKey(dbType, driver.version))) return null; - - return { - dbType, - label: driver.label || dbType, - currentVersion: driver.installed_version, - latestVersion: driver.version, - }; -} diff --git a/tests/agentDriverUpdateBadge.test.ts b/tests/agentDriverUpdateBadge.test.ts new file mode 100644 index 0000000000..0afce9e91a --- /dev/null +++ b/tests/agentDriverUpdateBadge.test.ts @@ -0,0 +1,20 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { countAvailableAgentDriverUpdates } from "../src/lib/agentDriverUpdateBadge.ts"; + +test("returns zero when there are no available agent driver updates", () => { + assert.equal(countAvailableAgentDriverUpdates([]), 0); + assert.equal(countAvailableAgentDriverUpdates([{ update_available: false }, { update_available: false }]), 0); +}); + +test("counts available agent driver updates", () => { + assert.equal( + countAvailableAgentDriverUpdates([ + { update_available: true }, + { update_available: false }, + { update_available: true }, + ]), + 2, + ); +}); diff --git a/tests/agentDriverUpdatePrompt.test.ts b/tests/agentDriverUpdatePrompt.test.ts deleted file mode 100644 index 010db9c566..0000000000 --- a/tests/agentDriverUpdatePrompt.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { - agentDriverUpdateIgnoreKey, - agentDriverUpdateKeyForConnection, - findAgentDriverUpdatePrompt, -} from "../src/lib/agentDriverUpdatePrompt.ts"; - -const oracleConnection = { - id: "conn", - name: "Oracle", - db_type: "oracle", - driver_profile: undefined, -} as const; - -test("uses the driver profile for versioned agent drivers", () => { - assert.equal(agentDriverUpdateKeyForConnection({ db_type: "oracle", driver_profile: "oracle-10g" }), "oracle-10g"); - assert.equal(agentDriverUpdateKeyForConnection({ db_type: "oracle" }), "oracle"); - assert.equal(agentDriverUpdateKeyForConnection({ db_type: "postgres" }), null); -}); - -test("finds installed agent drivers with available updates", () => { - assert.deepEqual( - findAgentDriverUpdatePrompt(oracleConnection, [ - { - db_type: "oracle", - label: "Oracle", - installed: true, - installed_version: "1.0.0", - version: "1.1.0", - update_available: true, - }, - ]), - { - dbType: "oracle", - label: "Oracle", - currentVersion: "1.0.0", - latestVersion: "1.1.0", - }, - ); -}); - -test("does not prompt for missing, current, or ignored driver updates", () => { - assert.equal( - findAgentDriverUpdatePrompt(oracleConnection, [ - { - db_type: "oracle", - label: "Oracle", - installed: false, - installed_version: null, - version: "1.1.0", - update_available: true, - }, - ]), - null, - ); - - assert.equal( - findAgentDriverUpdatePrompt( - oracleConnection, - [ - { - db_type: "oracle", - label: "Oracle", - installed: true, - installed_version: "1.0.0", - version: "1.1.0", - update_available: true, - }, - ], - new Set([agentDriverUpdateIgnoreKey("oracle", "1.1.0")]), - ), - null, - ); -}); diff --git a/tests/appUpdateBadge.test.ts b/tests/appUpdateBadge.test.ts index 0140c5e878..7826604141 100644 --- a/tests/appUpdateBadge.test.ts +++ b/tests/appUpdateBadge.test.ts @@ -14,7 +14,7 @@ test("app schedules hourly silent update checks and clears the timer", () => { const source = readFileSync("src/App.vue", "utf8"); assert.match(source, /UPDATE_CHECK_INTERVAL_MS\s*=\s*60\s*\*\s*60\s*\*\s*1000/); - assert.match(source, /setInterval\(\(\)\s*=>\s*checkUpdates\(\{\s*silent:\s*true\s*}\)/); + assert.match(source, /setInterval\(\(\)\s*=>\s*{[\s\S]*checkUpdates\(\{\s*silent:\s*true\s*}\)/); assert.match(source, /clearInterval\(updateCheckTimer\)/); }); @@ -23,3 +23,14 @@ test("app passes update availability to the toolbar badge", () => { assert.match(source, /:has-update-available="hasUpdateAvailable"/); }); + +test("driver manager entry can show an update count badge", () => { + const toolbarSource = readFileSync("src/components/layout/AppToolbar.vue", "utf8"); + const tabSource = readFileSync("src/components/layout/AppTabBar.vue", "utf8"); + const appSource = readFileSync("src/App.vue", "utf8"); + + assert.match(toolbarSource, /agentDriverUpdateCount/); + assert.match(toolbarSource, /v-if="agentDriverUpdateCount > 0"/); + assert.match(tabSource, /agentDriverUpdateCount/); + assert.match(appSource, /:agent-driver-update-count="agentDriverUpdateCount"/); +});