feat(drivers): show update count badge

This commit is contained in:
t8y2
2026-05-17 17:39:33 +08:00
parent cb9863b6bb
commit a16fed35d4
9 changed files with 90 additions and 236 deletions
+22 -107
View File
@@ -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<typeof setInterval> | 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<AgentDriverUpdatePrompt | null>(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<string> {
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<AgentDriverUpdateChoice> {
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<AgentDriverUpdateState[]>("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<AgentDriverUpdateBadgeState[]>("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(() => {
<div class="h-full flex flex-col min-w-0">
<AppTabBar
:show-driver-store="showDriverStore"
:agent-driver-update-count="agentDriverUpdateCount"
@toggle-driver-store="showDriverStore = true"
@close-driver-store="showDriverStore = false"
/>
<DriverStorePage v-if="showDriverStore" class="flex-1 min-h-0" />
<DriverStorePage
v-if="showDriverStore"
class="flex-1 min-h-0"
@update-count-change="updateAgentDriverUpdateCount"
/>
<div v-else-if="activeTab" class="flex flex-col flex-1 min-h-0">
<EditorToolbar
v-if="activeTab.mode === 'query' && !isPreviewTab(activeTab)"
@@ -959,41 +909,6 @@ onUnmounted(() => {
@download-and-install="downloadAndInstallUpdate"
@restart="restartApp"
/>
<Dialog :open="showAgentDriverUpdateDialog" @update:open="onAgentDriverUpdateOpenChange">
<DialogContent class="sm:max-w-[440px]">
<DialogHeader>
<DialogTitle>驱动有新版本</DialogTitle>
</DialogHeader>
<div v-if="agentDriverUpdatePrompt" class="space-y-3 text-sm text-muted-foreground">
<p>
{{ agentDriverUpdatePrompt.label }} 驱动可从 v{{ agentDriverUpdatePrompt.currentVersion }} 更新到 v{{
agentDriverUpdatePrompt.latestVersion
}}。
</p>
<p>可以先更新驱动再连接,也可以本次直接连接。</p>
</div>
<DialogFooter class="gap-2 sm:gap-2">
<Button
variant="ghost"
:disabled="isInstallingAgentDriverUpdate"
@click="chooseAgentDriverUpdate('ignore')"
>
本版本不再提醒
</Button>
<Button
variant="outline"
:disabled="isInstallingAgentDriverUpdate"
@click="chooseAgentDriverUpdate('skip')"
>
直接连接
</Button>
<Button :disabled="isInstallingAgentDriverUpdate" @click="chooseAgentDriverUpdate('update')">
{{ isInstallingAgentDriverUpdate ? "更新中..." : "更新后连接" }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Transition name="toast">
<div
v-if="toastVisible"
+13 -3
View File
@@ -13,6 +13,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { countAvailableAgentDriverUpdates } from "@/lib/agentDriverUpdateBadge";
import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database";
import * as api from "@/lib/api";
@@ -20,6 +21,10 @@ const { t } = useI18n();
const { toast } = useToast();
const isWeb = !isTauriRuntime();
const emit = defineEmits<{
"update-count-change": [count: number];
}>();
// ──────────── 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<AgentDriverInfo[]>("list_installed_agents");
updateAgentDrivers(await invoke<AgentDriverInfo[]>("list_installed_agents"));
}
async function forceRefresh() {
@@ -363,11 +373,11 @@ async function deleteJdbcDriver(path: string) {
// ──────────── Lifecycle ────────────
onMounted(async () => {
drivers.value = await invoke<AgentDriverInfo[]>("list_installed_agents_local");
updateAgentDrivers(await invoke<AgentDriverInfo[]>("list_installed_agents_local"));
void loadJavaRuntimeConfig();
invoke<AgentDriverInfo[]>("list_installed_agents").then((result) => {
drivers.value = result;
updateAgentDrivers(result);
});
unlisten = await listen<InstallProgress>("agent-install-progress", (event) => {
+8
View File
@@ -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) {
<Package class="h-3.5 w-3.5" />
</span>
<span class="min-w-0 truncate flex-1">驱动管理</span>
<span
v-if="(agentDriverUpdateCount ?? 0) > 0"
class="inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white"
aria-label="可更新驱动数量"
>
{{ (agentDriverUpdateCount ?? 0) > 99 ? "99+" : agentDriverUpdateCount }}
</span>
<button class="rounded hover:bg-muted-foreground/20 p-0.5 shrink-0" @click.stop="emit('close-driver-store')">
<X class="h-3 w-3" />
</button>
+8
View File
@@ -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) {
>
<Package class="h-3.5 w-3.5" />
驱动管理
<span
v-if="agentDriverUpdateCount > 0"
class="ml-0.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-red-500 px-1 text-[10px] font-medium leading-none text-white"
aria-label="可更新驱动数量"
>
{{ agentDriverUpdateCount > 99 ? "99+" : agentDriverUpdateCount }}
</span>
</Button>
<div class="flex-1" data-tauri-drag-region />
+7
View File
@@ -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;
}
-50
View File
@@ -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<ConnectionConfig, "db_type" | "driver_profile">,
): 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<ConnectionConfig, "db_type" | "driver_profile">,
drivers: readonly AgentDriverUpdateState[],
ignoredVersions: ReadonlySet<string> = 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,
};
}