refactor: convert to bun-workspaces monorepo (desktop/ + www/)
- Move the Tauri app (React frontend + Rust backend) into desktop/ via git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts) - Root package.json becomes a private bun workspace container with orchestration scripts; desktop package renamed gridline-desktop - Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin, static output, ready for Dokploy) - Update release.yml for the new layout: projectPath: desktop, desktop/src-tauri resource paths, rust-cache workspace path - Update README + AGENTS.md structure trees and dev commands - Verified: vitest (1074), cargo test (354), desktop build, tauri dev (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import { useEffect, useCallback, useRef, useState } from "react";
|
||||
import { useConnectionStore } from "../stores/connectionStore";
|
||||
import { useDbViewerStore } from "../stores/dbViewerStore";
|
||||
import { useNotificationStore } from "../stores/notificationStore";
|
||||
import * as cmd from "../lib/commands";
|
||||
import { pickDefaultSchema } from "../lib/utils";
|
||||
import type { ConnectionInput, TableInfo } from "../lib/types";
|
||||
|
||||
export function useDbConnection(connectionId: string) {
|
||||
const reset = useDbViewerStore((s) => s.reset);
|
||||
const populate = useDbViewerStore((s) => s.populate);
|
||||
const setSchemaTreeLoading = useDbViewerStore((s) => s.setSchemaTreeLoading);
|
||||
const currentDatabase = useDbViewerStore((s) => s.currentDatabase);
|
||||
const setCurrentDatabase = useDbViewerStore((s) => s.setCurrentDatabase);
|
||||
const setCurrentSchema = useDbViewerStore((s) => s.setCurrentSchema);
|
||||
const notify = useNotificationStore((s) => s.notify);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const [passwordPromptOpen, setPasswordPromptOpen] = useState(false);
|
||||
const inputRef = useRef<ConnectionInput | null>(null);
|
||||
// The database the pool is currently connected to. Unlike the selected
|
||||
// `currentDatabase`, this lets us reconnect whenever the selection drifts
|
||||
// from the live connection (including switching back to the first DB).
|
||||
const connectedDbRef = useRef<string | null>(null);
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
const conn = useConnectionStore
|
||||
.getState()
|
||||
.connections.find((c) => c.id === connectionId);
|
||||
if (!conn) {
|
||||
setConnectionError("Connection not found");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const password = await useConnectionStore.getState().getConnectionPassword(conn.id).catch(() => null);
|
||||
// Keychain-off + no session password: prompt the user instead of
|
||||
// connecting with an empty password (early return, no Tauri call).
|
||||
if (conn.use_keychain === false && !password) {
|
||||
setPasswordPromptOpen(true);
|
||||
return;
|
||||
}
|
||||
const sshPassword = conn.ssh_host
|
||||
? await cmd.getConnectionSshPassword(conn.id).catch(() => null)
|
||||
: null;
|
||||
const sshPassphrase = conn.ssh_host
|
||||
? await cmd.getConnectionSshPassphrase(conn.id).catch(() => null)
|
||||
: null;
|
||||
const input: ConnectionInput = {
|
||||
name: conn.name,
|
||||
db_type: conn.db_type,
|
||||
host: conn.host,
|
||||
port: conn.port,
|
||||
username: conn.username,
|
||||
password,
|
||||
database: conn.database,
|
||||
folder_id: conn.folder_id,
|
||||
ssh_host: conn.ssh_host,
|
||||
ssh_port: conn.ssh_port,
|
||||
ssh_user: conn.ssh_user,
|
||||
ssh_auth_method:
|
||||
conn.ssh_auth_method as "password" | "key" | null | undefined,
|
||||
ssh_private_key_path: conn.ssh_private_key_path,
|
||||
ssh_password: sshPassword,
|
||||
ssh_passphrase: sshPassphrase,
|
||||
ssl_mode:
|
||||
conn.ssl_mode as
|
||||
| "disable"
|
||||
| "require"
|
||||
| "verify-ca"
|
||||
| "verify-full"
|
||||
| null
|
||||
| undefined,
|
||||
ssl_ca_path: conn.ssl_ca_path,
|
||||
ssl_cert_path: conn.ssl_cert_path,
|
||||
ssl_key_path: conn.ssl_key_path,
|
||||
};
|
||||
await cmd.dbConnect(connectionId, input);
|
||||
setConnectionError(null);
|
||||
inputRef.current = input;
|
||||
connectedDbRef.current =
|
||||
input.db_type === "sqlite"
|
||||
? "main"
|
||||
: (input.database ?? "postgres");
|
||||
|
||||
// Load initial data, smart-selecting the default schema (e.g. `public`)
|
||||
setSchemaTreeLoading(true);
|
||||
try {
|
||||
const databases = await cmd
|
||||
.getDatabases(connectionId)
|
||||
.catch(() => [] as string[]);
|
||||
const schemas = await cmd
|
||||
.getSchemas(connectionId)
|
||||
.catch(() => [] as string[]);
|
||||
const defaultSchema = pickDefaultSchema(schemas);
|
||||
const tables = await cmd.getTables(
|
||||
connectionId,
|
||||
defaultSchema ?? undefined,
|
||||
);
|
||||
populate(databases, schemas, tables);
|
||||
if (databases.length > 0) {
|
||||
// Prefer the connection's configured database, fall back to the first
|
||||
// available one so the dropdown matches what the pool is connected to.
|
||||
const preferred =
|
||||
input.database && databases.includes(input.database)
|
||||
? input.database
|
||||
: databases[0];
|
||||
setCurrentDatabase(preferred);
|
||||
}
|
||||
setCurrentSchema(defaultSchema);
|
||||
} finally {
|
||||
setSchemaTreeLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
setConnectionError(msg);
|
||||
notify(`Failed to connect: ${msg}`, "error");
|
||||
}
|
||||
}, [connectionId, populate, setCurrentDatabase, setCurrentSchema, setSchemaTreeLoading, notify]);
|
||||
|
||||
useEffect(() => {
|
||||
connect();
|
||||
return () => {
|
||||
cmd.dbDisconnect(connectionId).catch(() => {});
|
||||
const hasPending = useDbViewerStore
|
||||
.getState()
|
||||
.changesQueue.some((c) => c.status === "pending");
|
||||
if (!hasPending) reset();
|
||||
};
|
||||
}, [connectionId, connect, reset]);
|
||||
|
||||
// Database switch effect: whenever the selected database drifts from the
|
||||
// pool's live connection, reconnect and refresh schemas/tables for that DB.
|
||||
useEffect(() => {
|
||||
if (!currentDatabase || !inputRef.current) return;
|
||||
if (currentDatabase === connectedDbRef.current) return;
|
||||
|
||||
let cancelled = false;
|
||||
const reconnect = async () => {
|
||||
const input = { ...inputRef.current!, database: currentDatabase };
|
||||
try {
|
||||
setSchemaTreeLoading(true);
|
||||
await cmd.dbConnect(connectionId, input);
|
||||
if (cancelled) return;
|
||||
connectedDbRef.current = currentDatabase;
|
||||
const schemas = await cmd
|
||||
.getSchemas(connectionId)
|
||||
.catch(() => [] as string[]);
|
||||
if (cancelled) return;
|
||||
const newSchema = pickDefaultSchema(schemas);
|
||||
const tables = await cmd
|
||||
.getTables(connectionId, newSchema ?? undefined)
|
||||
.catch(() => [] as TableInfo[]);
|
||||
if (cancelled) return;
|
||||
populate(useDbViewerStore.getState().databases, schemas, tables);
|
||||
setCurrentSchema(newSchema);
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
// Revert the selection so the dropdown matches the live connection
|
||||
setCurrentDatabase(connectedDbRef.current);
|
||||
notify(`Failed to switch database: ${msg}`, "error");
|
||||
} finally {
|
||||
if (!cancelled) setSchemaTreeLoading(false);
|
||||
}
|
||||
};
|
||||
reconnect();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [currentDatabase, connectionId, populate, setCurrentSchema, setSchemaTreeLoading, notify]);
|
||||
|
||||
const submitPassword = useCallback(
|
||||
(pw: string) => {
|
||||
useConnectionStore.getState().setSessionPassword(connectionId, pw);
|
||||
setPasswordPromptOpen(false);
|
||||
void connect();
|
||||
},
|
||||
[connectionId, connect],
|
||||
);
|
||||
|
||||
const cancelPassword = useCallback(() => setPasswordPromptOpen(false), []);
|
||||
|
||||
return { connectionError, connect, passwordPromptOpen, submitPassword, cancelPassword };
|
||||
}
|
||||
Reference in New Issue
Block a user