mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-29 16:18:55 +00:00
feat(drivers): show update count badge
This commit is contained in:
+22
-107
@@ -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,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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user