fix(jdbc): simplify driver selection and table edits
This commit is contained in:
parent
70f7b464b7
commit
c7fb855000
|
|
@ -4,12 +4,14 @@ 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 { 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";
|
||||
|
|
@ -254,6 +256,7 @@ const jdbcDrivers = ref<JdbcDriverInfo[]>([]);
|
|||
const jdbcMavenBundles = ref<JdbcMavenBundleInfo[]>([]);
|
||||
const agentDrivers = ref<AgentDriverInstallState[]>([]);
|
||||
const selectedJdbcDriverPath = ref("");
|
||||
const jdbcManualClasspathOpen = ref(false);
|
||||
const connectionUrlInput = ref("");
|
||||
const oceanbaseSubMode = ref<"mysql" | "oracle">("mysql");
|
||||
const h2ConnectionMode = ref<H2ConnectionMode>("file");
|
||||
|
|
@ -293,6 +296,13 @@ const jdbcDriverSelectItems = computed<JdbcDriverSelectItem[]>(() => {
|
|||
});
|
||||
|
||||
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;
|
||||
|
|
@ -751,7 +761,7 @@ const iconTypeMap: Record<string, string> = {
|
|||
custom_postgres: "postgres",
|
||||
};
|
||||
|
||||
const dbOptions = [
|
||||
const dbOptions: DbOption[] = [
|
||||
{ value: "postgres", label: "PostgreSQL" },
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "mongodb", label: "MongoDB" },
|
||||
|
|
@ -811,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<DbCategory[]>(() => [{ 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<DbCategory[]>(() => {
|
||||
const keyword = dbSearchQuery.value.trim().toLowerCase();
|
||||
if (!keyword) return dbCategories.value;
|
||||
|
|
@ -824,14 +841,7 @@ const filteredDbCategories = computed<DbCategory[]>(() => {
|
|||
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);
|
||||
});
|
||||
|
|
@ -2003,6 +2013,7 @@ function onJdbcDriverSelect(id: any) {
|
|||
if (!item) return;
|
||||
selectedJdbcDriverPath.value = id;
|
||||
addJdbcDriverPaths(item.paths);
|
||||
jdbcManualClasspathOpen.value = false;
|
||||
}
|
||||
|
||||
function openExternalUrl(url: string) {
|
||||
|
|
@ -2043,7 +2054,7 @@ function openExternalUrl(url: string) {
|
|||
<div class="max-h-[58vh] space-y-5 overflow-y-auto pr-2">
|
||||
<section v-for="category in filteredDbCategories" :key="category.key" class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
<h3 class="text-sm font-medium">{{ category.title }}</h3>
|
||||
<h3 v-if="category.title" class="text-sm font-medium">{{ category.title }}</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="dbPickerView === 'icon'" class="grid grid-cols-2 gap-2 sm:grid-cols-4 lg:grid-cols-5">
|
||||
|
|
@ -2229,10 +2240,6 @@ function openExternalUrl(url: string) {
|
|||
<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">
|
||||
|
|
@ -2246,7 +2253,16 @@ function openExternalUrl(url: string) {
|
|||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="flex items-start gap-1">
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="truncate text-xs font-medium">{{ t("connection.jdbcManualClasspath") }}</div>
|
||||
<Badge variant="outline" class="h-5 shrink-0 rounded-full px-2 text-[10px] font-medium">
|
||||
{{ t("connection.jdbcManualClasspathCount", { count: jdbcManualClasspathCount }) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Switch v-model="jdbcManualClasspathOpen" />
|
||||
</div>
|
||||
<div v-if="jdbcManualClasspathOpen" 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"
|
||||
|
|
@ -2263,6 +2279,10 @@ function openExternalUrl(url: string) {
|
|||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<span />
|
||||
<div class="col-span-3 space-y-2">
|
||||
|
|
|
|||
|
|
@ -179,7 +179,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
const isSaving = ref(false);
|
||||
const saveError = ref("");
|
||||
|
||||
const useTransaction = computed(() => editable.value && supportsDataGridTransaction(databaseType.value) && (!!customSaveHandler?.value || (!!connectionId.value && !!database.value && !!tableMeta.value)));
|
||||
const hasBackendSaveTarget = computed(() => !!connectionId.value && !!tableMeta.value);
|
||||
const useTransaction = computed(() => editable.value && supportsDataGridTransaction(databaseType.value) && (!!customSaveHandler?.value || hasBackendSaveTarget.value));
|
||||
|
||||
if (hasPendingChanges.value && useTransaction.value) {
|
||||
transactionActive.value = true;
|
||||
|
|
@ -648,7 +649,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
}
|
||||
|
||||
async function recordDataGridHistory(statements: string[], rollbackStatements: string[], elapsed: number, historyResult?: { affected_rows?: number; success?: boolean; error?: string }) {
|
||||
if (!connectionId.value || !database.value || !tableMeta.value) return;
|
||||
if (!connectionId.value || !tableMeta.value) return;
|
||||
const connName = connectionStore.getConfig(connectionId.value)?.name || "";
|
||||
const success = historyResult?.success ?? true;
|
||||
const details = {
|
||||
|
|
@ -664,7 +665,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
await historyStore.add({
|
||||
connection_id: connectionId.value,
|
||||
connection_name: connName,
|
||||
database: database.value,
|
||||
database: database.value ?? "",
|
||||
sql: statements.join("\n"),
|
||||
execution_time_ms: elapsed,
|
||||
success,
|
||||
|
|
@ -757,17 +758,17 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
|
|||
rollbackStatements: rollbackStmts,
|
||||
});
|
||||
|
||||
if (useTransaction.value && connectionId.value && database.value) {
|
||||
if (useTransaction.value && hasBackendSaveTarget.value) {
|
||||
try {
|
||||
apiResult = await api.executeInTransaction(connectionId.value, database.value, stmts, preparedSave?.executionSchema);
|
||||
apiResult = await api.executeInTransaction(connectionId.value!, database.value ?? "", stmts, preparedSave?.executionSchema);
|
||||
} catch (e: any) {
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
return;
|
||||
}
|
||||
} else if (connectionId.value && database.value) {
|
||||
} else if (hasBackendSaveTarget.value) {
|
||||
try {
|
||||
apiResult = await api.executeBatch(connectionId.value, database.value, stmts);
|
||||
apiResult = await api.executeBatch(connectionId.value!, database.value ?? "", stmts, preparedSave?.executionSchema);
|
||||
} catch (e: any) {
|
||||
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, e);
|
||||
isSaving.value = false;
|
||||
|
|
|
|||
|
|
@ -317,6 +317,8 @@
|
|||
jdbcDriverClassPlaceholder: "Most drivers auto-register; use com.vendor.jdbc.Driver if needed",
|
||||
jdbcDriverPaths: "Driver JARs",
|
||||
jdbcDriverSelectPlaceholder: "Choose imported driver",
|
||||
jdbcManualClasspath: "Manual classpath",
|
||||
jdbcManualClasspathCount: "{count} paths",
|
||||
jdbcDriverPathsPlaceholder: "/path/to/driver.jar\n/path/to/another-driver.jar",
|
||||
jdbcDriverBrowse: "Choose JDBC driver JAR",
|
||||
jdbcDocs: "View JDBC docs",
|
||||
|
|
|
|||
|
|
@ -318,6 +318,8 @@
|
|||
jdbcDriverClassPlaceholder: "多数驱动可自动识别;需要时填写 com.vendor.jdbc.Driver",
|
||||
jdbcDriverPaths: "驱动 JAR",
|
||||
jdbcDriverSelectPlaceholder: "选择已导入驱动",
|
||||
jdbcManualClasspath: "手动 classpath",
|
||||
jdbcManualClasspathCount: "{count} 个路径",
|
||||
jdbcDriverPathsPlaceholder: "/path/to/driver.jar\n/path/to/another-driver.jar",
|
||||
jdbcDriverBrowse: "选择 JDBC 驱动 JAR",
|
||||
jdbcDocs: "查看 JDBC 文档",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import type { QueryResult, QueryTab } from "@/types/database";
|
||||
import type { ConnectionConfig, QueryResult, QueryTab } from "@/types/database";
|
||||
|
||||
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
||||
|
||||
|
|
@ -19,10 +19,22 @@ export function isConnectionReadonly(connectionId: string): boolean {
|
|||
return connectionStore.getConfig(connectionId)?.read_only ?? false;
|
||||
}
|
||||
|
||||
function jdbcTargetLabel(connection: ConnectionConfig): string {
|
||||
const url = connection.connection_string?.trim() || "";
|
||||
const serviceMatch = url.match(/@\/\/[^/?;]+\/([^?;]+)/);
|
||||
if (serviceMatch?.[1]) return serviceMatch[1];
|
||||
const sidMatch = url.match(/@[^:]+:\d+:([^?;]+)/);
|
||||
if (sidMatch?.[1]) return sidMatch[1];
|
||||
const pathMatch = url.match(/^jdbc:[^:]+:\/\/[^/?;]+\/([^?;]+)/);
|
||||
if (pathMatch?.[1]) return pathMatch[1];
|
||||
return connection.driver_label || "JDBC";
|
||||
}
|
||||
|
||||
export function databaseDisplayNameForTab(connectionId: string, database: string, t: Translate): string {
|
||||
const connectionStore = useConnectionStore();
|
||||
const connection = connectionStore.getConfig(connectionId);
|
||||
if (connection?.db_type === "redis" && database !== "") return `db${database}`;
|
||||
if (connection?.db_type === "jdbc" && !database) return jdbcTargetLabel(connection);
|
||||
return database || t("editor.noDatabase");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { strict as assert } from "node:assert";
|
||||
import { test } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { executionSummaryItems, tabDisplayTitle, tabularResultItems } from "../../apps/desktop/src/lib/tabPresentation.ts";
|
||||
import { databaseDisplayNameForTab, executionSummaryItems, tabDisplayTitle, tabularResultItems } from "../../apps/desktop/src/lib/tabPresentation.ts";
|
||||
import { useConnectionStore } from "../../apps/desktop/src/stores/connectionStore.ts";
|
||||
import type { ConnectionConfig, QueryResult, QueryTab } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
|
|
@ -71,6 +71,39 @@ test("query tab display title uses custom title when present", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("jdbc tabs use the connection target when database is empty", () => {
|
||||
const restoreStorage = installMemoryStorage();
|
||||
setActivePinia(createPinia());
|
||||
useConnectionStore().addEphemeralConnection({
|
||||
...conn("conn-1"),
|
||||
db_type: "jdbc",
|
||||
connection_string: "jdbc:oracle:thin:@172.20.66.143:20001:XE",
|
||||
});
|
||||
const t = (key: string) => (key === "editor.noDatabase" ? "No database selected" : key);
|
||||
|
||||
try {
|
||||
assert.equal(databaseDisplayNameForTab("conn-1", "", t), "XE");
|
||||
assert.equal(
|
||||
tabDisplayTitle(
|
||||
queryTab({
|
||||
database: "",
|
||||
mode: "data",
|
||||
tableMeta: {
|
||||
schema: "SYSTEM",
|
||||
tableName: "DBX_JDBC_TEST",
|
||||
columns: [],
|
||||
primaryKeys: ["ID"],
|
||||
},
|
||||
}),
|
||||
t,
|
||||
),
|
||||
"DBX_JDBC_TEST@XE.SYSTEM",
|
||||
);
|
||||
} finally {
|
||||
restoreStorage();
|
||||
}
|
||||
});
|
||||
|
||||
test("tabular result items hide statement results without returned columns", () => {
|
||||
const results = [result([]), result(["id"]), result([]), result(["name"])];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue