CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
sql-editor.tsx257 linesDownload Raw Back to database-manager
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