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:
1 parent
c41c2ae1af
commit
13424588b9
26 files changed
+635
-25
No files matched your search
@@ -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())
|
||||
|
||||
Reference in new issue
Block a user