feat(oracle): add TNS connections and trigger editing
This commit is contained in:
1 parent
9a5edae219
commit
3b004abb1e
23 files changed
+980
-53
No files matched your search
@@ -29,6 +29,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection";
|
||||
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
|
||||
import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connection/connectionPresentation";
|
||||
import { h2ConnectionModeForConfig, h2FileJdbcUrlWithPath, h2FilePathFromJdbcUrl, isH2SplitJdbcUrl, type H2ConnectionMode } from "@/lib/database/h2Connection";
|
||||
@@ -494,6 +495,7 @@ const selectedJdbcDriverPath = ref("");
|
||||
const jdbcManualClasspathOpen = ref(false);
|
||||
const connectionUrlInput = ref("");
|
||||
const appliedConnectionUrlInput = ref("");
|
||||
const oracleTnsAdminPath = ref("");
|
||||
const oceanbaseSubMode = ref<"mysql" | "oracle">("mysql");
|
||||
const h2ConnectionMode = ref<H2ConnectionMode>("file");
|
||||
const dremioConnectionMode = ref<DremioConnectionMode>("legacy");
|
||||
@@ -1764,6 +1766,7 @@ function applyProfile(val: string, preserveConnectionFields = false) {
|
||||
}
|
||||
|
||||
if (!preserveConnectionFields) {
|
||||
oracleTnsAdminPath.value = "";
|
||||
form.value.port = profile.port;
|
||||
setSqlServerPortExplicit(form.value, false);
|
||||
form.value.username = profile.user;
|
||||
@@ -1910,6 +1913,7 @@ watch(
|
||||
visible_databases: config.visible_databases,
|
||||
visible_schemas: config.visible_schemas,
|
||||
};
|
||||
oracleTnsAdminPath.value = parseOracleTnsConnectionString(config.connection_string)?.tnsAdmin || "";
|
||||
productionProtectionEnabled.value = !!config.is_production || (config.production_databases?.length ?? 0) > 0;
|
||||
connectionUrlInput.value = config.db_type === "h2" && config.connection_string ? config.connection_string : "";
|
||||
appliedConnectionUrlInput.value = connectionUrlInput.value.trim();
|
||||
@@ -1988,12 +1992,14 @@ watch(
|
||||
);
|
||||
|
||||
const databaseLabel = computed(() => {
|
||||
if (form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns") return t("connection.oracleTnsAlias");
|
||||
if (form.value.db_type === "oracle") return t("connection.serviceName");
|
||||
if (form.value.db_type === "influxdb" && influxDbVersion.value === "2") return "Bucket";
|
||||
return t("connection.database");
|
||||
});
|
||||
|
||||
const databasePlaceholder = computed(() => {
|
||||
if (form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns") return t("connection.oracleTnsAliasPlaceholder");
|
||||
if (form.value.db_type === "kingbase") return t("connection.databasePlaceholderRequired");
|
||||
const fallback = defaultDatabaseForProfile();
|
||||
if (!fallback) return t("connection.databasePlaceholder");
|
||||
@@ -2485,7 +2491,7 @@ const zookeeperConnectString = computed({
|
||||
form.value.connection_string = normalizeZooKeeperConnectString(value);
|
||||
},
|
||||
});
|
||||
const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "access" && !isCloudflareD1Connection(form.value) && !isH2FileMode.value);
|
||||
const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "access" && !isCloudflareD1Connection(form.value) && !isH2FileMode.value && !(form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns"));
|
||||
const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile));
|
||||
const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true);
|
||||
const agentDriverFocus = computed<DriverStoreFocus>(() => ({ target: "driver", driver: agentDriverInstallKey(form.value.db_type, form.value.driver_profile) }));
|
||||
@@ -2812,6 +2818,7 @@ function applyConnectionUrlToForm(input: string): boolean {
|
||||
|
||||
const parsed = parseConnectionUrl(input, selectedType.value);
|
||||
form.value = applyParsedConnectionUrl(form.value, parsed);
|
||||
oracleTnsAdminPath.value = parseOracleTnsConnectionString(parsed.connectionString)?.tnsAdmin || "";
|
||||
selectedType.value = parsed.driverProfile;
|
||||
customDriverName.value = isCustomCompatibleProfile() ? parsed.driverLabel : "";
|
||||
mongoUseUrl.value = !!parsed.useMongoUrl;
|
||||
@@ -2964,7 +2971,22 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
normalized.connect_timeout_secs = Number.isFinite(timeout) && timeout > 0 ? timeout : 5;
|
||||
return { type: "ssh", ...normalized };
|
||||
});
|
||||
if (config.db_type === "oracle" && config.oracle_connection_type === "tns" && config.transport_layers.some((layer) => layer.enabled !== false)) {
|
||||
throw new Error(t("connection.oracleTnsTransportUnsupported"));
|
||||
}
|
||||
validateTransportLayers(config);
|
||||
if (config.db_type === "oracle" && config.oracle_connection_type === "tns") {
|
||||
const alias = config.database?.trim() || "";
|
||||
const tnsAdmin = normalizeOracleTnsAdminPath(oracleTnsAdminPath.value);
|
||||
if (!alias) throw new Error(t("connection.oracleTnsAliasRequired"));
|
||||
if (!tnsAdmin) throw new Error(t("connection.oracleTnsAdminRequired"));
|
||||
config.database = alias;
|
||||
config.connection_string = buildOracleTnsConnectionString(alias, tnsAdmin);
|
||||
} else if (config.db_type === "oracle" && parseOracleTnsConnectionString(config.connection_string)) {
|
||||
// Only clear DBX-generated TNS URLs when switching modes; preserve custom
|
||||
// service, SID, and descriptor JDBC strings exactly as before.
|
||||
config.connection_string = undefined;
|
||||
}
|
||||
const connectTimeout = Number(config.connect_timeout_secs);
|
||||
config.connect_timeout_secs = Number.isFinite(connectTimeout) && connectTimeout > 0 ? connectTimeout : 10;
|
||||
const queryTimeout = Number(config.query_timeout_secs);
|
||||
@@ -3774,6 +3796,7 @@ function resetForm() {
|
||||
selectedJdbcDriverPath.value = "";
|
||||
connectionUrlInput.value = "";
|
||||
appliedConnectionUrlInput.value = "";
|
||||
oracleTnsAdminPath.value = "";
|
||||
dialogStep.value = "select";
|
||||
dbSearchQuery.value = "";
|
||||
selectedDbCategory.value = "sql";
|
||||
@@ -3822,6 +3845,7 @@ function applyConnectionDraftToConfig(config: Omit<ConnectionConfig, "id">, draf
|
||||
function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) {
|
||||
applyProfile(draft.driverProfile);
|
||||
form.value = applyConnectionDraftToConfig(form.value, draft);
|
||||
oracleTnsAdminPath.value = parseOracleTnsConnectionString(form.value.connection_string)?.tnsAdmin || "";
|
||||
selectedType.value = draft.driverProfile;
|
||||
if (form.value.db_type === "h2") {
|
||||
h2ConnectionMode.value = h2ConnectionModeForConfig(form.value);
|
||||
@@ -4240,6 +4264,20 @@ async function browseHiveKerberosFile(target: "krb5" | "jaas") {
|
||||
}
|
||||
}
|
||||
|
||||
async function browseOracleTnsNamesFile() {
|
||||
if (!isTauriRuntime()) return;
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const selected = await open({
|
||||
title: t("connection.oracleTnsAdminBrowse"),
|
||||
multiple: false,
|
||||
filters: [{ name: "Oracle TNS names", extensions: ["ora"] }],
|
||||
});
|
||||
if (typeof selected === "string") {
|
||||
oracleTnsAdminPath.value = normalizeOracleTnsAdminPath(selected);
|
||||
resetTestState();
|
||||
}
|
||||
}
|
||||
|
||||
async function browseKafkaKerberosFile(target: "keytab" | "krb5") {
|
||||
if (isTauriRuntime()) {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
@@ -5664,7 +5702,7 @@ function openExternalUrl(url: string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<div v-if="form.db_type !== 'oracle' || form.oracle_connection_type !== 'tns'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ form.db_type === "elasticsearch" && elasticsearchConnectionMode === "kibana" ? t("connection.elasticsearchKibanaHost") : t("connection.host") }}</Label>
|
||||
<Input v-model="form.host" class="col-span-2" />
|
||||
<Input v-model.number="form.port" type="number" class="col-span-1" @input="markSqlServerPortExplicit" />
|
||||
@@ -5700,6 +5738,26 @@ function openExternalUrl(url: string) {
|
||||
<Input v-model="form.database" class="col-span-3" :placeholder="databasePlaceholder" />
|
||||
</div>
|
||||
|
||||
<div v-if="form.db_type === 'oracle' && form.oracle_connection_type === 'tns'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">TNS_ADMIN</Label>
|
||||
<div class="col-span-3 flex items-center gap-1">
|
||||
<Input v-model="oracleTnsAdminPath" class="flex-1" :placeholder="t('connection.oracleTnsAdminPlaceholder')" />
|
||||
<Tooltip v-if="isDesktop">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="browseOracleTnsNamesFile">
|
||||
<FolderOpen class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("connection.oracleTnsAdminBrowse") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.db_type === 'oracle' && form.oracle_connection_type === 'tns'" class="grid grid-cols-4 items-start gap-4">
|
||||
<span />
|
||||
<p class="col-span-3 text-xs text-muted-foreground">{{ t("connection.oracleTnsPathHint") }}</p>
|
||||
</div>
|
||||
|
||||
<template v-if="form.db_type === 'hive'">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.hiveAuthMode") }}</Label>
|
||||
@@ -5772,12 +5830,12 @@ function openExternalUrl(url: string) {
|
||||
|
||||
<div v-if="form.db_type === 'oracle'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelSmallClass">{{ t("connection.mode") }}</Label>
|
||||
<div class="col-span-3 grid h-8 grid-cols-2 overflow-hidden rounded-md border border-input bg-muted/30 p-0.5">
|
||||
<div class="col-span-3 grid h-8 grid-cols-3 overflow-hidden rounded-md border border-input bg-muted/30 p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
class="h-7 rounded-sm px-3 text-sm transition-colors"
|
||||
:class="form.oracle_connection_type !== 'sid' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
||||
:aria-pressed="form.oracle_connection_type !== 'sid'"
|
||||
:class="form.oracle_connection_type === 'service_name' || !form.oracle_connection_type ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
||||
:aria-pressed="form.oracle_connection_type === 'service_name' || !form.oracle_connection_type"
|
||||
@click="form.oracle_connection_type = 'service_name'"
|
||||
>
|
||||
{{ t("connection.serviceNameOnly") }}
|
||||
@@ -5791,6 +5849,15 @@ function openExternalUrl(url: string) {
|
||||
>
|
||||
SID
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="h-7 rounded-sm px-3 text-sm transition-colors"
|
||||
:class="form.oracle_connection_type === 'tns' ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'"
|
||||
:aria-pressed="form.oracle_connection_type === 'tns'"
|
||||
@click="form.oracle_connection_type = 'tns'"
|
||||
>
|
||||
TNS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2090,7 +2090,8 @@ function canDropIndex(index: EditableStructureIndex): boolean {
|
||||
}
|
||||
|
||||
const canEditForeignKeys = computed(() => structureCapabilities.value.foreignKey);
|
||||
const canEditMysqlTriggers = computed(() => structureDialect.value === "mysql");
|
||||
const canEditTriggers = computed(() => structureDialect.value === "mysql" || structureDialect.value === "oracle");
|
||||
const isOracleTriggerEditor = computed(() => structureDialect.value === "oracle");
|
||||
|
||||
function generatedForeignKeyName(column = ""): string {
|
||||
const table = structureIndexTableName() || "table";
|
||||
@@ -2138,14 +2139,14 @@ function canEditForeignKeyDraft(foreignKey: EditableStructureForeignKey): boolea
|
||||
}
|
||||
|
||||
function addTrigger() {
|
||||
if (!canEditMysqlTriggers.value || triggersLoading.value) return;
|
||||
if (!canEditTriggers.value || triggersLoading.value) return;
|
||||
activeTab.value = "triggers";
|
||||
triggers.value.push({
|
||||
id: `new:${uuid()}`,
|
||||
name: "",
|
||||
timing: "BEFORE",
|
||||
timing: isOracleTriggerEditor.value ? "BEFORE EACH ROW" : "BEFORE",
|
||||
event: "INSERT",
|
||||
statement: "BEGIN\n \nEND",
|
||||
statement: isOracleTriggerEditor.value ? "BEGIN\n NULL;\nEND" : "BEGIN\n \nEND",
|
||||
markedForDrop: false,
|
||||
});
|
||||
}
|
||||
@@ -2160,7 +2161,7 @@ function toggleDropTrigger(trigger: EditableStructureTrigger) {
|
||||
}
|
||||
|
||||
function canEditTriggerDraft(trigger: EditableStructureTrigger): boolean {
|
||||
return !triggersLoading.value && canEditMysqlTriggers.value && !trigger.markedForDrop;
|
||||
return !triggersLoading.value && canEditTriggers.value && !trigger.markedForDrop;
|
||||
}
|
||||
|
||||
function primarySqlOperation(sql: string): string {
|
||||
@@ -2293,7 +2294,7 @@ function addItemForActiveTab(): boolean {
|
||||
addForeignKey();
|
||||
return true;
|
||||
}
|
||||
if (activeTab.value === "triggers" && canEditMysqlTriggers.value) {
|
||||
if (activeTab.value === "triggers" && canEditTriggers.value) {
|
||||
addTrigger();
|
||||
return true;
|
||||
}
|
||||
@@ -2648,7 +2649,7 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
<Plus :class="structureIconClass" />
|
||||
{{ t("structureEditor.addForeignKey") }}
|
||||
</Button>
|
||||
<Button v-if="activeTab === 'triggers'" size="sm" :class="structureToolbarButtonClass" :disabled="!canEditMysqlTriggers || triggersLoading" @click="addTrigger">
|
||||
<Button v-if="activeTab === 'triggers'" size="sm" :class="structureToolbarButtonClass" :disabled="!canEditTriggers || triggersLoading" @click="addTrigger">
|
||||
<Plus :class="structureIconClass" />
|
||||
{{ t("structureEditor.addTrigger") }}
|
||||
</Button>
|
||||
@@ -3193,9 +3194,10 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
</div>
|
||||
<div v-else class="space-y-1.5">
|
||||
<div v-for="trigger in triggers" :key="trigger.id" class="rounded-md border px-[var(--structure-cell-px)] py-[var(--structure-header-py)] text-[length:var(--structure-font-size)]" :class="trigger.markedForDrop ? 'bg-destructive/5 opacity-60' : ''">
|
||||
<div class="grid grid-cols-[minmax(140px,1fr)_110px_110px_auto] gap-1.5">
|
||||
<div class="grid grid-cols-[minmax(140px,1fr)_minmax(130px,180px)_minmax(140px,1fr)_auto] gap-1.5">
|
||||
<Input v-model="trigger.name" :class="structureControlClass" :placeholder="t('structureEditor.triggerName')" :disabled="!canEditTriggerDraft(trigger)" />
|
||||
<Select v-model="trigger.timing" :disabled="!canEditTriggerDraft(trigger)">
|
||||
<Input v-if="isOracleTriggerEditor" v-model="trigger.timing" :class="structureControlClass" :disabled="!canEditTriggerDraft(trigger)" />
|
||||
<Select v-else v-model="trigger.timing" :disabled="!canEditTriggerDraft(trigger)">
|
||||
<SelectTrigger class="h-[var(--structure-control-height)] rounded-[6px] px-[var(--structure-control-px)] text-[length:var(--structure-font-size)] focus-visible:border-ring/50 focus-visible:ring-1 focus-visible:ring-ring/25">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -3203,7 +3205,8 @@ watch([activeTab, ddlLoading], ([tab, loading]) => {
|
||||
<SelectItem v-for="timing in triggerTimingOptions" :key="timing" :value="timing">{{ timing }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select v-model="trigger.event" :disabled="!canEditTriggerDraft(trigger)">
|
||||
<Input v-if="isOracleTriggerEditor" v-model="trigger.event" :class="structureControlClass" :disabled="!canEditTriggerDraft(trigger)" />
|
||||
<Select v-else v-model="trigger.event" :disabled="!canEditTriggerDraft(trigger)">
|
||||
<SelectTrigger class="h-[var(--structure-control-height)] rounded-[6px] px-[var(--structure-control-px)] text-[length:var(--structure-font-size)] focus-visible:border-ring/50 focus-visible:ring-1 focus-visible:ring-ring/25">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -210,6 +210,14 @@ export default {
|
||||
mongoDriverLegacy: "Legacy",
|
||||
serviceName: "Service/SID",
|
||||
serviceNameOnly: "Service Name",
|
||||
oracleTnsAlias: "Network Alias",
|
||||
oracleTnsAliasPlaceholder: "Alias from tnsnames.ora",
|
||||
oracleTnsAdminPlaceholder: "Directory containing tnsnames.ora",
|
||||
oracleTnsAdminBrowse: "Select tnsnames.ora",
|
||||
oracleTnsPathHint: "The DBX backend/Oracle agent reads this path. Desktop paths are local; Web and Docker paths are on the server.",
|
||||
oracleTnsAliasRequired: "Oracle TNS mode requires a network alias.",
|
||||
oracleTnsAdminRequired: "Oracle TNS mode requires the TNS_ADMIN directory containing tnsnames.ora.",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS connections cannot use SSH, proxy, or HTTP tunnel layers. Remove the transport layer or use Service Name/SID mode.",
|
||||
elasticsearchDirectMode: "Direct",
|
||||
elasticsearchKibanaProxyMode: "Kibana Proxy",
|
||||
elasticsearchKibanaHost: "Kibana Host",
|
||||
|
||||
@@ -212,6 +212,14 @@ export default withEnglishFallback({
|
||||
mongoDriverLegacy: "Legacy",
|
||||
serviceName: "Servicio/SID",
|
||||
serviceNameOnly: "Nombre de servicio",
|
||||
oracleTnsAlias: "Alias de red",
|
||||
oracleTnsAliasPlaceholder: "Alias de tnsnames.ora",
|
||||
oracleTnsAdminPlaceholder: "Directorio que contiene tnsnames.ora",
|
||||
oracleTnsAdminBrowse: "Seleccionar tnsnames.ora",
|
||||
oracleTnsPathHint: "La ruta la lee el backend de DBX/Oracle Agent. En escritorio es local; en Web y Docker pertenece al servidor.",
|
||||
oracleTnsAliasRequired: "El modo Oracle TNS requiere un alias de red.",
|
||||
oracleTnsAdminRequired: "El modo Oracle TNS requiere el directorio TNS_ADMIN que contiene tnsnames.ora.",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS no se puede combinar con túneles SSH, proxy o HTTP. Elimine la capa de transporte o use Servicio/SID.",
|
||||
version: "Versión",
|
||||
driverInstallHintPrefix: "Instala el driver requerido desde ",
|
||||
driverInstallHintSuffix: " en la barra superior antes de conectar.",
|
||||
|
||||
@@ -211,6 +211,14 @@ export default withEnglishFallback({
|
||||
mongoDriverLegacy: "Legacy",
|
||||
serviceName: "Servizio/SID",
|
||||
serviceNameOnly: "Nome Servizio",
|
||||
oracleTnsAlias: "Alias di rete",
|
||||
oracleTnsAliasPlaceholder: "Alias da tnsnames.ora",
|
||||
oracleTnsAdminPlaceholder: "Directory contenente tnsnames.ora",
|
||||
oracleTnsAdminBrowse: "Seleziona tnsnames.ora",
|
||||
oracleTnsPathHint: "Il percorso viene letto dal backend DBX/Oracle Agent. Sul desktop è locale; su Web e Docker appartiene al server.",
|
||||
oracleTnsAliasRequired: "La modalità Oracle TNS richiede un alias di rete.",
|
||||
oracleTnsAdminRequired: "La modalità Oracle TNS richiede la directory TNS_ADMIN contenente tnsnames.ora.",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS non può usare tunnel SSH, proxy o HTTP. Rimuovi il livello di trasporto o usa Servizio/SID.",
|
||||
version: "Versione",
|
||||
driverInstallHintPrefix: "Installa il driver richiesto da ",
|
||||
driverInstallHintSuffix: " nella barra degli strumenti superiore prima di connetterti.",
|
||||
|
||||
@@ -211,6 +211,14 @@ export default withEnglishFallback({
|
||||
mongoDriverLegacy: "レガシー",
|
||||
serviceName: "サービス/SID",
|
||||
serviceNameOnly: "サービス名",
|
||||
oracleTnsAlias: "ネットワーク・エイリアス",
|
||||
oracleTnsAliasPlaceholder: "tnsnames.ora のエイリアス",
|
||||
oracleTnsAdminPlaceholder: "tnsnames.ora を含むディレクトリ",
|
||||
oracleTnsAdminBrowse: "tnsnames.ora を選択",
|
||||
oracleTnsPathHint: "このパスは DBX バックエンド/Oracle Agent が読み取ります。デスクトップではローカルパス、Web/Docker ではサーバーパスを指定してください。",
|
||||
oracleTnsAliasRequired: "Oracle TNS モードではネットワーク・エイリアスが必要です。",
|
||||
oracleTnsAdminRequired: "Oracle TNS モードでは tnsnames.ora を含む TNS_ADMIN ディレクトリが必要です。",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS 接続は SSH、プロキシ、HTTP トンネルと併用できません。トランスポート層を削除するか、サービス名/SID モードを使用してください。",
|
||||
version: "バージョン",
|
||||
driverInstallHintPrefix: "接続前に上部ツールバーの",
|
||||
driverInstallHintSuffix: "から必要なドライバーをインストールしてください。",
|
||||
|
||||
@@ -212,6 +212,14 @@ export default withEnglishFallback({
|
||||
mongoDriverLegacy: "Legacy",
|
||||
serviceName: "Service/SID",
|
||||
serviceNameOnly: "Service Name",
|
||||
oracleTnsAlias: "Alias de rede",
|
||||
oracleTnsAliasPlaceholder: "Alias do tnsnames.ora",
|
||||
oracleTnsAdminPlaceholder: "Diretório que contém tnsnames.ora",
|
||||
oracleTnsAdminBrowse: "Selecionar tnsnames.ora",
|
||||
oracleTnsPathHint: "O caminho é lido pelo backend do DBX/Oracle Agent. No desktop é local; no Web e Docker pertence ao servidor.",
|
||||
oracleTnsAliasRequired: "O modo Oracle TNS requer um alias de rede.",
|
||||
oracleTnsAdminRequired: "O modo Oracle TNS requer o diretório TNS_ADMIN que contém tnsnames.ora.",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS não pode usar túneis SSH, proxy ou HTTP. Remova a camada de transporte ou use Serviço/SID.",
|
||||
version: "Versão",
|
||||
driverInstallHintPrefix: "Instale o driver necessário em ",
|
||||
driverInstallHintSuffix: " na barra de ferramentas superior antes de conectar.",
|
||||
|
||||
@@ -212,6 +212,14 @@ export default withEnglishFallback({
|
||||
mongoDriverLegacy: "旧版兼容",
|
||||
serviceName: "服务名/SID",
|
||||
serviceNameOnly: "服务名",
|
||||
oracleTnsAlias: "网络别名",
|
||||
oracleTnsAliasPlaceholder: "tnsnames.ora 中的别名",
|
||||
oracleTnsAdminPlaceholder: "包含 tnsnames.ora 的目录",
|
||||
oracleTnsAdminBrowse: "选择 tnsnames.ora",
|
||||
oracleTnsPathHint: "该路径由 DBX 后端/Oracle Agent 读取。桌面端填写本机路径,Web 和 Docker 部署填写服务器路径。",
|
||||
oracleTnsAliasRequired: "Oracle TNS 模式需要填写网络别名。",
|
||||
oracleTnsAdminRequired: "Oracle TNS 模式需要填写包含 tnsnames.ora 的 TNS_ADMIN 目录。",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS 连接不能与 SSH、代理或 HTTP 隧道组合使用。请移除传输层,或改用服务名/SID 模式。",
|
||||
elasticsearchDirectMode: "直连",
|
||||
elasticsearchKibanaProxyMode: "Kibana 代理",
|
||||
elasticsearchKibanaHost: "Kibana 主机",
|
||||
|
||||
@@ -212,6 +212,14 @@ export default withEnglishFallback({
|
||||
mongoDriverLegacy: "舊版相容",
|
||||
serviceName: "服務/SID",
|
||||
serviceNameOnly: "服務名稱",
|
||||
oracleTnsAlias: "網路別名",
|
||||
oracleTnsAliasPlaceholder: "tnsnames.ora 中的別名",
|
||||
oracleTnsAdminPlaceholder: "包含 tnsnames.ora 的目錄",
|
||||
oracleTnsAdminBrowse: "選擇 tnsnames.ora",
|
||||
oracleTnsPathHint: "此路徑由 DBX 後端/Oracle Agent 讀取。桌面版填寫本機路徑,Web 和 Docker 部署填寫伺服器路徑。",
|
||||
oracleTnsAliasRequired: "Oracle TNS 模式需要填寫網路別名。",
|
||||
oracleTnsAdminRequired: "Oracle TNS 模式需要填寫包含 tnsnames.ora 的 TNS_ADMIN 目錄。",
|
||||
oracleTnsTransportUnsupported: "Oracle TNS 連線不能與 SSH、代理或 HTTP Tunnel 組合使用。請移除傳輸層,或改用服務名稱/SID 模式。",
|
||||
version: "版本",
|
||||
driverInstallHintPrefix: "需要在上方導覽列「",
|
||||
driverInstallHintSuffix: "」中安裝對應的驅動程式才能連線。",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "./oracleTnsConnection";
|
||||
|
||||
describe("Oracle TNS connection settings", () => {
|
||||
it("round-trips an alias and TNS_ADMIN directory", () => {
|
||||
const value = buildOracleTnsConnectionString("DBX_FAILOVER", "C:\\oracle network\\admin");
|
||||
|
||||
expect(parseOracleTnsConnectionString(value)).toEqual({
|
||||
alias: "DBX_FAILOVER",
|
||||
tnsAdmin: "C:\\oracle network\\admin",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes a selected tnsnames.ora file to its parent directory", () => {
|
||||
expect(normalizeOracleTnsAdminPath("C:\\oracle\\network\\admin\\tnsnames.ora")).toBe("C:\\oracle\\network\\admin");
|
||||
expect(normalizeOracleTnsAdminPath("C:\\tnsnames.ora")).toBe("C:\\");
|
||||
expect(normalizeOracleTnsAdminPath("/opt/oracle/network/admin/tnsnames.ora")).toBe("/opt/oracle/network/admin");
|
||||
});
|
||||
|
||||
it("does not reinterpret service, SID, or descriptor JDBC URLs as TNS aliases", () => {
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@//db.example.com:1521/ORCLPDB1")).toBeNull();
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@db.example.com:1521:ORCL")).toBeNull();
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(HOST=db.example.com)))")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores malformed encoded aliases instead of breaking connection editing", () => {
|
||||
expect(parseOracleTnsConnectionString("jdbc:oracle:thin:@DBX%ZZ?TNS_ADMIN=%2Fopt%2Foracle")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
export interface OracleTnsConnectionConfig {
|
||||
alias: string;
|
||||
tnsAdmin: string;
|
||||
}
|
||||
|
||||
const ORACLE_TNS_JDBC_PREFIX = "jdbc:oracle:thin:@";
|
||||
|
||||
export function parseOracleTnsConnectionString(value?: string): OracleTnsConnectionConfig | null {
|
||||
const source = value?.trim() || "";
|
||||
if (!source.toLowerCase().startsWith(ORACLE_TNS_JDBC_PREFIX)) return null;
|
||||
|
||||
const target = source.slice(ORACLE_TNS_JDBC_PREFIX.length);
|
||||
if (!target || target.startsWith("(") || target.startsWith("//") || target.includes(":")) return null;
|
||||
|
||||
const [rawAlias, rawQuery = ""] = target.split("?", 2);
|
||||
let alias = "";
|
||||
try {
|
||||
alias = decodeURIComponent(rawAlias).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const tnsAdmin = new URLSearchParams(rawQuery).get("TNS_ADMIN")?.trim() || "";
|
||||
if (!alias) return null;
|
||||
return { alias, tnsAdmin };
|
||||
}
|
||||
|
||||
export function buildOracleTnsConnectionString(alias: string, tnsAdmin: string): string {
|
||||
const normalizedAlias = alias.trim();
|
||||
const normalizedTnsAdmin = normalizeOracleTnsAdminPath(tnsAdmin);
|
||||
const query = new URLSearchParams({ TNS_ADMIN: normalizedTnsAdmin });
|
||||
return `${ORACLE_TNS_JDBC_PREFIX}${encodeURIComponent(normalizedAlias)}?${query.toString()}`;
|
||||
}
|
||||
|
||||
export function normalizeOracleTnsAdminPath(value: string): string {
|
||||
const path = value.trim();
|
||||
if (!path) return "";
|
||||
if (!/(^|[\\/])tnsnames\.ora$/i.test(path)) return path;
|
||||
|
||||
const parent = path.replace(/[\\/]tnsnames\.ora$/i, "");
|
||||
if (/^[A-Za-z]:$/.test(parent)) return `${parent}\\`;
|
||||
if (parent) return parent;
|
||||
return path.startsWith("\\") ? "\\" : "/";
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureS
|
||||
import { extractIdentifierPartsAt, isSqlKeyword, sqlObjectNavigationTarget, type SqlObjectNavigationTarget, type SqlObjectNavigationType } from "@/lib/sql/sqlNavigation";
|
||||
import type { ActiveTabSidebarTarget } from "@/lib/sidebar/sidebarActiveTabTarget";
|
||||
import type { SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
import type { DatabaseType, QueryTab, TreeNode } from "@/types/database";
|
||||
|
||||
export interface QueryCursorTableCandidate {
|
||||
@@ -53,7 +54,9 @@ export function queryCursorTableCandidate(tab: QueryTab | undefined | null, data
|
||||
}
|
||||
|
||||
export function queryTableCandidateAtSqlPosition(input: QueryTableCandidateAtPositionInput): QueryCursorTableCandidate | null {
|
||||
const parts = extractQualifiedIdentifierPartsAt(input.sql, input.position).map((part) => part.value);
|
||||
const dialect = sqlSemanticDialectFor({ databaseType: input.databaseType });
|
||||
// View-data SQL quotes resolved names, so fold only unquoted input exactly as the database does before quoting it again.
|
||||
const parts = extractQualifiedIdentifierPartsAt(input.sql, input.position).map((part) => dialect.normalizeIdentifier(part.value, part.quoted));
|
||||
if (parts.length === 0) return null;
|
||||
|
||||
const tableName = parts[parts.length - 1];
|
||||
|
||||
@@ -140,7 +140,7 @@ export interface ConnectionConfig {
|
||||
client_cert_path?: string;
|
||||
client_key_path?: string;
|
||||
sysdba?: boolean;
|
||||
oracle_connection_type?: "service_name" | "sid";
|
||||
oracle_connection_type?: "service_name" | "sid" | "tns";
|
||||
connection_string?: string;
|
||||
jdbc_driver_class?: string;
|
||||
jdbc_driver_paths?: string[];
|
||||
|
||||
Reference in new issue
Block a user