Editor settings, SSH/SSL runtime, data import + table-menu loose ends (#7)

* feat: editor settings model + typed clamped defaults (Task 1)

* feat: carry SSH config + ssh_password to backend DbConfig (Task 2)

* feat: Change enum bulk/drop/empty + type-specific change payload builder (Task 3)

* feat: csv parser + shared export util (Task 4)

* feat: rustls TLS connector factory with modes + client auth (Task 5)

* feat: real SSH tunnel manager (testable backend) + pool eviction hook (Task 6)

* feat: table DDL fetch (sqlite + pg_dump arg builder) (Task 7)

* feat: keychain SSH secrets + connection delete purge + tunnel lifecycle (Task 8)

* feat: real SSH tunnel + TLS connect path for postgres/mysql (Task 9)

* feat: execute_change bulk/drop/empty + get_table_ddl command (Task 10)

* feat: fetch SSH secrets into dbConnect + save on connection form (Task 11)

* feat: Editor settings tab UI (Task 12)

* feat: QueryEditor applies editor settings live (Task 13)

* feat: ImportDialog with CSV/JSON preview + column mapping (Task 14)

* feat: table-menu export/empty/delete/import + queue labels + payload builder (Task 15)

* fix: error sanitization, encrypted-key guard, row-indexed import errors, caps (Task 16)

* docs: mark Editor Settings, SSH/SSL runtime, Data Import, table-menu loose ends shipped (Task 17)

* feat: auto-refresh schema tree after schema-modifying SQL (query + queue drop)

* fix: use theme-consistent red classes for danger menu items (text-error was undefined)

* feat: changes queue as tab-bar popover + amber pending border

* feat: redesign changes popover (visual/SQL toggle, cards, footer actions, Cmd+S)

* refactor: drop per-card status label from changes popover cards

* feat: green completion indicator on committed cards + auto-close tabs of dropped tables

* docs: changes queue popover UX + auto schema refresh statuses
This commit is contained in:
2026-08-02 20:38:23 +08:00
committed by GitHub
parent e32fe7967c
commit e0c0db8352
68 changed files with 3885 additions and 553 deletions
+90
View File
@@ -0,0 +1,90 @@
import type { QueueItem } from "../stores/dbViewerStore";
/**
* Payload emitted for a single queued change, keyed to match the Rust
* `Change` enum variants (snake_case). Sent to `executeChange`.
*/
export type ChangePayload = Record<string, unknown>;
function j(v: unknown): string {
return JSON.stringify(v ?? {});
}
function q(s: string): string {
return `"${s.replace(/"/g, '""')}"`;
}
function lit(v: unknown): string {
if (v === null || v === undefined) return "NULL";
if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
if (typeof v === "number") return String(v);
if (typeof v === "string") return `'${v.replace(/'/g, "''")}'`;
return `'${JSON.stringify(v).replace(/'/g, "''")}'`;
}
function tableRef(schema: string | undefined, table: string | undefined): string {
if (!table) return "-";
return schema ? `${q(schema)}.${q(table)}` : q(table);
}
export function buildChangeSql(item: QueueItem): string {
const t = tableRef(item.schema, item.table);
switch (item.type) {
case "insert": {
const data = item.newData ?? {};
const cols = Object.keys(data);
return `INSERT INTO ${t} (${cols.map(q).join(", ")}) VALUES (${cols.map((c) => lit(data[c])).join(", ")})`;
}
case "update": {
const data = item.newData ?? {};
const pk = item.primaryKey ?? {};
const setClause = Object.keys(data).map((c) => `${q(c)} = ${lit(data[c])}`).join(", ");
const whereClause = Object.keys(pk).map((c) => `${q(c)} = ${lit(pk[c])}`).join(" AND ");
return `UPDATE ${t} SET ${setClause} WHERE ${whereClause}`;
}
case "delete": {
const pk = item.primaryKey ?? {};
const whereClause = Object.keys(pk).map((c) => `${q(c)} = ${lit(pk[c])}`).join(" AND ");
return `DELETE FROM ${t} WHERE ${whereClause}`;
}
case "bulk_insert": {
const cols = item.columns ?? [];
const rows = item.rows ?? [];
const valueRows = rows
.map((row) => `(${row.map(lit).join(", ")})`)
.join(", ");
return `INSERT INTO ${t} (${cols.map(q).join(", ")}) VALUES ${valueRows}`;
}
case "empty_table":
return `DELETE FROM ${t}`;
case "drop_table":
return `DROP TABLE ${t}`;
default:
return item.sql ?? "";
}
}
export function buildChangePayload(item: QueueItem): ChangePayload {
const schema = item.schema ?? "";
const table = item.table ?? "";
switch (item.type) {
case "insert":
return { id: item.id, type: "insert", schema, table, data: j(item.newData) };
case "update":
return { id: item.id, type: "update", schema, table,
primary_key: j(item.primaryKey), old_data: j(item.oldData), new_data: j(item.newData) };
case "delete":
return { id: item.id, type: "delete", schema, table, primary_key: j(item.primaryKey) };
case "alter_table":
return { id: item.id, type: "alter_table", schema, table, sql: item.sql, rollback_sql: "" };
case "bulk_insert":
return { id: item.id, type: "bulk_insert", schema, table,
columns: item.columns ?? [], rows: item.rows ?? [] };
case "drop_table":
return { id: item.id, type: "drop_table", schema, table };
case "empty_table":
return { id: item.id, type: "empty_table", schema, table };
default:
return { id: item.id, type: item.type, sql: item.sql };
}
}