diff --git a/Cargo.lock b/Cargo.lock
index 7bf0316379..c2320b812f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1906,6 +1906,7 @@ dependencies = [
"log",
"mongodb",
"mysql_async",
+ "pageant",
"percent-encoding",
"portpicker",
"rayon",
diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue
index 36c0c086a4..dc3099066b 100644
--- a/apps/desktop/src/App.vue
+++ b/apps/desktop/src/App.vue
@@ -121,6 +121,8 @@ const newQueryContextSource = ref<"tab" | "sidebar">("tab");
const showSaveSqlDialog = ref(false);
const saveSqlName = ref("");
const saveSqlFolderId = ref("");
+const pendingSaveAndCloseTabId = ref(null);
+const pendingPrevActiveTabId = ref(null);
const ROOT_SAVED_SQL_FOLDER = "__root__";
const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore.activeTabId));
@@ -185,12 +187,15 @@ async function resolveActiveExecutableSql() {
: "";
}
+const blockDangerousRedisCommands = ref(true);
+
const { dangerSql, pendingDangerSql, showDangerDialog, suppressDangerConfirm, tryExecute, doExecute, cancelActiveExecution, tryExplain, onDangerConfirm, explainMode } = useSqlExecution({
activeTab,
activeConnection,
executableSql,
resolveExecutableSql: resolveActiveExecutableSql,
activeOutputView,
+ blockDangerousRedisCommands,
});
const dialogs = useDialogSources();
@@ -218,7 +223,19 @@ const connectionStats = computed(() => ({
}));
const recentConnections = computed(() => connectionStore.connections.slice(0, 5));
const savedSqlHistoryItems = computed(() => {
- const folderById = new Map(savedSqlStore.allFolders.map((folder) => [folder.id, folder.name]));
+ const folderById = new Map(savedSqlStore.allFolders.map((folder) => [folder.id, folder]));
+ const folderPath = (folderId?: string): string | undefined => {
+ if (!folderId) return undefined;
+ const parts: string[] = [];
+ const seen = new Set();
+ let folder = folderById.get(folderId);
+ while (folder && !seen.has(folder.id)) {
+ seen.add(folder.id);
+ parts.unshift(folder.name);
+ folder = folder.parentFolderId ? folderById.get(folder.parentFolderId) : undefined;
+ }
+ return parts.join("/");
+ };
return rankSavedSqlHistory(savedSqlStore.allFiles, { limit: 6 }).map((file) => {
const connection = connectionStore.getConfig(file.connectionId);
return {
@@ -226,13 +243,25 @@ const savedSqlHistoryItems = computed(() => {
name: file.name,
connectionName: connection ? connectionRedactedNameLabel(connection) : t("welcome.unknownConnection"),
database: file.database,
- folderName: file.folderId ? folderById.get(file.folderId) : undefined,
+ folderName: folderPath(file.folderId),
openCount: file.openCount ?? 0,
};
});
});
const saveSqlFolders = computed(() => {
- return savedSqlStore.allFolders;
+ const folderById = new Map(savedSqlStore.allFolders.map((folder) => [folder.id, folder]));
+ const pathForFolder = (folderId: string) => {
+ const parts: string[] = [];
+ const seen = new Set();
+ let folder = folderById.get(folderId);
+ while (folder && !seen.has(folder.id)) {
+ seen.add(folder.id);
+ parts.unshift(folder.name);
+ folder = folder.parentFolderId ? folderById.get(folder.parentFolderId) : undefined;
+ }
+ return parts.join(" / ");
+ };
+ return savedSqlStore.allFoldersTreeOrder.map((folder) => ({ ...folder, displayName: pathForFolder(folder.id) || folder.name }));
});
async function applyUiScale(scale: number) {
@@ -363,6 +392,36 @@ function defaultSavedSqlName(title: string) {
return normalized.endsWith(".sql") ? normalized : `${normalized}.sql`;
}
+async function handleSaveTab(tabId: string) {
+ const tab = queryStore.tabs.find((t) => t.id === tabId);
+ if (!tab || !tab.sql.trim()) return;
+ const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined;
+ if (existing) {
+ const updated = await savedSqlStore.saveFile({
+ id: existing.id,
+ connectionId: tab.connectionId,
+ folderId: existing.folderId,
+ name: existing.name,
+ database: tab.database,
+ schema: tab.schema,
+ sql: tab.sql,
+ });
+ queryStore.linkSavedSql(tab.id, updated.id, updated.name);
+ queryStore.markTabClean(tab);
+ toast(t("savedSql.saved"), 2000);
+ queryStore.closeTab(tabId, { force: true });
+ return;
+ }
+ // No existing saved SQL — open save dialog, then close after save
+ const prevActive = queryStore.activeTabId;
+ queryStore.activeTabId = tabId;
+ saveSqlName.value = defaultSavedSqlName(tab.title);
+ saveSqlFolderId.value = ROOT_SAVED_SQL_FOLDER;
+ pendingSaveAndCloseTabId.value = tabId;
+ pendingPrevActiveTabId.value = prevActive;
+ showSaveSqlDialog.value = true;
+}
+
async function openSaveSqlDialog() {
const tab = activeTab.value;
if (!tab || !tab.sql.trim()) return;
@@ -432,7 +491,15 @@ async function confirmSaveSqlToLibrary() {
sql: tab.sql,
});
queryStore.linkSavedSql(tab.id, saved.id, saved.name);
+ queryStore.markTabClean(tab);
showSaveSqlDialog.value = false;
+ if (pendingSaveAndCloseTabId.value) {
+ const closeId = pendingSaveAndCloseTabId.value;
+ pendingSaveAndCloseTabId.value = null;
+ if (pendingPrevActiveTabId.value) queryStore.activeTabId = pendingPrevActiveTabId.value;
+ pendingPrevActiveTabId.value = null;
+ queryStore.closeTab(closeId, { force: true });
+ }
toast(t("savedSql.saved"), 2000);
} catch (e: any) {
toast(t("savedSql.saveFailed", { message: e?.message || String(e) }), 5000);
@@ -1029,7 +1096,7 @@ onUnmounted(() => {
-
+
{
:active-connection="activeConnection"
:executable-sql="executableSql"
:explain-mode="explainMode"
+ :block-dangerous-redis-commands="blockDangerousRedisCommands"
@update:explain-mode="(m: 'explain' | 'autotrace') => (explainMode = m)"
+ @update:block-dangerous-redis-commands="(v: boolean) => (blockDangerousRedisCommands = v)"
@execute="tryExecute()"
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@@ -1190,7 +1259,15 @@ onUnmounted(() => {
-
-
+
diff --git a/apps/desktop/src/components/config/DriverInstallProgressCircle.vue b/apps/desktop/src/components/config/DriverInstallProgressCircle.vue
index 8eced5e8c4..96cd0b8e02 100644
--- a/apps/desktop/src/components/config/DriverInstallProgressCircle.vue
+++ b/apps/desktop/src/components/config/DriverInstallProgressCircle.vue
@@ -12,7 +12,7 @@ const props = defineProps<{
- {{ props.percent ?? "..." }}
+ {{ props.percent ?? 0 }}
diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue
index 388efaafd4..98816a99fa 100644
--- a/apps/desktop/src/components/config/DriverStoreDialog.vue
+++ b/apps/desktop/src/components/config/DriverStoreDialog.vue
@@ -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(null);
const installedJres = computed(() => {
const jreMap = new Map();
@@ -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([]);
+const jdbcMavenBundles = ref([]);
const jdbcDriverSearch = ref("");
const isLoadingJdbcDrivers = ref(false);
const jdbcPluginStatus = ref(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(() => {
+ 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) => {
+
{{
jdbcPluginStatus.compatible
@@ -1007,26 +1112,50 @@ watch(driverStoreTab, (tab) => {
{{ t("settings.jdbcImport") }}
+
+
+
+
+
+
{{ t("common.loading") }}
-
+
{{ t("settings.jdbcNoDrivers") }}
{{ t("driverStore.noMatchingDrivers") }}
-
+
-
{{ driver.name }}
-
{{ driver.path }}
+
+
{{ item.title }}
+
+ {{ item.source }}
+
+
+
{{ item.subtitle }}
-
{{ formatBytes(driver.size) }}
-
diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue
index 9d5feafb17..e95a85ed94 100644
--- a/apps/desktop/src/components/connection/ConnectionDialog.vue
+++ b/apps/desktop/src/components/connection/ConnectionDialog.vue
@@ -4,13 +4,15 @@ import { uuid } from "@/lib/utils";
import { useI18n } from "vue-i18n";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
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 { Switch } from "@/components/ui/switch";
+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 +35,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,8 +253,10 @@ const customDriverName = ref("");
const mongoUseUrl = ref(false);
const jdbcDriverPathsInput = ref("");
const jdbcDrivers = ref
([]);
+const jdbcMavenBundles = ref([]);
const agentDrivers = ref([]);
const selectedJdbcDriverPath = ref("");
+const jdbcManualClasspathOpen = ref(false);
const connectionUrlInput = ref("");
const oceanbaseSubMode = ref<"mysql" | "oracle">("mysql");
const h2ConnectionMode = ref("file");
@@ -270,6 +279,31 @@ const isPresetColor = (color: string | undefined) => colorOptions.some((c) => c.
const customColorInput = ref("");
const customColorOpen = ref(false);
+const jdbcDriverSelectItems = computed(() => {
+ 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])));
+const jdbcManualClasspathCount = computed(
+ () =>
+ jdbcDriverPathsInput.value
+ .split(/\r?\n/)
+ .map((value) => value.trim())
+ .filter(Boolean).length,
+);
+
function applyCustomColor(value: string) {
form.value.color = value;
customColorInput.value = value;
@@ -727,7 +761,7 @@ const iconTypeMap: Record = {
custom_postgres: "postgres",
};
-const dbOptions = [
+const dbOptions: DbOption[] = [
{ value: "postgres", label: "PostgreSQL" },
{ value: "mysql", label: "MySQL" },
{ value: "mongodb", label: "MongoDB" },
@@ -787,12 +821,19 @@ const dbOptions = [
{ value: "influxdb", label: "InfluxDB" },
{ value: "iris", label: "IRIS" },
{ value: "jdbc", label: "JDBC" },
- { value: "custom_mysql", label: "Custom (MySQL)" },
- { value: "custom_postgres", label: "Custom (PostgreSQL)" },
];
const dbCategories = computed(() => [{ key: "all", title: "", options: dbOptions }]);
+function matchesDbOption(option: DbOption, keyword: string, categoryTitle = "") {
+ const profile = driverProfiles[option.value];
+ return [option.label, option.value, profile?.label, profile?.type, categoryTitle].some((value) =>
+ String(value || "")
+ .toLowerCase()
+ .includes(keyword),
+ );
+}
+
const filteredDbCategories = computed(() => {
const keyword = dbSearchQuery.value.trim().toLowerCase();
if (!keyword) return dbCategories.value;
@@ -800,14 +841,7 @@ const filteredDbCategories = computed(() => {
return dbCategories.value
.map((category) => ({
...category,
- options: category.options.filter((option) => {
- const profile = driverProfiles[option.value];
- return [option.label, option.value, profile?.label, profile?.type, category.title].some((value) =>
- String(value || "")
- .toLowerCase()
- .includes(keyword),
- );
- }),
+ options: category.options.filter((option) => matchesDbOption(option, keyword, category.title)),
}))
.filter((category) => category.options.length > 0);
});
@@ -1939,9 +1973,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 +1999,21 @@ 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);
+ jdbcManualClasspathOpen.value = false;
}
function openExternalUrl(url: string) {
@@ -2014,7 +2054,7 @@ function openExternalUrl(url: string) {
-
{{ category.title }}
+ {{ category.title }}
@@ -2200,24 +2240,29 @@ function openExternalUrl(url: string) {
-
-
-
-
-