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:
@@ -1,23 +1,43 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import { ConfirmDialog } from "../ui/ConfirmDialog";
|
||||
import { ImportDialog } from "./ImportDialog";
|
||||
import { useDbViewerStore } from "../../stores/dbViewerStore";
|
||||
import { useUiStore } from "../../stores/uiStore";
|
||||
import { exportData } from "../../lib/exportData";
|
||||
import * as cmd from "../../lib/commands";
|
||||
import type { ColumnInfo } from "../../lib/types";
|
||||
|
||||
interface TableOverflowMenuProps {
|
||||
schema: string;
|
||||
table: string;
|
||||
onOpenTab: (schema: string, table: string, forceNew?: boolean) => string;
|
||||
connectionId?: string;
|
||||
columns?: ColumnInfo[];
|
||||
rows?: unknown[][];
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
id: string;
|
||||
label: string;
|
||||
stub?: boolean;
|
||||
danger?: boolean;
|
||||
}
|
||||
|
||||
export function TableOverflowMenu({ schema, table, onOpenTab }: TableOverflowMenuProps) {
|
||||
export function TableOverflowMenu({
|
||||
schema,
|
||||
table,
|
||||
onOpenTab,
|
||||
connectionId: connectionIdProp,
|
||||
columns,
|
||||
rows,
|
||||
}: TableOverflowMenuProps) {
|
||||
const storeConnectionId = useUiStore((s) => s.activeConnectionId);
|
||||
const connectionId = connectionIdProp ?? storeConnectionId;
|
||||
const addChange = useDbViewerStore((s) => s.addChange);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [confirmAction, setConfirmAction] = useState<"empty" | "delete" | null>(null);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -40,20 +60,40 @@ export function TableOverflowMenu({ schema, table, onOpenTab }: TableOverflowMen
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const handleAction = (id: string) => {
|
||||
const handleAction = async (id: string) => {
|
||||
switch (id) {
|
||||
case "open":
|
||||
onOpenTab(schema, table, true);
|
||||
setOpen(false);
|
||||
break;
|
||||
case "copy-schema": {
|
||||
const sql = `-- Schema for ${schema}.${table}\n-- TODO: fetch schema DDL`;
|
||||
if (navigator.clipboard) {
|
||||
void navigator.clipboard.writeText(sql);
|
||||
if (!connectionId) break;
|
||||
try {
|
||||
const ddl = await cmd.getTableDdl(connectionId, schema, table);
|
||||
if (navigator.clipboard) {
|
||||
void navigator.clipboard.writeText(ddl);
|
||||
}
|
||||
} catch {
|
||||
/* ignore copy failures */
|
||||
}
|
||||
setOpen(false);
|
||||
break;
|
||||
}
|
||||
case "export-csv":
|
||||
case "export-json":
|
||||
case "export-sql":
|
||||
case "export-md": {
|
||||
const format = id.replace("export-", "");
|
||||
if (rows && rows.length > 0 && columns && columns.length > 0) {
|
||||
exportData(rows, columns, format, `${schema}.${table}`);
|
||||
}
|
||||
setOpen(false);
|
||||
break;
|
||||
}
|
||||
case "import":
|
||||
setImportOpen(true);
|
||||
setOpen(false);
|
||||
break;
|
||||
case "empty":
|
||||
setConfirmAction("empty");
|
||||
setOpen(false);
|
||||
@@ -70,9 +110,11 @@ export function TableOverflowMenu({ schema, table, onOpenTab }: TableOverflowMen
|
||||
const items: MenuItem[] = [
|
||||
{ id: "open", label: "Open in new tab" },
|
||||
{ id: "copy-schema", label: "Copy table schema" },
|
||||
{ id: "export-csv", label: "Export data (CSV)", stub: true },
|
||||
{ id: "export-json", label: "Export data (JSON)", stub: true },
|
||||
{ id: "export-sql", label: "Export data (SQL)", stub: true },
|
||||
{ id: "export-csv", label: "Export data (CSV)" },
|
||||
{ id: "export-json", label: "Export data (JSON)" },
|
||||
{ id: "export-sql", label: "Export data (SQL)" },
|
||||
{ id: "export-md", label: "Export data (Markdown)" },
|
||||
{ id: "import", label: "Import data (CSV/JSON)" },
|
||||
{ id: "empty", label: "Empty Table", danger: true },
|
||||
{ id: "delete", label: "Delete Table", danger: true },
|
||||
];
|
||||
@@ -93,19 +135,12 @@ export function TableOverflowMenu({ schema, table, onOpenTab }: TableOverflowMen
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => handleAction(item.id)}
|
||||
disabled={item.stub}
|
||||
className={[
|
||||
"flex items-center justify-between px-3 py-2 text-sm w-full text-left transition-colors cursor-pointer",
|
||||
item.danger ? "text-error hover:bg-error/10" : "text-text-muted hover:text-text hover:bg-surface-raised",
|
||||
item.stub ? "opacity-50 cursor-not-allowed" : "",
|
||||
item.danger ? "text-red-400 hover:bg-red-500/10 hover:text-red-300" : "text-text-muted hover:text-text hover:bg-surface-raised",
|
||||
].join(" ")}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
{item.stub && (
|
||||
<span className="ml-2 text-[10px] px-1.5 py-0.5 rounded-full bg-surface-raised text-text-subtle">
|
||||
Soon
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -118,6 +153,12 @@ export function TableOverflowMenu({ schema, table, onOpenTab }: TableOverflowMen
|
||||
message={`Are you sure you want to delete ALL rows from "${schema}"."${table}"? This action cannot be undone.`}
|
||||
confirmLabel="Empty Table"
|
||||
onConfirm={() => {
|
||||
addChange({
|
||||
type: "empty_table",
|
||||
schema,
|
||||
table,
|
||||
description: `Empty Table: ${schema}.${table}`,
|
||||
});
|
||||
setConfirmAction(null);
|
||||
}}
|
||||
onCancel={() => setConfirmAction(null)}
|
||||
@@ -130,11 +171,36 @@ export function TableOverflowMenu({ schema, table, onOpenTab }: TableOverflowMen
|
||||
message={`Are you sure you want to permanently delete "${schema}"."${table}"? All data will be lost.`}
|
||||
confirmLabel="Delete Table"
|
||||
onConfirm={() => {
|
||||
addChange({
|
||||
type: "drop_table",
|
||||
schema,
|
||||
table,
|
||||
description: `Drop Table: ${schema}.${table}`,
|
||||
});
|
||||
setConfirmAction(null);
|
||||
}}
|
||||
onCancel={() => setConfirmAction(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ImportDialog
|
||||
open={importOpen}
|
||||
schema={schema}
|
||||
table={table}
|
||||
columns={columns?.map((c) => c.name) ?? []}
|
||||
onStage={(change) => {
|
||||
addChange({
|
||||
type: "bulk_insert",
|
||||
schema: change.schema,
|
||||
table: change.table,
|
||||
columns: change.columns,
|
||||
rows: change.rows,
|
||||
description: change.description,
|
||||
});
|
||||
setImportOpen(false);
|
||||
}}
|
||||
onClose={() => setImportOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user