feat(jdbc): install drivers from Maven bundles

This commit is contained in:
t8y2 committed 2026-06-11 21:41:37 +08:00
1 parent f969db571c
commit 70f7b464b7
19 files changed
+1039 -51

No files matched your search

@@ -12,7 +12,7 @@ const props = defineProps<{
<circle class="text-green-600 transition-[stroke-dashoffset] duration-200" cx="16" cy="16" r="13" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" pathLength="100" stroke-dasharray="100" :stroke-dashoffset="100 - (props.percent ?? 0)" />
</svg>
<span class="relative z-10 font-mono text-[10px] font-semibold leading-none text-green-700 tabular-nums">
{{ props.percent ?? "..." }}
{{ props.percent ?? 0 }}
</span>
</span>
</template>
@@ -5,6 +5,7 @@ import { Activity, ExternalLink, Cpu, FolderOpen, MemoryStick, Search, Square, T
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import DriverInstallProgressCircle from "@/components/config/DriverInstallProgressCircle.vue";
@@ -12,7 +13,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import { useToast } from "@/composables/useToast";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { countAvailableDriverUpdates } from "@/lib/agentDriverUpdateBadge";
import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database";
import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
import * as api from "@/lib/api";
import type { AgentDriverInfo, DriverRuntimeInfo, DriverRuntimeSummary, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/api";
import { formatRuntimeBytes, formatRuntimeCpu, formatRuntimeUptime, runtimeHealthClass, runtimeStatusClass, runtimeStatusDotClass } from "@/lib/driverRuntimePresentation";
@@ -63,6 +64,7 @@ const DRIVER_RUNTIME_POLL_MS = 5000;
const OFFLINE_DRIVER_DOWNLOAD_URL = "https://dbxio.com/cn/drivers";
let unlisten: (() => void) | null = null;
const lastProgressPercent = ref<number | null>(null);
const installedJres = computed(() => {
const jreMap = new Map<string, boolean>();
@@ -78,16 +80,28 @@ const progressText = computed(() => {
const p = progress.value;
if (!p) return "";
if (p.step === "jre-extract") return t("driverStore.progressJreExtract");
const label = p.step === "jre" ? t("driverStore.progressDownloadJre") : t("driverStore.progressDownloadDriver");
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);
const prefix = upgradingAll.value && upgradingCurrent.value ? `[${upgradingIndex.value}/${upgradingTotal.value}] ${upgradingCurrent.value} — ` : "";
const prefix = upgradingAll.value && upgradingCurrent.value ? `[${upgradingIndex.value}/${upgradingTotal.value}] ${upgradingCurrent.value} - ` : "";
return `${prefix}${label} ${dl} / ${total} (${pct}%)`;
});
const progressNumber = computed(() => driverInstallProgressPercent(progress.value));
const progressNumber = computed(() => {
const next = driverInstallProgressPercent(progress.value);
if (next !== null) {
lastProgressPercent.value = next;
}
return next ?? lastProgressPercent.value;
});
function resetInstallProgress() {
progress.value = null;
lastProgressPercent.value = null;
}
const updatableCount = computed(() => (props.updateNotificationsEnabled ? drivers.value.filter((d) => d.update_available).length : 0));
const usageSummary = computed(() => {
@@ -231,7 +245,7 @@ async function installDriver(dbType: string) {
async function runDriverInstall(dbType: string) {
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
installing.value = dbType;
progress.value = null;
resetInstallProgress();
try {
const blockers = await api.checkAgentUpdateBlockers([dbType]);
if (blockers.length > 0) {
@@ -245,7 +259,7 @@ async function runDriverInstall(dbType: string) {
toast(t("driverStore.driverInstallFailed", { label, error: e }));
} finally {
installing.value = null;
progress.value = null;
resetInstallProgress();
}
}
@@ -263,7 +277,7 @@ async function runQueuedDriverInstalls() {
async function upgradeAll() {
upgradingAll.value = true;
queuedDriverInstalls.value = [];
progress.value = null;
resetInstallProgress();
try {
const updatableDbTypes = drivers.value.filter((driver) => driver.update_available).map((driver) => driver.db_type);
const blockers = await api.checkAgentUpdateBlockers(updatableDbTypes);
@@ -286,7 +300,7 @@ async function upgradeAll() {
upgradingCurrent.value = "";
upgradingIndex.value = 0;
upgradingTotal.value = 0;
progress.value = null;
resetInstallProgress();
}
}
@@ -357,7 +371,7 @@ async function importOfflineZip() {
}
if (!selected) return;
importingZip.value = true;
progress.value = null;
resetInstallProgress();
try {
const count = await api.importAgentsFromZip(selected);
await refreshAgents();
@@ -366,7 +380,7 @@ async function importOfflineZip() {
toast(t("driverStore.offlineImportFailed", { error: e }));
} finally {
importingZip.value = false;
progress.value = null;
resetInstallProgress();
}
}
@@ -402,7 +416,7 @@ async function importDriverJar(dbType: string) {
async function reinstallJre(jreKey: string) {
reinstallingJre.value = jreKey;
progress.value = null;
resetInstallProgress();
try {
await api.reinstallJre(jreKey);
await refreshAgents();
@@ -411,7 +425,7 @@ async function reinstallJre(jreKey: string) {
toast(t("driverStore.jreReinstallFailed", { jre: jreKey, error: e }));
} finally {
reinstallingJre.value = null;
progress.value = null;
resetInstallProgress();
}
}
@@ -434,12 +448,45 @@ function formatSize(bytes: number): string {
// ──────────── JDBC drivers ────────────
const jdbcDrivers = ref<JdbcDriverInfo[]>([]);
const jdbcMavenBundles = ref<JdbcMavenBundleInfo[]>([]);
const jdbcDriverSearch = ref("");
const isLoadingJdbcDrivers = ref(false);
const jdbcPluginStatus = ref<JdbcPluginStatus | null>(null);
const isInstallingJdbcPlugin = ref(false);
const isUninstallingJdbcPlugin = ref(false);
const jdbcDriverPathInput = ref("");
const jdbcMavenCoordinateInput = ref("");
const jdbcMavenRepository = ref("https://repo.maven.apache.org/maven2/");
const customJdbcMavenRepository = ref("");
const isInstallingJdbcMavenDriver = ref(false);
const jdbcMavenRepositoryOptions = [
{ label: "Maven Central", value: "https://repo.maven.apache.org/maven2/" },
{ label: "Aliyun", value: "https://maven.aliyun.com/repository/public" },
{ label: "Huawei Cloud", value: "https://repo.huaweicloud.com/repository/maven" },
{ label: "Tencent Cloud", value: "https://mirrors.cloud.tencent.com/nexus/repository/maven-public" },
{ label: "Custom", value: "custom" },
];
type JdbcDriverListItem =
| {
kind: "manual";
id: string;
title: string;
subtitle: string;
source: string;
size: number;
driver: JdbcDriverInfo;
}
| {
kind: "maven";
id: string;
title: string;
subtitle: string;
source: string;
size: number;
bundle: JdbcMavenBundleInfo;
};
const filteredAgentDrivers = computed(() => {
const query = agentDriverSearch.value.trim().toLowerCase();
@@ -447,10 +494,34 @@ const filteredAgentDrivers = computed(() => {
return drivers.value.filter((driver) => [driver.label, driver.db_type, driver.version, driver.installed_version, driver.jre].filter(Boolean).join(" ").toLowerCase().includes(query));
});
const jdbcDriverListItems = computed<JdbcDriverListItem[]>(() => {
const bundleItems = jdbcMavenBundles.value.map((bundle) => ({
kind: "maven" as const,
id: `maven:${bundle.id}`,
title: bundle.coordinate,
subtitle: `${bundle.artifacts.length} JARs - ${bundle.repositories.join(", ")}`,
source: t("driverStore.jdbcSourceMaven"),
size: bundle.artifacts.reduce((total, artifact) => total + Number(artifact.size || 0), 0),
bundle,
}));
const manualItems = jdbcDrivers.value
.filter((driver) => !driver.bundle_id)
.map((driver) => ({
kind: "manual" as const,
id: `manual:${driver.path}`,
title: driver.name,
subtitle: driver.path,
source: t("driverStore.jdbcSourceManual"),
size: driver.size,
driver,
}));
return [...bundleItems, ...manualItems].sort((a, b) => a.title.localeCompare(b.title));
});
const filteredJdbcDrivers = computed(() => {
const query = jdbcDriverSearch.value.trim().toLowerCase();
if (!query) return jdbcDrivers.value;
return jdbcDrivers.value.filter((driver) => [driver.name, driver.path, String(driver.size)].join(" ").toLowerCase().includes(query));
if (!query) return jdbcDriverListItems.value;
return jdbcDriverListItems.value.filter((item) => [item.title, item.subtitle, String(item.size)].join(" ").toLowerCase().includes(query));
});
function formatBytes(bytes: number) {
@@ -568,7 +639,9 @@ function jreUsageLabel(key: string) {
async function loadJdbcDrivers() {
isLoadingJdbcDrivers.value = true;
try {
jdbcDrivers.value = await api.listJdbcDrivers();
const [drivers, bundles] = await Promise.all([api.listJdbcDrivers(), api.listJdbcMavenBundles()]);
jdbcDrivers.value = drivers;
jdbcMavenBundles.value = bundles;
} catch (e: any) {
toast(String(e?.message || e), 5000);
} finally {
@@ -597,6 +670,7 @@ async function loadJdbcPluginStatus() {
async function installJdbcPlugin() {
if (isInstallingJdbcPlugin.value) return;
isInstallingJdbcPlugin.value = true;
resetInstallProgress();
try {
jdbcPluginStatus.value = await api.installJdbcPlugin();
emitDriverUpdateCount();
@@ -606,6 +680,7 @@ async function installJdbcPlugin() {
toast(String(e?.message || e), 5000);
} finally {
isInstallingJdbcPlugin.value = false;
resetInstallProgress();
}
}
@@ -697,6 +772,24 @@ async function importJdbcDriverPathInput() {
await importJdbcDriverPaths(paths);
}
async function installJdbcMavenDriver() {
const coordinate = jdbcMavenCoordinateInput.value.trim();
const repository = jdbcMavenRepository.value === "custom" ? customJdbcMavenRepository.value.trim() : jdbcMavenRepository.value;
if (!coordinate || !repository || isInstallingJdbcMavenDriver.value) return;
isInstallingJdbcMavenDriver.value = true;
try {
jdbcDrivers.value = await api.installJdbcDriverFromMaven(coordinate, [repository]);
jdbcMavenBundles.value = await api.listJdbcMavenBundles();
jdbcMavenCoordinateInput.value = "";
void loadDriverStoreUsage();
toast(t("driverStore.jdbcMavenInstallSuccess"));
} catch (e: any) {
toast(String(e?.message || e), 8000);
} finally {
isInstallingJdbcMavenDriver.value = false;
}
}
async function deleteJdbcDriver(path: string) {
try {
jdbcDrivers.value = await api.deleteJdbcDriver(path);
@@ -707,6 +800,17 @@ async function deleteJdbcDriver(path: string) {
}
}
async function deleteJdbcMavenBundle(bundleId: string) {
try {
jdbcDrivers.value = await api.deleteJdbcMavenBundle(bundleId);
jdbcMavenBundles.value = await api.listJdbcMavenBundles();
void loadDriverStoreUsage();
toast(t("settings.jdbcDeleteSuccess"));
} catch (e: any) {
toast(String(e?.message || e), 5000);
}
}
// ──────────── Lifecycle ────────────
onMounted(async () => {
@@ -961,6 +1065,7 @@ watch(driverStoreTab, (tab) => {
</p>
</div>
<div class="flex shrink-0 items-center gap-3">
<DriverInstallProgressCircle v-if="isInstallingJdbcPlugin" :percent="progressNumber" :title="progressTitle(t('driverStore.progressDownloadJdbcPlugin'))" />
<span v-if="jdbcPluginStatus?.installed" class="text-xs" :class="jdbcPluginStatus.compatible ? 'text-green-600' : 'text-destructive'">
{{
jdbcPluginStatus.compatible
@@ -1007,26 +1112,50 @@ watch(driverStoreTab, (tab) => {
{{ t("settings.jdbcImport") }}
</Button>
</div>
<div class="grid gap-2 md:grid-cols-[minmax(0,1fr)_180px_auto]">
<Input v-model="jdbcMavenCoordinateInput" class="h-8 text-xs" :placeholder="t('driverStore.jdbcMavenCoordinatePlaceholder')" @keydown.enter.prevent="installJdbcMavenDriver" />
<Select v-model="jdbcMavenRepository">
<SelectTrigger class="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="repo in jdbcMavenRepositoryOptions" :key="repo.value" :value="repo.value">
{{ repo.label }}
</SelectItem>
</SelectContent>
</Select>
<Button class="h-8 shrink-0 rounded-full" :disabled="!jdbcMavenCoordinateInput.trim() || isInstallingJdbcMavenDriver || (jdbcMavenRepository === 'custom' && !customJdbcMavenRepository.trim())" @click="installJdbcMavenDriver">
<Loader2 v-if="isInstallingJdbcMavenDriver" class="h-4 w-4 animate-spin" />
<Download v-else class="h-4 w-4" />
{{ t("driverStore.jdbcMavenInstall") }}
</Button>
</div>
<Input v-if="jdbcMavenRepository === 'custom'" v-model="customJdbcMavenRepository" class="h-8 text-xs" placeholder="https://repo.example.com/repository/maven-public" @keydown.enter.prevent="installJdbcMavenDriver" />
</div>
<div class="rounded-md border">
<div v-if="isLoadingJdbcDrivers" class="p-4 text-sm text-muted-foreground">
{{ t("common.loading") }}
</div>
<div v-else-if="jdbcDrivers.length === 0" class="p-4 text-sm text-muted-foreground">
<div v-else-if="jdbcDriverListItems.length === 0" class="p-4 text-sm text-muted-foreground">
{{ t("settings.jdbcNoDrivers") }}
</div>
<div v-else-if="filteredJdbcDrivers.length === 0" class="p-4 text-sm text-muted-foreground">
{{ t("driverStore.noMatchingDrivers") }}
</div>
<div v-else class="divide-y">
<div v-for="driver in filteredJdbcDrivers" :key="driver.path" class="flex items-center gap-3 p-3">
<div v-for="item in filteredJdbcDrivers" :key="item.id" class="flex items-center gap-3 p-3">
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium">{{ driver.name }}</div>
<div class="truncate text-xs text-muted-foreground">{{ driver.path }}</div>
<div class="flex min-w-0 items-center gap-2">
<div class="truncate text-sm font-medium">{{ item.title }}</div>
<Badge variant="outline" class="h-5 shrink-0 rounded-full px-2 text-[10px] font-medium">
{{ item.source }}
</Badge>
</div>
<div class="truncate text-xs text-muted-foreground">{{ item.subtitle }}</div>
</div>
<div class="shrink-0 text-xs text-muted-foreground">{{ formatBytes(driver.size) }}</div>
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0 rounded-full" @click="deleteJdbcDriver(driver.path)">
<div class="shrink-0 text-xs text-muted-foreground">{{ formatBytes(item.size) }}</div>
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0 rounded-full" @click="item.kind === 'maven' ? deleteJdbcMavenBundle(item.bundle.id) : deleteJdbcDriver(item.driver.path)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
@@ -10,7 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import type { ConnectionConfig, DatabaseType, JdbcDriverInfo, ProxyTunnelConfig, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
import type { ConnectionConfig, DatabaseType, JdbcDriverInfo, JdbcMavenBundleInfo, ProxyTunnelConfig, SshTunnelConfig, TransportLayerConfig } from "@/types/database";
import { useConnectionStore } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { useToast } from "@/composables/useToast";
@@ -33,6 +33,11 @@ type DbCategory = { key: string; title: string; options: DbOption[] };
type DialogStep = "select" | "config";
type DbPickerView = "icon" | "list";
type ConfigTab = "connection" | "advanced" | "tls" | "transport";
type JdbcDriverSelectItem = {
id: string;
label: string;
paths: string[];
};
type LegacyTransportFields = {
ssh_enabled?: boolean;
@@ -246,6 +251,7 @@ const customDriverName = ref("");
const mongoUseUrl = ref(false);
const jdbcDriverPathsInput = ref("");
const jdbcDrivers = ref<JdbcDriverInfo[]>([]);
const jdbcMavenBundles = ref<JdbcMavenBundleInfo[]>([]);
const agentDrivers = ref<AgentDriverInstallState[]>([]);
const selectedJdbcDriverPath = ref("");
const connectionUrlInput = ref("");
@@ -270,6 +276,24 @@ const isPresetColor = (color: string | undefined) => colorOptions.some((c) => c.
const customColorInput = ref("");
const customColorOpen = ref(false);
const jdbcDriverSelectItems = computed<JdbcDriverSelectItem[]>(() => {
const bundles = jdbcMavenBundles.value.map((bundle) => ({
id: `maven:${bundle.id}`,
label: bundle.coordinate,
paths: bundle.artifacts.map((artifact) => artifact.path),
}));
const manual = jdbcDrivers.value
.filter((driver) => !driver.bundle_id)
.map((driver) => ({
id: `manual:${driver.path}`,
label: driver.name,
paths: [driver.path],
}));
return [...bundles, ...manual].sort((left, right) => left.label.localeCompare(right.label));
});
const jdbcDriverSelectItemById = computed(() => new Map(jdbcDriverSelectItems.value.map((item) => [item.id, item])));
function applyCustomColor(value: string) {
form.value.color = value;
customColorInput.value = value;
@@ -1939,9 +1963,12 @@ async function browseJdbcDriverPaths() {
async function loadJdbcDrivers() {
if (!isDesktop) return;
try {
jdbcDrivers.value = await api.listJdbcDrivers();
const [drivers, bundles] = await Promise.all([api.listJdbcDrivers(), api.listJdbcMavenBundles()]);
jdbcDrivers.value = drivers;
jdbcMavenBundles.value = bundles;
} catch {
jdbcDrivers.value = [];
jdbcMavenBundles.value = [];
}
}
@@ -1962,18 +1989,20 @@ async function loadAgentDrivers() {
}
}
function addJdbcDriverPath(path: string) {
function addJdbcDriverPaths(paths: string[]) {
const existing = jdbcDriverPathsInput.value
.split(/\r?\n/)
.map((value) => value.trim())
.filter(Boolean);
jdbcDriverPathsInput.value = Array.from(new Set([...existing, path])).join("\n");
jdbcDriverPathsInput.value = Array.from(new Set([...existing, ...paths])).join("\n");
}
function onJdbcDriverSelect(path: any) {
if (typeof path !== "string" || !path) return;
selectedJdbcDriverPath.value = path;
addJdbcDriverPath(path);
function onJdbcDriverSelect(id: any) {
if (typeof id !== "string" || !id) return;
const item = jdbcDriverSelectItemById.value.get(id);
if (!item) return;
selectedJdbcDriverPath.value = id;
addJdbcDriverPaths(item.paths);
}
function openExternalUrl(url: string) {
@@ -2207,13 +2236,13 @@ function openExternalUrl(url: string) {
<div class="grid grid-cols-4 items-start gap-4">
<Label class="text-right mt-2">{{ t("connection.jdbcDriverPaths") }}</Label>
<div class="col-span-3 space-y-2">
<Select v-if="jdbcDrivers.length > 0" :model-value="selectedJdbcDriverPath" @update:model-value="onJdbcDriverSelect">
<Select v-if="jdbcDriverSelectItems.length > 0" :model-value="selectedJdbcDriverPath" @update:model-value="onJdbcDriverSelect">
<SelectTrigger>
<SelectValue :placeholder="t('connection.jdbcDriverSelectPlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="driver in jdbcDrivers" :key="driver.path" :value="driver.path">
{{ driver.name }}
<SelectItem v-for="driver in jdbcDriverSelectItems" :key="driver.id" :value="driver.id">
{{ driver.label }}
</SelectItem>
</SelectContent>
</Select>