fix(app): decode imported connection link names (#1930)
Co-authored-by: zero <zero@zeroMacmini.local>
This commit is contained in:
parent
2aba40ecb7
commit
f963a6e793
|
|
@ -24,7 +24,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
|||
import * as api from "@/lib/api";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connectionUrl";
|
||||
import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
|
||||
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
|
||||
import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation";
|
||||
import { h2ConnectionModeForConfig, h2FileJdbcUrl, h2FilePathFromJdbcUrl, type H2ConnectionMode } from "@/lib/h2Connection";
|
||||
import { firstZooKeeperEndpoint, normalizeZooKeeperConnectString } from "@/lib/zookeeperConnection";
|
||||
|
|
@ -1464,6 +1464,12 @@ async function testConnection() {
|
|||
|
||||
function applyConnectionUrlToForm(input: string): boolean {
|
||||
try {
|
||||
const draft = parseConnectionDeepLink(input);
|
||||
if (draft) {
|
||||
applyConnectionDraftToForm({ ...draft, oneTime: undefined });
|
||||
return true;
|
||||
}
|
||||
|
||||
const parsed = parseConnectionUrl(input, selectedType.value);
|
||||
form.value = applyParsedConnectionUrl(form.value, parsed);
|
||||
selectedType.value = parsed.driverProfile;
|
||||
|
|
@ -2146,8 +2152,7 @@ function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) {
|
|||
void nextTick(() => save());
|
||||
}
|
||||
|
||||
function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
|
||||
resetForm();
|
||||
function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) {
|
||||
applyProfile(draft.driverProfile);
|
||||
form.value = {
|
||||
...form.value,
|
||||
|
|
@ -2183,6 +2188,11 @@ function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
|
|||
dialogStep.value = "config";
|
||||
configTab.value = "connection";
|
||||
resetTestState();
|
||||
}
|
||||
|
||||
function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
|
||||
resetForm();
|
||||
applyConnectionDraftToForm(draft);
|
||||
submitOneTimePrefill(draft);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ function optionalBooleanParam(params: URLSearchParams, ...keys: string[]): boole
|
|||
function draftFromConnectionUrl(value: string, preferredProfile?: string): ConnectionDeepLinkDraft {
|
||||
const parsed = parseConnectionUrl(value, preferredProfile);
|
||||
return {
|
||||
name: parsed.name,
|
||||
dbType: parsed.dbType,
|
||||
driverProfile: parsed.driverProfile,
|
||||
driverLabel: parsed.driverLabel,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { ConnectionConfig, DatabaseType } from "@/types/database";
|
||||
|
||||
export interface ParsedConnectionUrl {
|
||||
name?: string;
|
||||
dbType: DatabaseType;
|
||||
driverProfile: string;
|
||||
driverLabel: string;
|
||||
|
|
@ -172,6 +173,28 @@ function queryParamValue(params: string, key: string): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function connectionNameParam(parsed: URL): string | undefined {
|
||||
for (const [key, value] of parsed.searchParams) {
|
||||
if (key.toLowerCase() === "name") {
|
||||
const name = value.trim();
|
||||
if (name) return name;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function stripConnectionNameParam(params: string): string {
|
||||
if (!params) return params;
|
||||
return params
|
||||
.split("&")
|
||||
.filter((part) => {
|
||||
if (!part) return true;
|
||||
const [rawKey] = part.split("=");
|
||||
return decodeUrlPart(rawKey).trim().toLowerCase() !== "name";
|
||||
})
|
||||
.join("&");
|
||||
}
|
||||
|
||||
function extractMysqlCredentialParams(params: string): { username?: string; password?: string; urlParams: string } {
|
||||
let username: string | undefined;
|
||||
let password: string | undefined;
|
||||
|
|
@ -430,8 +453,10 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
|||
}
|
||||
|
||||
const urlParams = parsed.search.replace(/^\?/, "");
|
||||
const name = connectionNameParam(parsed);
|
||||
const urlParamsWithoutName = stripConnectionNameParam(urlParams);
|
||||
const normalizedFragment = decodeUrlPart(parsed.hash.replace(/^#/, "")).trim().toLowerCase();
|
||||
const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParams, "insecure=true"].filter(Boolean).join("&") : urlParams;
|
||||
const parsedUrlParams = profile.type === "redis" && normalizedFragment === "insecure" ? [urlParamsWithoutName, "insecure=true"].filter(Boolean).join("&") : urlParamsWithoutName;
|
||||
const mysqlCredentials = isJdbcUrl && profile.type === "mysql" ? extractMysqlCredentialParams(parsedUrlParams) : undefined;
|
||||
const effectiveUrlParams = mysqlCredentials?.urlParams ?? parsedUrlParams;
|
||||
if (profile.type === "mongodb") {
|
||||
|
|
@ -452,6 +477,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
|||
}
|
||||
if (profile.type === "zookeeper") {
|
||||
return {
|
||||
...(name ? { name } : {}),
|
||||
dbType: profile.type,
|
||||
driverProfile: profile.profile,
|
||||
driverLabel: profile.label,
|
||||
|
|
@ -460,13 +486,14 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
|||
username: decodeUrlPart(parsed.username),
|
||||
password: decodeUrlPart(parsed.password),
|
||||
database: undefined,
|
||||
urlParams,
|
||||
urlParams: urlParamsWithoutName,
|
||||
ssl: false,
|
||||
connectionString: zookeeperConnectStringFromUrl(parsed, profile.defaultPort),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...(name ? { name } : {}),
|
||||
dbType: profile.type,
|
||||
driverProfile: profile.profile,
|
||||
driverLabel: profile.label,
|
||||
|
|
@ -496,6 +523,7 @@ export function applyParsedConnectionUrl(config: Omit<ConnectionConfig, "id">, p
|
|||
driver_label: parsed.driverLabel,
|
||||
host: parsed.host,
|
||||
port: parsed.port,
|
||||
name: parsed.name?.trim() || config.name,
|
||||
username: parsed.username,
|
||||
password: parsed.password,
|
||||
database: parsed.database,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,23 @@ test("parses encoded database URL with password", () => {
|
|||
assert.equal(draft?.urlParams, "sslmode=require");
|
||||
});
|
||||
|
||||
test("uses the nested database URL name as connection name", () => {
|
||||
const nested = encodeURIComponent("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker&charset=utf8mb4");
|
||||
const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}`);
|
||||
|
||||
assert.equal(draft?.name, "公司 - 本地Docker");
|
||||
assert.equal(draft?.dbType, "mysql");
|
||||
assert.equal(draft?.host, "localhost");
|
||||
assert.equal(draft?.urlParams, "charset=utf8mb4");
|
||||
});
|
||||
|
||||
test("top-level deep link name overrides nested database URL name", () => {
|
||||
const nested = encodeURIComponent("mysql://root@localhost/?name=Nested");
|
||||
const draft = parseConnectionDeepLink(`dbx://connection/new?url=${nested}&name=Top+Level`);
|
||||
|
||||
assert.equal(draft?.name, "Top Level");
|
||||
});
|
||||
|
||||
test("allows password query field to override database URL password", () => {
|
||||
const draft = parseConnectionDeepLink("dbx://connection/new?url=postgres%3A%2F%2Fapp%3Asecret%40db.internal%3A5432%2Forders&password=override");
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,32 @@ test("parses mysql URLs with encoded credentials", () => {
|
|||
assert.equal(parsed.urlParams, "charset=utf8mb4");
|
||||
});
|
||||
|
||||
test("parses mysql URL name as decoded connection name", () => {
|
||||
const parsed = parseConnectionUrl("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker&charset=utf8mb4");
|
||||
|
||||
assert.equal(parsed.name, "公司 - 本地Docker");
|
||||
assert.equal(parsed.host, "localhost");
|
||||
assert.equal(parsed.username, "root");
|
||||
assert.equal(parsed.password, "123456");
|
||||
assert.equal(parsed.urlParams, "charset=utf8mb4");
|
||||
});
|
||||
|
||||
test("consumes mysql URL name when it is the only URL param", () => {
|
||||
const parsed = parseConnectionUrl("mysql://root:123456@localhost/?name=%E5%85%AC%E5%8F%B8+-+%E6%9C%AC%E5%9C%B0Docker");
|
||||
|
||||
assert.equal(parsed.name, "公司 - 本地Docker");
|
||||
assert.equal(parsed.urlParams, "");
|
||||
});
|
||||
|
||||
test("removes only the connection name from URL params", () => {
|
||||
const parsed = parseConnectionUrl("mysql://root@localhost/app?Name=Analytics+Local&ssl-mode=required");
|
||||
|
||||
assert.equal(parsed.name, "Analytics Local");
|
||||
assert.equal(parsed.database, "app");
|
||||
assert.equal(parsed.urlParams, "ssl-mode=required");
|
||||
assert.equal(parsed.ssl, true);
|
||||
});
|
||||
|
||||
test("parses mysql TLS URL params into the SSL switch state", () => {
|
||||
assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?ssl-mode=required").ssl, true);
|
||||
assert.equal(parseConnectionUrl("mysql://root@tidb.example.com:4000/test?require_ssl=true").ssl, true);
|
||||
|
|
|
|||
Loading…
Reference in New Issue