* 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
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
import { useCallback, useEffect, useRef } from "react";
|
|
import Editor, { type OnMount, type BeforeMount } from "@monaco-editor/react";
|
|
import * as monaco from "monaco-editor";
|
|
import { useSettingsStore } from "../../stores/settingsStore";
|
|
|
|
interface QueryEditorProps {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
onRun: () => void;
|
|
readOnly?: boolean;
|
|
}
|
|
|
|
export function QueryEditor({
|
|
value,
|
|
onChange,
|
|
onRun,
|
|
readOnly = false,
|
|
}: QueryEditorProps) {
|
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
|
|
const editorFontFamily = useSettingsStore(
|
|
(s) => s.settings?.editor_font_family ?? "Space Mono",
|
|
);
|
|
const editorFontSize = useSettingsStore(
|
|
(s) => s.settings?.editor_font_size ?? 13,
|
|
);
|
|
const editorWordWrap = useSettingsStore(
|
|
(s) => s.settings?.editor_word_wrap ?? "off",
|
|
);
|
|
const editorMinimap = useSettingsStore(
|
|
(s) => s.settings?.editor_minimap ?? false,
|
|
);
|
|
const editorTabSize = useSettingsStore(
|
|
(s) => s.settings?.editor_tab_size ?? 4,
|
|
);
|
|
|
|
useEffect(() => {
|
|
editorRef.current?.updateOptions?.({
|
|
fontFamily: editorFontFamily,
|
|
fontSize: editorFontSize,
|
|
wordWrap: editorWordWrap === "on" ? "on" : "off",
|
|
minimap: { enabled: editorMinimap },
|
|
tabSize: editorTabSize,
|
|
});
|
|
monaco.editor.remeasureFonts();
|
|
}, [editorFontFamily, editorFontSize, editorWordWrap, editorMinimap, editorTabSize]);
|
|
|
|
const handleMount: OnMount = useCallback(
|
|
(editor) => {
|
|
editorRef.current = editor;
|
|
editor.addAction({
|
|
id: "run-query",
|
|
label: "Run Query",
|
|
keybindings: [2048 | 3], // Cmd/Ctrl+Enter
|
|
run: () => onRun(),
|
|
});
|
|
editor.focus();
|
|
|
|
// Custom fonts (@fontsource Space Mono) load asynchronously. Monaco
|
|
// measures glyph widths at creation, so if the font lands after that the
|
|
// cursor/selection drift rightward the further along the line you are.
|
|
// Re-measure now (fonts may already be ready) and again once fonts load.
|
|
const reMeasure = () => monaco.editor.remeasureFonts();
|
|
reMeasure();
|
|
try {
|
|
void document.fonts?.load('13px "Space Mono"').then(() => {
|
|
requestAnimationFrame(reMeasure);
|
|
// WebKit can settle a frame late; re-measure once more to be safe
|
|
setTimeout(reMeasure, 200);
|
|
});
|
|
} catch {
|
|
// fonts API unavailable — nothing more we can do
|
|
}
|
|
},
|
|
[onRun],
|
|
);
|
|
|
|
// Transparent editor background so the app's canvas shows through
|
|
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
|
|
monaco.editor.defineTheme("gridline-sql", {
|
|
base: "vs-dark",
|
|
inherit: true,
|
|
rules: [],
|
|
colors: {
|
|
"editor.background": "#00000000",
|
|
"editorGutter.background": "#00000000",
|
|
"editor.lineHighlightBackground": "#ffffff08",
|
|
"editorLineNumber.foreground": "#5b5b5e",
|
|
"editorLineNumber.activeForeground": "#a1a1a6",
|
|
},
|
|
});
|
|
}, []);
|
|
|
|
return (
|
|
<div className="h-full min-h-0" data-testid="query-editor">
|
|
<Editor
|
|
height="100%"
|
|
language="sql"
|
|
theme="gridline-sql"
|
|
beforeMount={handleBeforeMount}
|
|
value={value}
|
|
onChange={(v) => onChange(v ?? "")}
|
|
onMount={handleMount}
|
|
options={{
|
|
minimap: { enabled: editorMinimap },
|
|
fontSize: editorFontSize,
|
|
fontFamily: editorFontFamily,
|
|
lineNumbers: "on",
|
|
scrollBeyondLastLine: false,
|
|
wordWrap: editorWordWrap === "on" ? "on" : "off",
|
|
readOnly,
|
|
placeholder: "Enter your SQL query…",
|
|
automaticLayout: true,
|
|
tabSize: editorTabSize,
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
} |