feat: add SQL editor, query results, and workspace panels

Add CodeMirror 6 SQL editor with Ctrl+Enter execution, ResultsTable
with virtual scrolling and resizable columns, ResultsPanel, EditableCell,
WorkspacePanel (editor + results split), and TabContent router.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 19:06:48 +03:00
parent d333732346
commit 13a8535b5c
6 changed files with 520 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
import { useState, useCallback } from "react";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { SqlEditor } from "@/components/editor/SqlEditor";
import { ResultsPanel } from "@/components/results/ResultsPanel";
import { useQueryExecution } from "@/hooks/use-query-execution";
import { Button } from "@/components/ui/button";
import { Play, Loader2 } from "lucide-react";
import type { QueryResult } from "@/types";
interface Props {
connectionId: string;
initialSql?: string;
onSqlChange?: (sql: string) => void;
onResult?: (result: QueryResult | null, error: string | null) => void;
}
export function WorkspacePanel({
connectionId,
initialSql = "",
onSqlChange,
onResult,
}: Props) {
const [sqlValue, setSqlValue] = useState(initialSql);
const [result, setResult] = useState<QueryResult | null>(null);
const [error, setError] = useState<string | null>(null);
const queryMutation = useQueryExecution();
const handleChange = useCallback(
(val: string) => {
setSqlValue(val);
onSqlChange?.(val);
},
[onSqlChange]
);
const handleExecute = useCallback(() => {
if (!sqlValue.trim() || !connectionId) return;
setError(null);
queryMutation.mutate(
{ connectionId, sql: sqlValue },
{
onSuccess: (data) => {
setResult(data);
setError(null);
onResult?.(data, null);
},
onError: (err) => {
setResult(null);
setError(String(err));
onResult?.(null, String(err));
},
}
);
}, [connectionId, sqlValue, queryMutation, onResult]);
return (
<ResizablePanelGroup orientation="vertical">
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
<div className="flex h-full flex-col">
<div className="flex items-center gap-2 border-b px-2 py-1">
<Button
size="sm"
variant="ghost"
className="h-6 gap-1 text-xs"
onClick={handleExecute}
disabled={queryMutation.isPending || !sqlValue.trim()}
>
{queryMutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Play className="h-3 w-3" />
)}
Run
</Button>
<span className="text-[11px] text-muted-foreground">
Ctrl+Enter to execute
</span>
</div>
<div className="flex-1 overflow-hidden">
<SqlEditor
value={sqlValue}
onChange={handleChange}
onExecute={handleExecute}
/>
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel id="results" defaultSize="60%" minSize="15%">
<ResultsPanel
result={result}
error={error}
isLoading={queryMutation.isPending}
/>
</ResizablePanel>
</ResizablePanelGroup>
);
}