Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect, useCallback } from 'react';4import MonacoEditor from '@monaco-editor/react';5import { Play, Loader2, AlertCircle, CheckCircle2, History, X } from 'lucide-react';6import { Button } from '@/components/ui/button';7import { useTheme } from 'next-themes';8import { cn } from '@/lib/utils';9 10interface SqlEditorProps {11 deploymentId?: string;12 queryEndpoint?: string;13 workspaceId?: string;14}15 16interface QueryResult {17 success: boolean;18 columns?: string[];19 rows?: unknown[][];20 rowsAffected?: number;21 error?: string;22 executionTime?: number;23}24 25const HISTORY_KEY = 'osw-sql-history';26const MAX_HISTORY = 20;27 28export function SqlEditor({ deploymentId, queryEndpoint, workspaceId }: SqlEditorProps) {29 const apiBase = workspaceId ? `/api/w/${workspaceId}` : '/api';30 const [sql, setSql] = useState('SELECT * FROM ');31 const [executing, setExecuting] = useState(false);32 const [result, setResult] = useState<QueryResult | null>(null);33 const [history, setHistory] = useState<string[]>([]);34 const [showHistory, setShowHistory] = useState(false);35 const { resolvedTheme } = useTheme();36 const [mounted, setMounted] = useState(false);37 38 useEffect(() => {39 setMounted(true);40 // Load history from localStorage41 const savedHistory = localStorage.getItem(HISTORY_KEY);42 if (savedHistory) {43 try {44 setHistory(JSON.parse(savedHistory));45 } catch {46 // Ignore invalid JSON47 }48 }49 }, []);50 51 const saveToHistory = useCallback((query: string) => {52 setHistory(prev => {53 const newHistory = [query, ...prev.filter(q => q !== query)].slice(0, MAX_HISTORY);54 localStorage.setItem(HISTORY_KEY, JSON.stringify(newHistory));55 return newHistory;56 });57 }, []);58 59 const executeQuery = useCallback(async () => {60 if (!sql.trim()) return;61 62 setExecuting(true);63 setResult(null);64 const startTime = Date.now();65 66 try {67 const endpoint = queryEndpoint || `${apiBase}/admin/deployments/${deploymentId}/database/query`;68 const res = await fetch(endpoint, {69 method: 'POST',70 headers: { 'Content-Type': 'application/json' },71 body: JSON.stringify({ sql: sql.trim() }),72 });73 74 const data = await res.json();75 const executionTime = Date.now() - startTime;76 77 if (!res.ok) {78 setResult({79 success: false,80 error: data.error || 'Query failed',81 executionTime,82 });83 } else {84 setResult({85 success: true,86 columns: data.columns,87 rows: data.rows,88 rowsAffected: data.rowsAffected,89 executionTime,90 });91 saveToHistory(sql.trim());92 }93 } catch (err) {94 setResult({95 success: false,96 error: err instanceof Error ? err.message : 'Query failed',97 executionTime: Date.now() - startTime,98 });99 } finally {100 setExecuting(false);101 }102 }, [sql, deploymentId, saveToHistory]);103 104 // Handle keyboard shortcut105 const handleEditorMount = useCallback((editor: unknown) => {106 const monacoEditor = editor as { addCommand: (keybinding: number, handler: () => void) => void };107 // Cmd/Ctrl + Enter to execute108 monacoEditor.addCommand(2048 | 3, () => { // KeyMod.CtrlCmd | KeyCode.Enter109 executeQuery();110 });111 }, [executeQuery]);112 113 if (!mounted) {114 return <div className="h-full flex items-center justify-center">115 <Loader2 className="h-6 w-6 animate-spin" />116 </div>;117 }118 119 return (120 <div className="h-full flex flex-col gap-4">121 {/* Editor Section */}122 <div className="flex flex-col gap-2">123 <div className="flex items-center justify-between">124 <div className="flex items-center gap-2">125 <Button126 onClick={executeQuery}127 disabled={executing || !sql.trim()}128 size="sm"129 >130 {executing ? (131 <Loader2 className="h-4 w-4 animate-spin mr-1" />132 ) : (133 <Play className="h-4 w-4 mr-1" />134 )}135 Execute136 </Button>137 <span className="text-xs text-muted-foreground">138 Ctrl/Cmd + Enter139 </span>140 </div>141 <Button142 variant="ghost"143 size="sm"144 onClick={() => setShowHistory(!showHistory)}145 >146 <History className="h-4 w-4 mr-1" />147 History148 </Button>149 </div>150 151 {/* History dropdown */}152 {showHistory && history.length > 0 && (153 <div className="border rounded-lg bg-background shadow-lg max-h-40 overflow-auto">154 {history.map((query, i) => (155 <button156 key={i}157 onClick={() => {158 setSql(query);159 setShowHistory(false);160 }}161 className="w-full text-left px-3 py-2 text-sm font-mono hover:bg-muted border-b last:border-0 truncate"162 >163 {query}164 </button>165 ))}166 </div>167 )}168 169 <div className="h-32 border rounded-lg overflow-hidden">170 <MonacoEditor171 language="sql"172 theme={resolvedTheme === 'dark' ? 'vs-dark' : 'light'}173 value={sql}174 onChange={value => setSql(value || '')}175 onMount={handleEditorMount}176 options={{177 minimap: { enabled: false },178 fontSize: 13,179 lineNumbers: 'off',180 folding: false,181 scrollBeyondLastLine: false,182 wordWrap: 'on',183 automaticLayout: true,184 }}185 />186 </div>187 </div>188 189 {/* Results Section */}190 <div className="flex-1 overflow-hidden border rounded-lg">191 {result === null ? (192 <div className="h-full flex items-center justify-center text-muted-foreground text-sm">193 Execute a query to see results194 </div>195 ) : result.success ? (196 <div className="h-full flex flex-col">197 {/* Status bar */}198 <div className="flex items-center gap-2 px-3 py-2 bg-muted/30 border-b text-sm">199 <CheckCircle2 className="h-4 w-4 text-green-500" />200 {result.rows && result.rows.length > 0 ? (201 <span>{result.rows.length} row{result.rows.length !== 1 ? 's' : ''}</span>202 ) : result.rowsAffected !== undefined && result.rowsAffected > 0 ? (203 <span>{result.rowsAffected} row{result.rowsAffected !== 1 ? 's' : ''} affected</span>204 ) : (205 <span>Query executed successfully</span>206 )}207 <span className="text-muted-foreground">({result.executionTime}ms)</span>208 </div>209 210 {/* Results table */}211 {result.columns && result.columns.length > 0 && result.rows ? (212 <div className="flex-1 overflow-auto">213 <table className="w-full text-sm">214 <thead className="sticky top-0 bg-muted">215 <tr>216 {result.columns.map((col, i) => (217 <th key={i} className="text-left p-2 font-medium border-r last:border-0">218 {col}219 </th>220 ))}221 </tr>222 </thead>223 <tbody>224 {result.rows.map((row, i) => (225 <tr key={i} className="border-t hover:bg-muted/30">226 {row.map((cell, j) => (227 <td key={j} className="p-2 font-mono text-xs border-r last:border-0 max-w-xs truncate">228 {cell === null ? (229 <span className="text-muted-foreground italic">NULL</span>230 ) : typeof cell === 'object' ? (231 JSON.stringify(cell)232 ) : (233 String(cell)234 )}235 </td>236 ))}237 </tr>238 ))}239 </tbody>240 </table>241 </div>242 ) : null}243 </div>244 ) : (245 <div className="h-full flex flex-col items-center justify-center gap-2 p-4">246 <AlertCircle className="h-6 w-6 text-destructive" />247 <p className="text-sm text-destructive font-medium">Query Error</p>248 <p className="text-sm text-muted-foreground text-center max-w-md">249 {result.error}250 </p>251 </div>252 )}253 </div>254 </div>255 );256}257 