mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
Support DBX connection deep links (#359)
* fix(mysql): avoid metadata collation conflicts Split MySQL column and primary key metadata lookups so MariaDB and OceanBase do not join information_schema tables with incompatible collations. * feat(grid): add invert column visibility action * feat(grid): improve cell detail editing * feat(grid): add keyboard cell navigation * feat(grid): add column name copy in header tooltip * feat(grid): improve transpose navigation * feat(connection): support dbx connection deep links * docs: document connection deep links * fix(connection): harden deep link boolean handling
This commit is contained in:
Generated
+70
-2
@@ -1713,6 +1713,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-log",
|
||||
@@ -1985,6 +1986,15 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||
dependencies = [
|
||||
"const-random",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dom_query"
|
||||
version = "0.27.0"
|
||||
@@ -2955,6 +2965,12 @@ dependencies = [
|
||||
"ahash 0.7.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -3251,7 +3267,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
"windows-registry 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3504,7 +3520,7 @@ checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222"
|
||||
dependencies = [
|
||||
"socket2",
|
||||
"widestring",
|
||||
"windows-registry",
|
||||
"windows-registry 0.6.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -4712,6 +4728,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||
dependencies = [
|
||||
"dlv-list",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@@ -5932,6 +5958,16 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.42.0"
|
||||
@@ -7420,6 +7456,27 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-deep-link"
|
||||
version = "2.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ee75bc5627f77bfdf40c913255ebc258117b10ebe2b2239a1a1cf40b0b58aa"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"plist",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.18",
|
||||
"tracing",
|
||||
"url",
|
||||
"windows-registry 0.5.3",
|
||||
"windows-result 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.7.1"
|
||||
@@ -8979,6 +9036,17 @@ dependencies = [
|
||||
"windows-link 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
|
||||
dependencies = [
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
|
||||
@@ -33,6 +33,7 @@ import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode }
|
||||
import { resolveExecutableSql } from "@/lib/sqlExecutionTarget";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { sqlFileTitleFromPath } from "@/lib/sqlFileOpen";
|
||||
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
|
||||
import {
|
||||
isCloseTabShortcut,
|
||||
isExecuteSqlShortcut,
|
||||
@@ -95,6 +96,7 @@ const authenticated = ref(isDesktop);
|
||||
const setupRequired = ref(false);
|
||||
|
||||
const showConnectionDialog = ref(false);
|
||||
const connectionDialogPrefill = ref<ConnectionDeepLinkDraft | null>(null);
|
||||
const showSettingsDialog = ref(false);
|
||||
const showDriverStore = ref(false);
|
||||
const agentDriverUpdateCount = ref(0);
|
||||
@@ -178,7 +180,11 @@ const { getDatabaseOptions } = useDatabaseOptions();
|
||||
const { openLineageTarget, openDatabaseSearchTarget, onStructureEditorSaved, openTableTarget } =
|
||||
useNavigationTargets(dialogs);
|
||||
const { onExecuteSql, onReloadData, onPaginate, onSort } = useDataGridActions(activeTab);
|
||||
const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openTableTarget, openSqlFilePath });
|
||||
const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({
|
||||
openTableTarget,
|
||||
openSqlFilePath,
|
||||
openConnectionDeepLink,
|
||||
});
|
||||
|
||||
const appVersion = ref("");
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
@@ -396,6 +402,36 @@ async function openPendingSqlFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openConnectionDeepLink(url: string) {
|
||||
try {
|
||||
const draft = parseConnectionDeepLink(url);
|
||||
if (!draft) return;
|
||||
connectionStore.stopEditing();
|
||||
connectionStore.stopCreatingConnectionInGroup();
|
||||
connectionDialogPrefill.value = draft;
|
||||
showConnectionDialog.value = true;
|
||||
} catch (e: any) {
|
||||
toast(t("connection.parseConnectionUrlFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
async function openPendingConnectionLinks() {
|
||||
if (!isTauriRuntime()) return;
|
||||
try {
|
||||
const links = await api.pendingOpenConnectionLinks();
|
||||
for (const link of links) {
|
||||
await openConnectionDeepLink(link);
|
||||
}
|
||||
} catch {
|
||||
/* ignore startup deep-link probing errors */
|
||||
}
|
||||
}
|
||||
|
||||
function setConnectionDialogOpen(value: boolean) {
|
||||
showConnectionDialog.value = value;
|
||||
if (!value) connectionDialogPrefill.value = null;
|
||||
}
|
||||
|
||||
async function newQuery() {
|
||||
const connId = connectionStore.activeConnectionId || connectionStore.connections[0]?.id;
|
||||
if (!connId) return;
|
||||
@@ -686,6 +722,7 @@ onMounted(async () => {
|
||||
.catch(() => {});
|
||||
setupTauriListeners();
|
||||
void openPendingSqlFiles();
|
||||
void openPendingConnectionLinks();
|
||||
console.log(`[STARTUP] onMounted sync done: ${(performance.now() - mountStart).toFixed(0)}ms`);
|
||||
});
|
||||
|
||||
@@ -899,11 +936,12 @@ onUnmounted(() => {
|
||||
|
||||
<AppDialogs
|
||||
:show-connection-dialog="showConnectionDialog"
|
||||
:connection-prefill="connectionDialogPrefill"
|
||||
:show-settings-dialog="showSettingsDialog"
|
||||
:app-version="appVersion"
|
||||
:show-danger-dialog="showDangerDialog"
|
||||
:danger-sql="dangerSql"
|
||||
@update:show-connection-dialog="showConnectionDialog = $event"
|
||||
@update:show-connection-dialog="setConnectionDialogOpen"
|
||||
@update:show-settings-dialog="showSettingsDialog = $event"
|
||||
@update:show-danger-dialog="showDangerDialog = $event"
|
||||
@danger-confirm="onDangerConfirm"
|
||||
@@ -913,7 +951,7 @@ onUnmounted(() => {
|
||||
(msg: string) => toast(t('connection.connectFailed', { message: translateBackendError(t, msg) }), 5000)
|
||||
"
|
||||
@open-driver-store="
|
||||
showConnectionDialog = false;
|
||||
setConnectionDialogOpen(false);
|
||||
showDriverStore = true;
|
||||
"
|
||||
@structure-editor-saved="onStructureEditorSaved(onReloadData, toast)"
|
||||
|
||||
@@ -16,6 +16,7 @@ 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 type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
|
||||
import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connectionPresentation";
|
||||
import { mongodbAuthFailureHint, mongoUrlParam, setMongoUrlParam } from "@/lib/mongoConnectionOptions";
|
||||
import { showAgentDriverInstallHint, type AgentDriverInstallState } from "@/lib/agentDriverInstallHint";
|
||||
@@ -45,6 +46,7 @@ const isDesktop = isTauriRuntime();
|
||||
|
||||
const props = defineProps<{
|
||||
editConfig?: ConnectionConfig;
|
||||
prefillConfig?: ConnectionDeepLinkDraft | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -612,6 +614,7 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
|
||||
config.ssh_connect_timeout_secs = Number.isFinite(sshTimeout) && sshTimeout > 0 ? sshTimeout : 5;
|
||||
const proxyPort = Number(config.proxy_port);
|
||||
config.proxy_port = Number.isFinite(proxyPort) && proxyPort > 0 ? proxyPort : 1080;
|
||||
if (!config.one_time) config.one_time = undefined;
|
||||
if (config.db_type === "mongodb" && !mongoUseUrl.value) {
|
||||
config.connection_string = undefined;
|
||||
}
|
||||
@@ -678,22 +681,95 @@ function resetForm() {
|
||||
resetTestState();
|
||||
}
|
||||
|
||||
const submittedOneTimePrefillKey = ref<string | null>(null);
|
||||
|
||||
function oneTimePrefillKey(draft: ConnectionDeepLinkDraft) {
|
||||
return JSON.stringify([
|
||||
draft.name,
|
||||
draft.dbType,
|
||||
draft.driverProfile,
|
||||
draft.driverLabel,
|
||||
draft.host,
|
||||
draft.port,
|
||||
draft.username,
|
||||
draft.password,
|
||||
draft.database,
|
||||
draft.urlParams,
|
||||
draft.ssl,
|
||||
draft.connectionString,
|
||||
draft.oracleConnectionType,
|
||||
draft.useMongoUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) {
|
||||
if (!draft.oneTime) return;
|
||||
const key = oneTimePrefillKey(draft);
|
||||
if (submittedOneTimePrefillKey.value === key) return;
|
||||
submittedOneTimePrefillKey.value = key;
|
||||
void nextTick(() => save());
|
||||
}
|
||||
|
||||
function applyConnectionPrefill(draft: ConnectionDeepLinkDraft) {
|
||||
resetForm();
|
||||
applyProfile(draft.driverProfile);
|
||||
form.value = {
|
||||
...form.value,
|
||||
db_type: draft.dbType,
|
||||
driver_profile: draft.driverProfile,
|
||||
driver_label: draft.driverLabel,
|
||||
host: draft.host ?? form.value.host,
|
||||
port: draft.port ?? form.value.port,
|
||||
username: draft.username ?? form.value.username,
|
||||
password: draft.password ?? form.value.password,
|
||||
database: draft.database ?? form.value.database,
|
||||
url_params: draft.urlParams ?? form.value.url_params,
|
||||
ssl: draft.ssl ?? form.value.ssl,
|
||||
connection_string: draft.connectionString ?? form.value.connection_string,
|
||||
oracle_connection_type: draft.oracleConnectionType ?? form.value.oracle_connection_type,
|
||||
one_time: draft.oneTime || undefined,
|
||||
};
|
||||
selectedType.value = draft.driverProfile;
|
||||
customDriverName.value = isCustomCompatibleProfile() ? draft.driverLabel : "";
|
||||
mongoUseUrl.value = !!draft.useMongoUrl;
|
||||
if (draft.name?.trim()) {
|
||||
form.value.name = draft.name.trim();
|
||||
} else if (!form.value.name.trim()) {
|
||||
form.value.name = draft.database || draft.host || draft.driverLabel;
|
||||
}
|
||||
dialogStep.value = "config";
|
||||
configTab.value = "connection";
|
||||
resetTestState();
|
||||
submitOneTimePrefill(draft);
|
||||
}
|
||||
|
||||
watch(
|
||||
open,
|
||||
(value) => {
|
||||
if (!value) {
|
||||
submittedOneTimePrefillKey.value = null;
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
if (!props.editConfig) {
|
||||
resetForm();
|
||||
if (props.prefillConfig) applyConnectionPrefill(props.prefillConfig);
|
||||
}
|
||||
if (!props.prefillConfig?.oneTime) {
|
||||
void loadJdbcDrivers();
|
||||
void loadAgentDrivers();
|
||||
}
|
||||
void loadJdbcDrivers();
|
||||
void loadAgentDrivers();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.prefillConfig,
|
||||
(draft) => {
|
||||
if (open.value && draft && !props.editConfig) applyConnectionPrefill(draft);
|
||||
},
|
||||
);
|
||||
|
||||
watch(canUseSsh, (value) => {
|
||||
if (!value && configTab.value === "ssh") {
|
||||
configTab.value = "connection";
|
||||
@@ -731,6 +807,7 @@ async function save() {
|
||||
emit("connectSucceeded", config.name);
|
||||
})
|
||||
.catch((e: any) => {
|
||||
if (config.one_time) void store.removeConnection(config.id);
|
||||
emit("connectFailed", mongodbAuthFailureHint(String(e?.message || e)));
|
||||
});
|
||||
return;
|
||||
|
||||
@@ -21,9 +21,11 @@ const DatabaseSearchDialog = defineAsyncComponent(() => import("@/components/sea
|
||||
const DatabaseExportDialog = defineAsyncComponent(() => import("@/components/export/DatabaseExportDialog.vue"));
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useDialogSources } from "@/composables/useDialogSources";
|
||||
import type { ConnectionDeepLinkDraft } from "@/lib/connectionDeepLink";
|
||||
|
||||
const props = defineProps<{
|
||||
showConnectionDialog: boolean;
|
||||
connectionPrefill?: ConnectionDeepLinkDraft | null;
|
||||
showSettingsDialog: boolean;
|
||||
settingsInitialTab?: string;
|
||||
appVersion?: string;
|
||||
@@ -99,6 +101,7 @@ watch(
|
||||
v-if="shouldShowConnectionDialog"
|
||||
:open="shouldShowConnectionDialog"
|
||||
:edit-config="editConfig"
|
||||
:prefill-config="connectionPrefill"
|
||||
@update:open="emit('update:showConnectionDialog', $event)"
|
||||
@connect-started="emit('connectStarted', $event)"
|
||||
@connect-succeeded="emit('connectSucceeded', $event)"
|
||||
|
||||
@@ -5,11 +5,20 @@ import type { NavigationTarget } from "@/composables/useNavigationTargets";
|
||||
export function useTauriEvents(deps: {
|
||||
openTableTarget: (target: NavigationTarget) => Promise<void>;
|
||||
openSqlFilePath: (path: string) => Promise<void>;
|
||||
openConnectionDeepLink: (url: string) => Promise<void>;
|
||||
}) {
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const unlistenHandles: Array<() => void> = [];
|
||||
|
||||
function focusCurrentWindow() {
|
||||
void import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {}),
|
||||
);
|
||||
}
|
||||
|
||||
function setupTauriListeners() {
|
||||
import("@tauri-apps/api/event")
|
||||
.then(({ listen }) => {
|
||||
@@ -30,11 +39,7 @@ export function useTauriEvents(deps: {
|
||||
} else {
|
||||
deps.openTableTarget({ connectionId: connection_id, database, schema, tableName: table });
|
||||
}
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {}),
|
||||
);
|
||||
focusCurrentWindow();
|
||||
} catch (e) {
|
||||
console.error("[DBX] mcp-open-table error:", e);
|
||||
}
|
||||
@@ -60,11 +65,7 @@ export function useTauriEvents(deps: {
|
||||
const tabId = queryStore.createTab(connection_id, database, undefined, "query");
|
||||
queryStore.updateSql(tabId, sql);
|
||||
await queryStore.executeTabSql(tabId, sql);
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {}),
|
||||
);
|
||||
focusCurrentWindow();
|
||||
} catch (e) {
|
||||
console.error("[DBX] mcp-execute-query error:", e);
|
||||
}
|
||||
@@ -75,15 +76,22 @@ export function useTauriEvents(deps: {
|
||||
for (const path of event.payload) {
|
||||
await deps.openSqlFilePath(path);
|
||||
}
|
||||
import("@tauri-apps/api/window").then(({ getCurrentWindow }) =>
|
||||
getCurrentWindow()
|
||||
.setFocus()
|
||||
.catch(() => {}),
|
||||
);
|
||||
focusCurrentWindow();
|
||||
} catch (e) {
|
||||
console.error("[DBX] dbx-open-sql-files error:", e);
|
||||
}
|
||||
}).then((unlisten) => unlistenHandles.push(unlisten));
|
||||
|
||||
listen<string[]>("dbx-open-connection-links", async (event) => {
|
||||
try {
|
||||
for (const url of event.payload) {
|
||||
await deps.openConnectionDeepLink(url);
|
||||
}
|
||||
focusCurrentWindow();
|
||||
} catch (e) {
|
||||
console.error("[DBX] dbx-open-connection-links error:", e);
|
||||
}
|
||||
}).then((unlisten) => unlistenHandles.push(unlisten));
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ export const executeSqlFile = forward("executeSqlFile");
|
||||
export const cancelSqlFileExecution = forward("cancelSqlFileExecution");
|
||||
export const listenSqlFileProgress = forward("listenSqlFileProgress");
|
||||
export const pendingOpenSqlFiles = forward("pendingOpenSqlFiles");
|
||||
export const pendingOpenConnectionLinks = forward("pendingOpenConnectionLinks");
|
||||
export const readExternalSqlFile = forward("readExternalSqlFile");
|
||||
|
||||
// Data Transfer
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { connectionProfileForScheme, parseConnectionUrl } from "@/lib/connectionUrl";
|
||||
|
||||
export interface ConnectionDeepLinkDraft {
|
||||
name?: string;
|
||||
dbType: DatabaseType;
|
||||
driverProfile: string;
|
||||
driverLabel: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
username?: string;
|
||||
password?: string;
|
||||
database?: string;
|
||||
urlParams?: string;
|
||||
ssl?: boolean;
|
||||
connectionString?: string;
|
||||
oracleConnectionType?: "service_name" | "sid";
|
||||
useMongoUrl?: boolean;
|
||||
oneTime?: boolean;
|
||||
}
|
||||
|
||||
const CONNECTION_DEEP_LINK_TARGET = "connection/new";
|
||||
|
||||
function normalizePath(url: URL): string {
|
||||
return [url.hostname, url.pathname.replace(/^\/+/, "")].filter(Boolean).join("/").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function optionalParam(params: URLSearchParams, ...keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = params.get(key)?.trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function optionalNumberParam(params: URLSearchParams, ...keys: string[]): number | undefined {
|
||||
const value = optionalParam(params, ...keys);
|
||||
if (!value) return undefined;
|
||||
const numberValue = Number(value);
|
||||
return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : undefined;
|
||||
}
|
||||
|
||||
function optionalBooleanParam(params: URLSearchParams, ...keys: string[]): boolean {
|
||||
const value = optionalParam(params, ...keys)?.toLowerCase();
|
||||
return value === "true" || value === "1" || value === "yes" || value === "on";
|
||||
}
|
||||
|
||||
function draftFromConnectionUrl(value: string, preferredProfile?: string): ConnectionDeepLinkDraft {
|
||||
const parsed = parseConnectionUrl(value, preferredProfile);
|
||||
return {
|
||||
dbType: parsed.dbType,
|
||||
driverProfile: parsed.driverProfile,
|
||||
driverLabel: parsed.driverLabel,
|
||||
host: parsed.host,
|
||||
port: parsed.port,
|
||||
username: parsed.username,
|
||||
password: parsed.password,
|
||||
database: parsed.database,
|
||||
urlParams: parsed.urlParams,
|
||||
ssl: parsed.ssl,
|
||||
connectionString: parsed.connectionString,
|
||||
oracleConnectionType: parsed.oracleConnectionType,
|
||||
useMongoUrl: parsed.useMongoUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseConnectionDeepLink(value: string): ConnectionDeepLinkDraft | null {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (url.protocol !== "dbx:") return null;
|
||||
if (normalizePath(url) !== CONNECTION_DEEP_LINK_TARGET) return null;
|
||||
|
||||
const params = url.searchParams;
|
||||
const preferredProfile = optionalParam(params, "type");
|
||||
const rawConnectionUrl = optionalParam(params, "url");
|
||||
const draft: ConnectionDeepLinkDraft = rawConnectionUrl
|
||||
? draftFromConnectionUrl(rawConnectionUrl, preferredProfile)
|
||||
: (() => {
|
||||
const profile = connectionProfileForScheme(preferredProfile || "mysql");
|
||||
if (!profile) throw new Error(`Unsupported connection type: ${preferredProfile}`);
|
||||
return {
|
||||
dbType: profile.type,
|
||||
driverProfile: profile.profile,
|
||||
driverLabel: profile.label,
|
||||
port: profile.defaultPort,
|
||||
ssl: false,
|
||||
};
|
||||
})();
|
||||
|
||||
const oneTime = optionalBooleanParam(params, "one_time");
|
||||
|
||||
return {
|
||||
...draft,
|
||||
name: optionalParam(params, "name") ?? draft.name,
|
||||
host: optionalParam(params, "host") ?? draft.host,
|
||||
port: optionalNumberParam(params, "port") ?? draft.port,
|
||||
username: optionalParam(params, "user") ?? draft.username,
|
||||
password: optionalParam(params, "password") ?? draft.password,
|
||||
database: optionalParam(params, "database") ?? draft.database,
|
||||
urlParams: optionalParam(params, "url_params") ?? draft.urlParams,
|
||||
ssl: optionalBooleanParam(params, "ssl") ? true : draft.ssl,
|
||||
...(oneTime ? { oneTime: true } : {}),
|
||||
};
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export interface ParsedConnectionUrl {
|
||||
useMongoUrl?: boolean;
|
||||
}
|
||||
|
||||
type ConnectionProfile = {
|
||||
export type ConnectionProfile = {
|
||||
type: DatabaseType;
|
||||
profile: string;
|
||||
label: string;
|
||||
@@ -66,7 +66,7 @@ function databaseFromPath(pathname: string): string | undefined {
|
||||
return decodeUrlPart(value.split("/")[0]);
|
||||
}
|
||||
|
||||
function profileForScheme(scheme: string, preferredProfile?: string): ConnectionProfile | undefined {
|
||||
export function connectionProfileForScheme(scheme: string, preferredProfile?: string): ConnectionProfile | undefined {
|
||||
if ((scheme === "http" || scheme === "https") && preferredProfile) {
|
||||
return HTTP_SELECTED_PROFILES[preferredProfile];
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
|
||||
}
|
||||
|
||||
const scheme = parsed.protocol.replace(/:$/, "").toLowerCase();
|
||||
const profile = profileForScheme(scheme, preferredProfile);
|
||||
const profile = connectionProfileForScheme(scheme, preferredProfile);
|
||||
if (!profile) {
|
||||
throw new Error(`Unsupported connection URL scheme: ${scheme}`);
|
||||
}
|
||||
|
||||
@@ -499,6 +499,10 @@ export async function pendingOpenSqlFiles(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function pendingOpenConnectionLinks(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(_path: string): Promise<string> {
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
@@ -112,6 +112,10 @@ export async function pendingOpenSqlFiles(): Promise<string[]> {
|
||||
return invoke("pending_open_sql_files");
|
||||
}
|
||||
|
||||
export async function pendingOpenConnectionLinks(): Promise<string[]> {
|
||||
return invoke("pending_open_connection_links");
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(path: string): Promise<string> {
|
||||
return invoke("read_external_sql_file", { path });
|
||||
}
|
||||
|
||||
@@ -601,6 +601,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
|
||||
async function disconnect(connectionId: string) {
|
||||
const shouldRemoveOneTimeConnection = getConfig(connectionId)?.one_time === true;
|
||||
await api.disconnectDb(connectionId);
|
||||
connectedIds.value.delete(connectionId);
|
||||
const node = treeNodes.value.find((n) => n.connectionId === connectionId);
|
||||
@@ -613,6 +614,9 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
activeConnectionId.value = null;
|
||||
}
|
||||
invalidateCompletionCache(connectionId);
|
||||
if (shouldRemoveOneTimeConnection) {
|
||||
await removeConnection(connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureConnected(connectionId: string) {
|
||||
|
||||
@@ -79,6 +79,7 @@ export interface ConnectionConfig {
|
||||
connection_string?: string;
|
||||
jdbc_driver_class?: string;
|
||||
jdbc_driver_paths?: string[];
|
||||
one_time?: boolean;
|
||||
}
|
||||
|
||||
export interface AttachedDatabaseConfig {
|
||||
|
||||
@@ -689,6 +689,7 @@ mod tests {
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -313,6 +313,7 @@ mod tests {
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ pub struct ConnectionConfig {
|
||||
pub jdbc_driver_class: Option<String>,
|
||||
#[serde(default)]
|
||||
pub jdbc_driver_paths: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub one_time: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -88,6 +90,10 @@ fn default_proxy_port() -> u16 {
|
||||
1080
|
||||
}
|
||||
|
||||
fn is_false(value: &bool) -> bool {
|
||||
!*value
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProxyType {
|
||||
@@ -671,6 +677,7 @@ mod tests {
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1276,6 +1276,7 @@ mod tests {
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
};
|
||||
|
||||
let params = external_driver_query_params(&config, "SELECT * FROM events", "analytics", Some("app"));
|
||||
|
||||
@@ -166,6 +166,7 @@ mod tests {
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,48 @@ CLI JSON 错误使用稳定错误码:
|
||||
| `DBX_NOT_RUNNING` | DBX 桌面端 bridge 不可用 |
|
||||
| `ERROR` | 未预期的运行时错误 |
|
||||
|
||||
## 桌面端 Deep Link
|
||||
|
||||
DBX 桌面端支持通过 `dbx://connection/new` 从浏览器、堡垒机或脚本打开新建连接窗口并预填连接信息。
|
||||
|
||||
DSN 模式:
|
||||
|
||||
```bash
|
||||
open 'dbx://connection/new?url=postgres%3A%2F%2Fapp%3Asecret%40db.internal%3A5432%2Forders'
|
||||
```
|
||||
|
||||
字段模式:
|
||||
|
||||
```bash
|
||||
open 'dbx://connection/new?type=mysql&host=127.0.0.1&port=3306&user=root&password=secret&database=test'
|
||||
```
|
||||
|
||||
支持的字段:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `type` | 数据库类型,例如 `mysql`、`postgres`、`redis`、`mongodb` |
|
||||
| `url` | 数据库 DSN,建议 URL encode |
|
||||
| `name` | DBX 中显示的连接名;未传时优先使用 `database`,其次使用 `host` |
|
||||
| `host` | 主机地址 |
|
||||
| `port` | 端口 |
|
||||
| `user` | 用户名 |
|
||||
| `password` | 密码 |
|
||||
| `database` | 数据库名;Redis 可传 DB index,例如 `0` |
|
||||
| `url_params` | 额外连接参数,例如 `sslmode=require` |
|
||||
| `ssl` | 是否启用 SSL,`true` 表示启用 |
|
||||
| `one_time` | `true` 时自动连接,断开后自动删除连接信息 |
|
||||
|
||||
一次性连接示例:
|
||||
|
||||
```bash
|
||||
open 'dbx://connection/new?type=redis&host=127.0.0.1&port=6379&user=default&password=secret&database=0&one_time=true'
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
测试系统级 `dbx://` 唤起前,请先安装并启动一次 DBX 桌面端,让系统注册 URL 协议。macOS 可使用 `open 'dbx://connection/new?...'` 测试。
|
||||
</Callout>
|
||||
|
||||
## Codex
|
||||
|
||||
Codex 可以直接通过 shell 调用 CLI:
|
||||
|
||||
@@ -130,6 +130,48 @@ CLI JSON errors use stable codes:
|
||||
| `DBX_NOT_RUNNING` | DBX Desktop bridge is unavailable |
|
||||
| `ERROR` | Unexpected runtime failure |
|
||||
|
||||
## Desktop Deep Links
|
||||
|
||||
DBX Desktop supports `dbx://connection/new` links for opening the new connection dialog from browsers, bastion hosts, or scripts with connection fields prefilled.
|
||||
|
||||
DSN mode:
|
||||
|
||||
```bash
|
||||
open 'dbx://connection/new?url=postgres%3A%2F%2Fapp%3Asecret%40db.internal%3A5432%2Forders'
|
||||
```
|
||||
|
||||
Field mode:
|
||||
|
||||
```bash
|
||||
open 'dbx://connection/new?type=mysql&host=127.0.0.1&port=3306&user=root&password=secret&database=test'
|
||||
```
|
||||
|
||||
Supported fields:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `type` | Database type, such as `mysql`, `postgres`, `redis`, or `mongodb` |
|
||||
| `url` | Database DSN; URL encoding is recommended |
|
||||
| `name` | Display name in DBX; when omitted, DBX uses `database`, then `host` |
|
||||
| `host` | Hostname or IP address |
|
||||
| `port` | Port |
|
||||
| `user` | Username |
|
||||
| `password` | Password |
|
||||
| `database` | Database name; for Redis this can be a DB index such as `0` |
|
||||
| `url_params` | Extra connection parameters, such as `sslmode=require` |
|
||||
| `ssl` | Enable SSL when set to `true` |
|
||||
| `one_time` | Automatically connect when set to `true`, then delete the connection after disconnecting |
|
||||
|
||||
One-time connection example:
|
||||
|
||||
```bash
|
||||
open 'dbx://connection/new?type=redis&host=127.0.0.1&port=6379&user=default&password=secret&database=0&one_time=true'
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Before testing system-level `dbx://` launches, install and open DBX Desktop once so the operating system can register the URL scheme. On macOS, use `open 'dbx://connection/new?...'` to test it.
|
||||
</Callout>
|
||||
|
||||
## Codex
|
||||
|
||||
Codex can call the CLI directly from shell tools:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { strict as assert } from "node:assert";
|
||||
import test from "node:test";
|
||||
import { parseConnectionDeepLink } from "../../apps/desktop/src/lib/connectionDeepLink.ts";
|
||||
|
||||
test("parses dbx connection deep link query fields", () => {
|
||||
const draft = parseConnectionDeepLink(
|
||||
"dbx://connection/new?type=postgres&host=db.internal&port=15432&user=app&database=orders&name=Orders",
|
||||
);
|
||||
|
||||
assert.deepEqual(draft, {
|
||||
name: "Orders",
|
||||
dbType: "postgres",
|
||||
driverProfile: "postgres",
|
||||
driverLabel: "PostgreSQL",
|
||||
host: "db.internal",
|
||||
port: 15432,
|
||||
username: "app",
|
||||
password: undefined,
|
||||
database: "orders",
|
||||
urlParams: undefined,
|
||||
ssl: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("parses encoded database URL with password", () => {
|
||||
const draft = parseConnectionDeepLink(
|
||||
"dbx://connection/new?url=postgres%3A%2F%2Fapp%3Asecret%40db.internal%3A5432%2Forders%3Fsslmode%3Drequire",
|
||||
);
|
||||
|
||||
assert.equal(draft?.dbType, "postgres");
|
||||
assert.equal(draft?.driverProfile, "postgres");
|
||||
assert.equal(draft?.host, "db.internal");
|
||||
assert.equal(draft?.port, 5432);
|
||||
assert.equal(draft?.username, "app");
|
||||
assert.equal(draft?.password, "secret");
|
||||
assert.equal(draft?.database, "orders");
|
||||
assert.equal(draft?.urlParams, "sslmode=require");
|
||||
});
|
||||
|
||||
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",
|
||||
);
|
||||
|
||||
assert.equal(draft?.password, "override");
|
||||
});
|
||||
|
||||
test("parses boolean control parameters consistently", () => {
|
||||
const draft = parseConnectionDeepLink("dbx://connection/new?type=mysql&ssl=1&one_time=yes");
|
||||
|
||||
assert.equal(draft?.ssl, true);
|
||||
assert.equal(draft?.oneTime, true);
|
||||
});
|
||||
|
||||
test("ignores unsupported dbx deep link targets", () => {
|
||||
assert.equal(parseConnectionDeepLink("dbx://query/open?sql=select%201"), null);
|
||||
assert.equal(parseConnectionDeepLink("dbx://connections/new?type=postgres"), null);
|
||||
});
|
||||
@@ -27,6 +27,7 @@ tokio = { version = "1", features = ["full"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
anyhow = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tauri-plugin-deep-link = "2"
|
||||
tauri-plugin-dialog = "2.7.0"
|
||||
tauri-plugin-fs = "2.5.0"
|
||||
tauri-plugin-single-instance = "2.4.2"
|
||||
|
||||
@@ -62,6 +62,7 @@ mod tests {
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
one_time: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
|
||||
const CONNECTION_DEEP_LINK_PREFIX: &str = "dbx://connection/new";
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pending_open_connection_links(state: tauri::State<'_, DeepLinkOpenState>) -> Vec<String> {
|
||||
dedupe_links(state.drain())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DeepLinkOpenState {
|
||||
pending: Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
impl DeepLinkOpenState {
|
||||
pub fn push(&self, links: Vec<String>) {
|
||||
if links.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Ok(mut pending) = self.pending.lock() {
|
||||
pending.extend(links);
|
||||
}
|
||||
}
|
||||
|
||||
fn drain(&self) -> Vec<String> {
|
||||
self.pending.lock().map(|mut pending| pending.drain(..).collect()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connection_deep_links_from_args<I, S>(args: I) -> Vec<String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
args.into_iter().filter_map(|arg| connection_deep_link_from_arg(arg.as_ref())).collect()
|
||||
}
|
||||
|
||||
pub fn connection_deep_link_from_arg(arg: &str) -> Option<String> {
|
||||
let trimmed = arg.trim();
|
||||
let suffix = trimmed.strip_prefix(CONNECTION_DEEP_LINK_PREFIX)?;
|
||||
if !(suffix.is_empty()
|
||||
|| suffix.starts_with('?')
|
||||
|| suffix.starts_with('#')
|
||||
|| suffix == "/"
|
||||
|| suffix.starts_with("/?")
|
||||
|| suffix.starts_with("/#"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn dedupe_links(links: Vec<String>) -> Vec<String> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut unique = Vec::new();
|
||||
for link in links {
|
||||
if seen.insert(link.clone()) {
|
||||
unique.push(link);
|
||||
}
|
||||
}
|
||||
unique
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn filters_connection_deep_links() {
|
||||
let links = connection_deep_links_from_args([
|
||||
"dbx://connection/new?type=mysql&host=127.0.0.1",
|
||||
"--flag",
|
||||
"dbx://open?x=1",
|
||||
"dbx://connections/new?type=postgres",
|
||||
"dbx://connection/newer?type=mysql",
|
||||
]);
|
||||
|
||||
assert_eq!(links, vec!["dbx://connection/new?type=mysql&host=127.0.0.1".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drains_pending_links_once() {
|
||||
let state = DeepLinkOpenState::default();
|
||||
state.push(vec!["dbx://connection/new?type=mysql".to_string()]);
|
||||
|
||||
assert_eq!(state.drain(), vec!["dbx://connection/new?type=mysql"]);
|
||||
assert!(state.drain().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dedupes_links_while_preserving_order() {
|
||||
assert_eq!(
|
||||
dedupe_links(vec![
|
||||
"dbx://connection/new?type=mysql".to_string(),
|
||||
"dbx://connection/new?type=postgres".to_string(),
|
||||
"dbx://connection/new?type=mysql".to_string(),
|
||||
]),
|
||||
vec!["dbx://connection/new?type=mysql", "dbx://connection/new?type=postgres"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod connection;
|
||||
#[allow(dead_code, unused_imports)]
|
||||
mod connection_secrets;
|
||||
pub mod database_export;
|
||||
pub mod deep_link;
|
||||
pub mod external_sql;
|
||||
pub mod history;
|
||||
pub mod mcp_bridge;
|
||||
|
||||
@@ -13,6 +13,8 @@ use tauri::{
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
};
|
||||
use tauri::{Emitter, Manager, RunEvent};
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
|
||||
fn should_hide_window_on_close(target_os: &str) -> bool {
|
||||
matches!(target_os, "macos" | "windows")
|
||||
@@ -26,6 +28,17 @@ fn show_main_window(app: &tauri::AppHandle) {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_connection_deep_links(app: &tauri::AppHandle, links: Vec<String>) {
|
||||
if links.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(state) = app.try_state::<commands::deep_link::DeepLinkOpenState>() {
|
||||
state.push(links.clone());
|
||||
}
|
||||
let _ = app.emit("dbx-open-connection-links", links);
|
||||
show_main_window(app);
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
fn setup_windows_tray(app: &mut tauri::App) -> tauri::Result<()> {
|
||||
let menu = MenuBuilder::new(app).text("show", "Show DBX").separator().text("quit", "Quit DBX").build()?;
|
||||
@@ -75,9 +88,13 @@ pub fn run() {
|
||||
let startup_begin = Instant::now();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
|
||||
let links = commands::deep_link::connection_deep_links_from_args(args.clone());
|
||||
open_connection_deep_links(app, links);
|
||||
|
||||
let paths = commands::external_sql::sql_file_paths_from_args(args, std::path::Path::new(&cwd));
|
||||
if !paths.is_empty() {
|
||||
if let Some(state) = app.try_state::<commands::external_sql::ExternalSqlOpenState>() {
|
||||
@@ -123,6 +140,9 @@ pub fn run() {
|
||||
));
|
||||
app.manage(state.clone());
|
||||
app.manage(commands::external_sql::ExternalSqlOpenState::default());
|
||||
app.manage(commands::deep_link::DeepLinkOpenState::default());
|
||||
let startup_links = commands::deep_link::connection_deep_links_from_args(std::env::args().skip(1));
|
||||
open_connection_deep_links(app.handle(), startup_links);
|
||||
|
||||
let app_handle = app.handle().clone();
|
||||
commands::mcp_bridge::start(app_handle, state);
|
||||
@@ -137,6 +157,8 @@ pub fn run() {
|
||||
#[cfg(target_os = "windows")]
|
||||
setup_windows_tray(app)?;
|
||||
window_state_guard::enforce_main_window_bounds(app.handle());
|
||||
#[cfg(any(windows, target_os = "linux"))]
|
||||
let _ = app.deep_link().register_all();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
@@ -198,6 +220,7 @@ pub fn run() {
|
||||
commands::sql_file::cancel_sql_file_execution,
|
||||
commands::external_sql::pending_open_sql_files,
|
||||
commands::external_sql::read_external_sql_file,
|
||||
commands::deep_link::pending_open_connection_links,
|
||||
commands::table_import::preview_table_import_file,
|
||||
commands::table_import::import_table_file,
|
||||
commands::table_import::cancel_table_import,
|
||||
@@ -260,6 +283,13 @@ pub fn run() {
|
||||
.run(|app_handle, event| {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let RunEvent::Opened { urls } = &event {
|
||||
let links: Vec<String> = urls
|
||||
.iter()
|
||||
.map(|url| url.to_string())
|
||||
.filter_map(|url| commands::deep_link::connection_deep_link_from_arg(&url))
|
||||
.collect();
|
||||
open_connection_deep_links(app_handle, links);
|
||||
|
||||
let paths: Vec<String> = urls
|
||||
.iter()
|
||||
.filter_map(|url| url.to_file_path().ok())
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
"desktop": {
|
||||
"schemes": ["dbx"]
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDkxNEYyMzE3QkJCNkM3REYKUldUZng3YTdGeU5Qa1NnTUZCRVhJVkxsOWZKQWFPVCtacUgySlNlcjRDU2RmZElCQnphd25pYUMK",
|
||||
"endpoints": [
|
||||
|
||||
Reference in New Issue
Block a user