feat(jdbc): add optional JDBC plugin support

This commit is contained in:
t8y2
2026-05-10 12:57:38 +08:00
parent 3a6078e10c
commit 10bfede1c7
37 changed files with 2236 additions and 20 deletions
+193 -10
View File
@@ -6,16 +6,17 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "
import { Button } from "@/components/ui/button";
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 type { ConnectionConfig, DatabaseType } from "@/types/database";
import type { ConnectionConfig, DatabaseType, JdbcDriverInfo } from "@/types/database";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/api";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import { applyParsedConnectionUrl, parseConnectionUrl } from "@/lib/connectionUrl";
import { ArrowLeft, ChevronRight, Copy, FolderOpen, Grid3X3, Link2, List, Search } from "lucide-vue-next";
import { ArrowLeft, ChevronRight, Copy, ExternalLink, FolderOpen, Grid3X3, Link2, List, Search } from "lucide-vue-next";
type DbOption = { value: string; label: string };
type DbCategory = { key: string; title: string; options: DbOption[] };
@@ -68,12 +69,17 @@ const defaultForm = (): Omit<ConnectionConfig, "id"> => ({
ssh_connect_timeout_secs: 5,
ssl: false,
connection_string: undefined,
jdbc_driver_class: undefined,
jdbc_driver_paths: [],
});
const form = ref(defaultForm());
const selectedType = ref("mysql");
const customDriverName = ref("");
const mongoUseUrl = ref(false);
const jdbcDriverPathsInput = ref("");
const jdbcDrivers = ref<JdbcDriverInfo[]>([]);
const selectedJdbcDriverPath = ref("");
const connectionUrlInput = ref("");
const dialogStep = ref<DialogStep>("select");
const dbPickerView = ref<DbPickerView>("icon");
@@ -170,6 +176,7 @@ const driverProfiles: Record<
icon: "cockroachdb",
},
dm: { type: "dameng", port: 5236, user: "SYSDBA", label: "DM (Dameng)", icon: "dm" },
jdbc: { type: "jdbc", port: 0, user: "", label: "JDBC", icon: "jdbc" },
tdengine: { type: "mysql", port: 6030, user: "root", label: "TDengine", icon: "tdengine" },
custom_mysql: {
type: "mysql",
@@ -220,6 +227,13 @@ function applyProfile(val: string, preserveConnectionFields = false) {
if (profile.type === "sqlite" || profile.type === "duckdb") {
form.value.host = "";
}
if (profile.type === "jdbc") {
form.value.host = "";
form.value.connection_string = "";
form.value.jdbc_driver_class = "";
form.value.jdbc_driver_paths = [];
jdbcDriverPathsInput.value = "";
}
}
}
@@ -252,9 +266,12 @@ watch(
ssh_connect_timeout_secs: config.ssh_connect_timeout_secs || 5,
ssl: config.ssl || false,
connection_string: config.connection_string,
jdbc_driver_class: config.jdbc_driver_class,
jdbc_driver_paths: config.jdbc_driver_paths || [],
};
selectedType.value = profile;
mongoUseUrl.value = !!config.connection_string;
jdbcDriverPathsInput.value = (config.jdbc_driver_paths || []).join("\n");
customDriverName.value = isCustomCompatibleProfile() ? config.driver_label || "" : "";
dialogStep.value = "config";
configTab.value = "connection";
@@ -330,6 +347,7 @@ const iconTypeMap: Record<string, string> = {
cockroachdb: "cockroachdb",
tdengine: "tdengine",
dm: "dm",
jdbc: "jdbc",
custom_mysql: "mysql",
custom_postgres: "postgres",
};
@@ -360,6 +378,7 @@ const dbOptions = [
{ value: "vastbase", label: "Vastbase" },
{ value: "redshift", label: "Redshift" },
{ value: "cockroachdb", label: "CockroachDB" },
{ value: "jdbc", label: "JDBC" },
{ value: "custom_mysql", label: "Custom (MySQL)" },
{ value: "custom_postgres", label: "Custom (PostgreSQL)" },
];
@@ -387,7 +406,8 @@ const filteredDbCategories = computed<DbCategory[]>(() => {
const hasDbPickerResults = computed(() => filteredDbCategories.value.some((category) => category.options.length > 0));
const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selectedProfile().icon || selectedType.value);
const canUseSsh = computed(() => form.value.db_type !== "sqlite");
const isJdbcConnection = computed(() => form.value.db_type === "jdbc");
const canUseSsh = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "jdbc");
const testResultMessage = computed(() => {
if (!testResult.value) return "";
return testResult.value.ok ? t("connection.testSuccess") : testResult.value.message;
@@ -439,6 +459,16 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
if (config.db_type === "mongodb" && !mongoUseUrl.value) {
config.connection_string = undefined;
}
if (config.db_type === "jdbc") {
config.host = "";
config.port = 0;
config.connection_string = config.connection_string?.trim() || "";
config.jdbc_driver_class = config.jdbc_driver_class?.trim() || undefined;
config.jdbc_driver_paths = jdbcDriverPathsInput.value
.split(/\r?\n/)
.map((path) => path.trim())
.filter(Boolean);
}
return config;
}
@@ -477,6 +507,8 @@ function resetForm() {
selectedType.value = "mysql";
customDriverName.value = "";
mongoUseUrl.value = false;
jdbcDriverPathsInput.value = "";
selectedJdbcDriverPath.value = "";
connectionUrlInput.value = "";
dialogStep.value = "select";
dbPickerView.value = "icon";
@@ -493,6 +525,7 @@ watch(open, (value) => {
if (!props.editConfig) {
resetForm();
}
void loadJdbcDrivers();
});
watch(canUseSsh, (value) => {
@@ -513,6 +546,10 @@ async function save() {
} else {
const config = connectionConfigForSubmit(uuid());
await store.addConnection(config);
if (config.db_type === "jdbc") {
open.value = false;
return;
}
open.value = false;
await nextTick();
emit("connectStarted", config.name);
@@ -569,6 +606,58 @@ async function browseDbFilePath() {
}
}
}
async function browseJdbcDriverPaths() {
if (!isTauriRuntime()) return;
const { open } = await import("@tauri-apps/plugin-dialog");
const selected = await open({
title: t("connection.jdbcDriverBrowse"),
multiple: true,
filters: [{ name: "JDBC Driver", extensions: ["jar"] }],
});
if (!selected) return;
const paths = Array.isArray(selected) ? selected : [selected];
const existing = jdbcDriverPathsInput.value
.split(/\r?\n/)
.map((path) => path.trim())
.filter(Boolean);
const merged = Array.from(
new Set([...existing, ...paths.filter((path): path is string => typeof path === "string")]),
);
jdbcDriverPathsInput.value = merged.join("\n");
}
async function loadJdbcDrivers() {
if (!isDesktop) return;
try {
jdbcDrivers.value = await api.listJdbcDrivers();
} catch {
jdbcDrivers.value = [];
}
}
function addJdbcDriverPath(path: string) {
const existing = jdbcDriverPathsInput.value
.split(/\r?\n/)
.map((value) => value.trim())
.filter(Boolean);
jdbcDriverPathsInput.value = Array.from(new Set([...existing, path])).join("\n");
}
function onJdbcDriverSelect(path: any) {
if (typeof path !== "string" || !path) return;
selectedJdbcDriverPath.value = path;
addJdbcDriverPath(path);
}
function openExternalUrl(url: string) {
if (isTauriRuntime()) {
import("@tauri-apps/plugin-shell").then(({ open }) => open(url));
} else {
window.open(url, "_blank", "noopener,noreferrer");
}
}
</script>
<template>
@@ -689,17 +778,17 @@ async function browseDbFilePath() {
<template v-else>
<div class="space-y-3">
<Tabs v-model="configTab" class="min-h-0">
<div class="flex items-center justify-between border-b pb-2">
<div v-if="canUseSsh" class="flex items-center justify-between border-b pb-2">
<TabsList>
<TabsTrigger value="connection">{{ t("connection.basicTab") }}</TabsTrigger>
<TabsTrigger v-if="canUseSsh" value="ssh">{{ t("connection.sshTunnel") }}</TabsTrigger>
<TabsTrigger value="ssh">{{ t("connection.sshTunnel") }}</TabsTrigger>
</TabsList>
</div>
<TabsContent value="connection" class="m-0">
<div class="grid gap-4 py-4 pr-2 max-h-[65vh] overflow-y-auto">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">URL</Label>
<div v-if="!isJdbcConnection" class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.connectionUrlOptional") }}</Label>
<div class="col-span-3 flex items-center gap-1">
<Input
v-model="connectionUrlInput"
@@ -765,8 +854,90 @@ async function browseDbFilePath() {
</div>
</div>
<!-- JDBC: optional external plugin -->
<template v-if="form.db_type === 'jdbc'">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.jdbcUrl") }}</Label>
<Input
v-model="form.connection_string"
class="col-span-3"
:placeholder="t('connection.jdbcUrlPlaceholder')"
/>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.user") }}</Label>
<Input v-model="form.username" class="col-span-3" placeholder="sa" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.password") }}</Label>
<Input v-model="form.password" type="password" class="col-span-3" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.jdbcDriverClass") }}</Label>
<Input
v-model="form.jdbc_driver_class"
class="col-span-3"
:placeholder="t('connection.jdbcDriverClassPlaceholder')"
/>
</div>
<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"
>
<SelectTrigger>
<SelectValue :placeholder="t('connection.jdbcDriverSelectPlaceholder')" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="driver in jdbcDrivers" :key="driver.path" :value="driver.path">
{{ driver.name }}
</SelectItem>
</SelectContent>
</Select>
<div class="flex items-start gap-1">
<textarea
v-model="jdbcDriverPathsInput"
class="flex min-h-12 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
:placeholder="t('connection.jdbcDriverPathsPlaceholder')"
/>
<Tooltip v-if="isDesktop">
<TooltipTrigger as-child>
<Button
type="button"
variant="outline"
size="icon"
class="h-9 w-9 shrink-0"
@click="browseJdbcDriverPaths"
>
<FolderOpen class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("connection.jdbcDriverBrowse") }}</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<div class="grid grid-cols-4 items-start gap-4">
<span />
<div class="col-span-3 space-y-2">
<p class="text-xs text-muted-foreground">
{{ t("connection.jdbcPluginHint") }}
</p>
<div class="flex flex-wrap gap-2">
<Button type="button" variant="outline" size="sm" @click="openExternalUrl('https://dbxio.com')">
<ExternalLink class="h-3.5 w-3.5" />
{{ t("connection.jdbcDocs") }}
</Button>
</div>
</div>
</div>
</template>
<!-- SQLite / DuckDB: file path only -->
<template v-if="form.db_type === 'sqlite' || form.db_type === 'duckdb'">
<template v-else-if="form.db_type === 'sqlite' || form.db_type === 'duckdb'">
<div class="grid grid-cols-4 items-center gap-4">
<Label class="text-right">{{ t("connection.filePath") }}</Label>
<div class="col-span-3 flex items-center gap-1">
@@ -1074,9 +1245,21 @@ async function browseDbFilePath() {
<Button
class="shrink-0"
@click="save"
:disabled="isSaving || !form.name || (!form.host && !(mongoUseUrl && form.connection_string))"
:disabled="
isSaving ||
!form.name ||
(!form.host &&
!(mongoUseUrl && form.connection_string) &&
!(form.db_type === 'jdbc' && form.connection_string))
"
>
{{ isSaving ? t("common.loading") : editingId ? t("connection.save") : t("connection.saveAndConnect") }}
{{
isSaving
? t("common.loading")
: editingId || isJdbcConnection
? t("connection.save")
: t("connection.saveAndConnect")
}}
</Button>
</DialogFooter>
</template>
+209 -3
View File
@@ -2,7 +2,7 @@
import { ref, watch, shallowRef, computed } from "vue";
import type { EditorView as EditorViewType } from "@codemirror/view";
import { useI18n } from "vue-i18n";
import { Settings } from "lucide-vue-next";
import { FolderOpen, Settings, Trash2 } from "lucide-vue-next";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
@@ -13,9 +13,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useSettingsStore, EDITOR_THEMES, FONT_FAMILIES, DEFAULT_EDITOR_SETTINGS } from "@/stores/settingsStore";
import { loadEditorTheme, editorFontTheme } from "@/lib/editorThemes";
import { isTauriRuntime } from "@/lib/tauriRuntime";
import type { JdbcDriverInfo, JdbcPluginStatus } from "@/types/database";
import * as api from "@/lib/api";
import { useToast } from "@/composables/useToast";
const { t } = useI18n();
const settingsStore = useSettingsStore();
const { toast } = useToast();
const props = defineProps<{
open: boolean;
@@ -98,6 +102,12 @@ function setAppLayout(value: "separated" | "classic") {
const activeSettingsTab = ref("editor");
const isWeb = !isTauriRuntime();
const jdbcDrivers = ref<JdbcDriverInfo[]>([]);
const isLoadingJdbcDrivers = ref(false);
const jdbcPluginStatus = ref<JdbcPluginStatus | null>(null);
const isInstallingJdbcPlugin = ref(false);
const isUninstallingJdbcPlugin = ref(false);
const jdbcDriverPathInput = ref("");
watch(
() => props.open,
@@ -108,6 +118,8 @@ watch(
oldPassword.value = "";
newPassword.value = "";
confirmNewPassword.value = "";
void loadJdbcDrivers();
void loadJdbcPluginStatus();
}
},
);
@@ -153,6 +165,105 @@ async function changePassword() {
}
}
function formatBytes(bytes: number) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
async function loadJdbcDrivers() {
if (isWeb) return;
isLoadingJdbcDrivers.value = true;
try {
jdbcDrivers.value = await api.listJdbcDrivers();
} catch (e: any) {
toast(String(e?.message || e), 5000);
} finally {
isLoadingJdbcDrivers.value = false;
}
}
async function loadJdbcPluginStatus() {
if (isWeb) return;
try {
jdbcPluginStatus.value = await api.jdbcPluginStatus();
} catch (e: any) {
toast(String(e?.message || e), 5000);
}
}
async function installJdbcPlugin() {
if (isWeb || isInstallingJdbcPlugin.value) return;
isInstallingJdbcPlugin.value = true;
try {
jdbcPluginStatus.value = await api.installJdbcPlugin();
toast(t("settings.jdbcPluginInstallSuccess"));
await loadJdbcDrivers();
} catch (e: any) {
toast(String(e?.message || e), 5000);
} finally {
isInstallingJdbcPlugin.value = false;
}
}
async function uninstallJdbcPlugin() {
if (isWeb || isUninstallingJdbcPlugin.value) return;
isUninstallingJdbcPlugin.value = true;
try {
jdbcPluginStatus.value = await api.uninstallJdbcPlugin();
toast(t("settings.jdbcPluginUninstallSuccess"));
await loadJdbcDrivers();
} catch (e: any) {
toast(String(e?.message || e), 5000);
} finally {
isUninstallingJdbcPlugin.value = false;
}
}
async function importJdbcDriverPaths(paths: string[]) {
if (!paths.length) return;
try {
jdbcDrivers.value = await api.importJdbcDrivers(paths);
jdbcDriverPathInput.value = "";
toast(t("settings.jdbcImportSuccess", { count: paths.length }));
} catch (e: any) {
toast(String(e?.message || e), 5000);
}
}
async function importJdbcDrivers() {
if (isWeb) return;
const { open } = await import("@tauri-apps/plugin-dialog");
const selected = await open({
title: t("settings.jdbcImport"),
multiple: true,
filters: [{ name: "JDBC Driver", extensions: ["jar"] }],
});
if (!selected) return;
const paths = (Array.isArray(selected) ? selected : [selected]).filter(
(path): path is string => typeof path === "string",
);
await importJdbcDriverPaths(paths);
}
async function importJdbcDriverPathInput() {
const paths = jdbcDriverPathInput.value
.split(/\r?\n/)
.map((path) => path.trim())
.filter(Boolean);
await importJdbcDriverPaths(paths);
}
async function deleteJdbcDriver(path: string) {
try {
jdbcDrivers.value = await api.deleteJdbcDriver(path);
toast(t("settings.jdbcDeleteSuccess"));
} catch (e: any) {
toast(String(e?.message || e), 5000);
}
}
// ---------- CodeMirror preview ----------
const previewRef = ref<HTMLDivElement>();
const previewView = shallowRef<EditorViewType | null>(null);
@@ -261,6 +372,7 @@ watch(
<TabsList class="w-full">
<TabsTrigger value="editor" class="flex-1">{{ t("settings.editorTab") }}</TabsTrigger>
<TabsTrigger value="appearance" class="flex-1">{{ t("settings.appearanceTab") }}</TabsTrigger>
<TabsTrigger v-if="!isWeb" value="jdbc" class="flex-1">{{ t("settings.jdbcTab") }}</TabsTrigger>
<TabsTrigger v-if="isWeb" value="security" class="flex-1">{{ t("settings.securityTab") }}</TabsTrigger>
<TabsTrigger value="about" class="flex-1">{{ t("settings.aboutTab") }}</TabsTrigger>
</TabsList>
@@ -382,7 +494,7 @@ watch(
</div>
</div>
<DialogFooter class="border-t-0 bg-transparent gap-2 sm:gap-0">
<DialogFooter class="border-t-0 bg-transparent gap-3 sm:gap-3">
<Button variant="outline" @click="resetDefaults">
{{ t("settings.resetDefaults") }}
</Button>
@@ -427,7 +539,7 @@ watch(
</div>
</div>
<DialogFooter class="border-t-0 bg-transparent gap-2 sm:gap-0">
<DialogFooter class="border-t-0 bg-transparent gap-3 sm:gap-3">
<Button variant="outline" @click="resetDefaults">
{{ t("settings.resetDefaults") }}
</Button>
@@ -441,6 +553,100 @@ watch(
</DialogFooter>
</TabsContent>
<TabsContent v-if="!isWeb" value="jdbc" class="space-y-5 py-2">
<div class="rounded-md border bg-muted/20 p-4">
<div class="flex min-h-12 items-center justify-between gap-3">
<div class="min-w-0 space-y-1">
<Label>{{ t("settings.jdbcPlugin") }}</Label>
<p v-if="!jdbcPluginStatus?.installed" class="text-xs text-muted-foreground">
{{ t("settings.jdbcPluginNotInstalled") }}
</p>
</div>
<div class="flex shrink-0 items-center gap-3">
<span
v-if="jdbcPluginStatus?.installed"
class="text-xs"
:class="jdbcPluginStatus.compatible ? 'text-green-600' : 'text-destructive'"
>
{{
jdbcPluginStatus.compatible
? t("settings.jdbcPluginInstalled", {
version: jdbcPluginStatus.version || "-",
})
: t("settings.jdbcPluginIncompatible")
}}
</span>
<Button
v-if="jdbcPluginStatus?.installed"
type="button"
variant="outline"
:disabled="isUninstallingJdbcPlugin"
@click="uninstallJdbcPlugin"
>
{{ isUninstallingJdbcPlugin ? t("common.loading") : t("settings.jdbcPluginUninstall") }}
</Button>
<Button
v-else
type="button"
variant="default"
:disabled="isInstallingJdbcPlugin"
@click="installJdbcPlugin"
>
{{ isInstallingJdbcPlugin ? t("common.loading") : t("settings.jdbcPluginInstall") }}
</Button>
</div>
</div>
</div>
<div class="space-y-3">
<div class="space-y-1">
<Label>{{ t("settings.jdbcDrivers") }}</Label>
</div>
<div class="flex items-center gap-2">
<Input
v-model="jdbcDriverPathInput"
class="flex-1"
:placeholder="t('settings.jdbcDriverPathPlaceholder')"
@keydown.enter.prevent="importJdbcDriverPathInput"
/>
<Button variant="outline" :disabled="!jdbcDriverPathInput.trim()" @click="importJdbcDriverPathInput">
{{ t("settings.jdbcImportPath") }}
</Button>
<Button class="shrink-0" @click="importJdbcDrivers">
<FolderOpen class="h-4 w-4" />
{{ t("settings.jdbcImport") }}
</Button>
</div>
</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">
{{ t("settings.jdbcNoDrivers") }}
</div>
<div v-else class="divide-y">
<div v-for="driver in jdbcDrivers" :key="driver.path" 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>
<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" @click="deleteJdbcDriver(driver.path)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
</div>
</div>
<DialogFooter class="border-t-0 bg-transparent">
<Button variant="outline" @click="emit('update:open', false)">
{{ t("common.close") }}
</Button>
</DialogFooter>
</TabsContent>
<TabsContent v-if="isWeb" value="security" class="space-y-5 py-2">
<div class="space-y-3">
<Label class="text-base">{{ t("auth.changePassword") }}</Label>
+27
View File
@@ -99,6 +99,7 @@ export default {
driverName: "Driver Name",
driverNamePlaceholder: "Vendor or environment name",
urlParams: "URL Params",
connectionUrlOptional: "URL (optional)",
connectionUrlPlaceholder: "postgresql://user:pass{'@'}host:5432/db?sslmode=require",
parseConnectionUrl: "Parse connection URL",
parseConnectionUrlApplied: "Connection URL applied",
@@ -139,6 +140,16 @@ export default {
sshKeyPathBrowse: "Browse",
sshExposeLan: "Expose tunnel to LAN",
sshConnectTimeout: "SSH Timeout (seconds)",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "Driver Class (optional)",
jdbcDriverClassPlaceholder: "Most drivers auto-register; use com.vendor.jdbc.Driver if needed",
jdbcDriverPaths: "Driver JARs",
jdbcDriverSelectPlaceholder: "Choose imported driver",
jdbcDriverPathsPlaceholder: "/path/to/driver.jar\n/path/to/another-driver.jar",
jdbcDriverBrowse: "Choose JDBC driver JAR",
jdbcDocs: "View JDBC docs",
jdbcPluginHint: "Install the DBX JDBC plugin first, then import the database vendor's JDBC driver JAR.",
dmCompatHint: "Requires DM8 ODBC driver installed on your system.",
dmDownload: "Download from Dameng",
mongoLegacyHint:
@@ -786,6 +797,7 @@ export default {
title: "Settings",
editorTab: "Editor",
appearanceTab: "Appearance",
jdbcTab: "JDBC Drivers",
securityTab: "Security",
aboutTab: "About",
fontFamily: "Font Family",
@@ -806,6 +818,21 @@ export default {
wordWrap: "Word wrap",
wordWrapDescription: "Wrap long SQL lines within the editor width",
preview: "Live Preview",
jdbcPlugin: "DBX JDBC plugin",
jdbcPluginInstall: "Install JDBC plugin",
jdbcPluginInstallSuccess: "JDBC plugin installed",
jdbcPluginUninstall: "Uninstall",
jdbcPluginUninstallSuccess: "JDBC plugin uninstalled",
jdbcPluginInstalled: "Installed: v{version}",
jdbcPluginIncompatible: "Incompatible protocol",
jdbcPluginNotInstalled: "Not installed",
jdbcDrivers: "JDBC driver JARs",
jdbcImport: "Import JAR",
jdbcImportPath: "Import Path",
jdbcDriverPathPlaceholder: "/path/to/driver.jar",
jdbcImportSuccess: "Imported {count} driver file(s)",
jdbcDeleteSuccess: "Driver removed",
jdbcNoDrivers: "No JDBC drivers imported yet.",
aboutDescription: "An open-source, lightweight database management tool.",
community: "Community",
qqGroup: "QQ Group",
+27
View File
@@ -98,6 +98,7 @@ export default {
driverName: "驱动名称",
driverNamePlaceholder: "厂商或环境名称",
urlParams: "URL 参数",
connectionUrlOptional: "URL(可选)",
connectionUrlPlaceholder: "postgresql://user:pass{'@'}host:5432/db?sslmode=require",
parseConnectionUrl: "解析连接 URL",
parseConnectionUrlApplied: "已应用连接 URL",
@@ -138,6 +139,16 @@ export default {
sshKeyPathBrowse: "浏览",
sshExposeLan: "允许局域网访问隧道",
sshConnectTimeout: "SSH 超时时间(秒)",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "驱动类(可选)",
jdbcDriverClassPlaceholder: "多数驱动可自动识别;需要时填写 com.vendor.jdbc.Driver",
jdbcDriverPaths: "驱动 JAR",
jdbcDriverSelectPlaceholder: "选择已导入驱动",
jdbcDriverPathsPlaceholder: "/path/to/driver.jar\n/path/to/another-driver.jar",
jdbcDriverBrowse: "选择 JDBC 驱动 JAR",
jdbcDocs: "查看 JDBC 文档",
jdbcPluginHint: "先安装 DBX JDBC 插件,再导入数据库厂商提供的 JDBC 驱动 JAR。",
dmCompatHint: "需要在系统上安装达梦 DM8 ODBC 驱动程序。",
dmDownload: "前往达梦官网下载",
mongoLegacyHint:
@@ -773,6 +784,7 @@ export default {
title: "设置",
editorTab: "编辑器",
appearanceTab: "外观",
jdbcTab: "JDBC 驱动",
securityTab: "安全",
aboutTab: "关于我们",
fontFamily: "字体",
@@ -793,6 +805,21 @@ export default {
wordWrap: "自动换行",
wordWrapDescription: "长 SQL 在编辑器宽度内自动折行显示",
preview: "实时预览",
jdbcPlugin: "DBX JDBC 插件",
jdbcPluginInstall: "安装 JDBC 插件",
jdbcPluginInstallSuccess: "JDBC 插件已安装",
jdbcPluginUninstall: "卸载",
jdbcPluginUninstallSuccess: "JDBC 插件已卸载",
jdbcPluginInstalled: "已安装:v{version}",
jdbcPluginIncompatible: "协议不兼容",
jdbcPluginNotInstalled: "未安装",
jdbcDrivers: "JDBC 驱动 JAR",
jdbcImport: "导入 JAR",
jdbcImportPath: "导入路径",
jdbcDriverPathPlaceholder: "/path/to/driver.jar",
jdbcImportSuccess: "已导入 {count} 个驱动文件",
jdbcDeleteSuccess: "驱动已删除",
jdbcNoDrivers: "还没有导入 JDBC 驱动。",
aboutDescription: "开源、轻量的数据库管理工具。",
community: "社区",
qqGroup: "QQ 群",
+7
View File
@@ -36,6 +36,13 @@ export const connectDb = forward("connectDb");
export const disconnectDb = forward("disconnectDb");
export const saveConnections = forward("saveConnections");
export const loadConnections = forward("loadConnections");
export const listPlugins = forward("listPlugins");
export const listJdbcDrivers = forward("listJdbcDrivers");
export const importJdbcDrivers = forward("importJdbcDrivers");
export const deleteJdbcDriver = forward("deleteJdbcDriver");
export const jdbcPluginStatus = forward("jdbcPluginStatus");
export const installJdbcPlugin = forward("installJdbcPlugin");
export const uninstallJdbcPlugin = forward("uninstallJdbcPlugin");
export const loadSavedSqlLibrary = forward("loadSavedSqlLibrary");
export const saveSavedSqlFolder = forward("saveSavedSqlFolder");
export const deleteSavedSqlFolder = forward("deleteSavedSqlFolder");
+2 -1
View File
@@ -7,6 +7,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
"redshift",
"dameng",
"gaussdb",
"jdbc",
]);
export const SQL_FILE_UNSUPPORTED_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch"]);
@@ -78,7 +79,7 @@ export const FIELD_LINEAGE_SUPPORTED_TYPES = new Set<DatabaseType>([
export const FETCH_FIRST_TYPES = new Set<DatabaseType>(["oracle", "dameng"]);
export const TREE_SCHEMA_TYPES = new Set<DatabaseType>(["postgres", "redshift", "sqlserver", "gaussdb"]);
export const TREE_SCHEMA_TYPES = new Set<DatabaseType>(["postgres", "redshift", "sqlserver", "gaussdb", "jdbc"]);
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb"]);
+31
View File
@@ -7,6 +7,9 @@ import type {
ForeignKeyInfo,
TriggerInfo,
QueryResult,
InstalledPlugin,
JdbcDriverInfo,
JdbcPluginStatus,
SidebarLayout,
SavedSqlFile,
SavedSqlFolder,
@@ -91,6 +94,34 @@ export async function loadConnections(): Promise<ConnectionConfig[]> {
return get("/api/connection/list");
}
export async function listPlugins(): Promise<InstalledPlugin[]> {
return get("/api/plugins");
}
export async function listJdbcDrivers(): Promise<JdbcDriverInfo[]> {
return [];
}
export async function importJdbcDrivers(_paths: string[]): Promise<JdbcDriverInfo[]> {
return [];
}
export async function deleteJdbcDriver(_path: string): Promise<JdbcDriverInfo[]> {
return [];
}
export async function jdbcPluginStatus(): Promise<JdbcPluginStatus> {
return { installed: false, version: null, protocol_version: null, compatible: true, path: "" };
}
export async function installJdbcPlugin(): Promise<JdbcPluginStatus> {
return { installed: false, version: null, protocol_version: null, compatible: true, path: "" };
}
export async function uninstallJdbcPlugin(): Promise<JdbcPluginStatus> {
return { installed: false, version: null, protocol_version: null, compatible: true, path: "" };
}
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
return get("/api/saved-sql");
}
+31
View File
@@ -9,6 +9,9 @@ import type {
ForeignKeyInfo,
TriggerInfo,
QueryResult,
InstalledPlugin,
JdbcDriverInfo,
JdbcPluginStatus,
SavedSqlFile,
SavedSqlFolder,
SavedSqlLibrary,
@@ -244,6 +247,34 @@ export async function loadConnections(): Promise<ConnectionConfig[]> {
return invoke("load_connections");
}
export async function listPlugins(): Promise<InstalledPlugin[]> {
return invoke("list_plugins");
}
export async function listJdbcDrivers(): Promise<JdbcDriverInfo[]> {
return invoke("list_jdbc_drivers");
}
export async function importJdbcDrivers(paths: string[]): Promise<JdbcDriverInfo[]> {
return invoke("import_jdbc_drivers", { paths });
}
export async function deleteJdbcDriver(path: string): Promise<JdbcDriverInfo[]> {
return invoke("delete_jdbc_driver", { path });
}
export async function jdbcPluginStatus(): Promise<JdbcPluginStatus> {
return invoke("jdbc_plugin_status");
}
export async function installJdbcPlugin(): Promise<JdbcPluginStatus> {
return invoke("install_jdbc_plugin");
}
export async function uninstallJdbcPlugin(): Promise<JdbcPluginStatus> {
return invoke("uninstall_jdbc_plugin");
}
export async function loadSavedSqlLibrary(): Promise<SavedSqlLibrary> {
return invoke("load_saved_sql_library");
}
+40 -1
View File
@@ -13,7 +13,8 @@ export type DatabaseType =
| "starrocks"
| "redshift"
| "dameng"
| "gaussdb";
| "gaussdb"
| "jdbc";
export interface ConnectionConfig {
id: string;
@@ -40,6 +41,44 @@ export interface ConnectionConfig {
ssl?: boolean;
sysdba?: boolean;
connection_string?: string;
jdbc_driver_class?: string;
jdbc_driver_paths?: string[];
}
export interface PluginDriverManifest {
id: string;
label: string;
kind: string;
database_type?: string;
}
export interface PluginManifest {
id: string;
name: string;
version?: string;
protocol_version?: number;
description?: string;
executable?: string;
drivers: PluginDriverManifest[];
}
export interface InstalledPlugin {
manifest: PluginManifest;
path: string;
}
export interface JdbcDriverInfo {
name: string;
path: string;
size: number;
}
export interface JdbcPluginStatus {
installed: boolean;
version?: string | null;
protocol_version?: number | null;
compatible: boolean;
path: string;
}
export interface DatabaseInfo {