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 { ExplainView } from "@/components/results/ExplainView"; import { useQueryExecution } from "@/hooks/use-query-execution"; import { useAddHistory } from "@/hooks/use-history"; import { useCompletionSchema } from "@/hooks/use-completion-schema"; import { useConnections } from "@/hooks/use-connections"; import { useAppStore } from "@/stores/app-store"; import { Button } from "@/components/ui/button"; import { Play, Loader2, Lock, BarChart3, Download, AlignLeft, Bookmark, Table2, Braces } from "lucide-react"; import { format as formatSql } from "sql-formatter"; import { SaveQueryDialog } from "@/components/saved-queries/SaveQueryDialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { exportCsv, exportJson } from "@/lib/tauri"; import { save } from "@tauri-apps/plugin-dialog"; import { toast } from "sonner"; import type { QueryResult, ExplainResult } 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 readOnlyMap = useAppStore((s) => s.readOnlyMap); const currentDatabase = useAppStore((s) => s.currentDatabase); const isReadOnly = readOnlyMap[connectionId] ?? true; const [sqlValue, setSqlValue] = useState(initialSql); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [explainData, setExplainData] = useState(null); const [resultView, setResultView] = useState<"results" | "explain">("results"); const [resultViewMode, setResultViewMode] = useState<"table" | "json">("table"); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const queryMutation = useQueryExecution(); const addHistoryMutation = useAddHistory(); const { data: connections } = useConnections(); const { data: completionSchema } = useCompletionSchema(connectionId); const connName = connections?.find((c) => c.id === connectionId)?.name ?? "unknown"; const handleChange = useCallback( (val: string) => { setSqlValue(val); onSqlChange?.(val); }, [onSqlChange] ); const recordHistory = useCallback( ( sql: string, status: "success" | "error", executionTimeMs: number, rowCount?: number, errorMessage?: string ) => { addHistoryMutation.mutate({ id: crypto.randomUUID(), connection_id: connectionId, connection_name: connName, database: currentDatabase ?? "", sql, status, error_message: errorMessage, row_count: rowCount, execution_time_ms: executionTimeMs, executed_at: new Date().toISOString(), }); }, [addHistoryMutation, connectionId, connName, currentDatabase] ); const handleExecute = useCallback(() => { if (!sqlValue.trim() || !connectionId) return; setError(null); setExplainData(null); setResultView("results"); queryMutation.mutate( { connectionId, sql: sqlValue }, { onSuccess: (data) => { setResult(data); setError(null); onResult?.(data, null); recordHistory( sqlValue, "success", data.execution_time_ms, data.row_count ); }, onError: (err) => { setResult(null); setError(String(err)); onResult?.(null, String(err)); recordHistory(sqlValue, "error", 0, undefined, String(err)); }, } ); }, [connectionId, sqlValue, queryMutation, onResult, recordHistory]); const handleExplain = useCallback(() => { if (!sqlValue.trim() || !connectionId) return; setError(null); const explainSql = `EXPLAIN (ANALYZE, COSTS, BUFFERS, FORMAT JSON) ${sqlValue}`; queryMutation.mutate( { connectionId, sql: explainSql }, { onSuccess: (data) => { try { const raw = data.rows[0]?.[0]; const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; const plan: ExplainResult = Array.isArray(parsed) ? parsed[0] : parsed; setExplainData(plan); setResultView("explain"); setResult(null); setError(null); } catch { setError("Failed to parse EXPLAIN output"); setExplainData(null); } }, onError: (err) => { setResult(null); setError(String(err)); setExplainData(null); }, } ); }, [connectionId, sqlValue, queryMutation]); const handleFormat = useCallback(() => { if (!sqlValue.trim()) return; try { const formatted = formatSql(sqlValue, { language: "postgresql" }); setSqlValue(formatted); onSqlChange?.(formatted); } catch { // Silently ignore format errors on invalid SQL } }, [sqlValue, onSqlChange]); const handleExport = useCallback( async (format: "csv" | "json") => { if (!result || result.columns.length === 0) return; const ext = format === "csv" ? "csv" : "json"; const path = await save({ title: `Export as ${ext.toUpperCase()}`, defaultPath: `query_result.${ext}`, filters: [{ name: ext.toUpperCase(), extensions: [ext] }], }); if (!path) return; try { if (format === "csv") { await exportCsv(path, result.columns, result.rows as unknown[][]); } else { await exportJson(path, result.columns, result.rows as unknown[][]); } toast.success(`Exported to ${path}`); } catch (err) { toast.error("Export failed", { description: String(err) }); } }, [result] ); return ( <>
{result && result.columns.length > 0 && ( handleExport("csv")}> Export CSV handleExport("json")}> Export JSON )} Ctrl+Enter to execute {isReadOnly && ( Read-Only )}
{(explainData || result || error) && (
{explainData && ( )} {resultView === "results" && result && result.columns.length > 0 && (
)}
)} {resultView === "explain" && explainData ? ( ) : ( )}
); }