import { useState, useRef, useCallback, useEffect } from "react"; import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels"; import { Play, Loader2, BookOpen, Zap, Database } from "lucide-react"; import { toast } from "sonner"; import { Link } from "wouter"; import { api, type LintResult, type ParseResult, type FormatResult, type InjectionResult, type HealthResult } from "@/lib/api"; import { SqlEditor } from "@/components/SqlEditor"; import { AstTreeView } from "@/components/AstTreeView"; import { LintPanel } from "@/components/LintPanel"; import { InjectionPanel } from "@/components/InjectionPanel"; import { FormatterPanel } from "@/components/FormatterPanel"; // ── Constants ────────────────────────────────────────────────────────────── type AnalysisTab = "ast" | "lint" | "injection" | "formatted"; const TABS: { id: AnalysisTab; label: string }[] = [ { id: "ast", label: "AST Tree" }, { id: "lint", label: "Lint" }, { id: "injection", label: "Injection" }, { id: "formatted", label: "Formatted" }, ]; const DIALECTS = [ { value: "ansi", label: "ANSI SQL" }, { value: "postgres", label: "PostgreSQL" }, { value: "mysql", label: "MySQL" }, { value: "tsql", label: "T-SQL" }, { value: "sqlite", label: "SQLite" }, { value: "bigquery", label: "BigQuery" }, { value: "snowflake", label: "Snowflake" }, { value: "redshift", label: "Redshift" }, { value: "duckdb", label: "DuckDB" }, { value: "hive", label: "Hive" }, { value: "sparksql", label: "Spark SQL" }, { value: "trino", label: "Trino" }, { value: "databricks", label: "Databricks" }, { value: "oracle", label: "Oracle" }, { value: "teradata", label: "Teradata" }, { value: "clickhouse", label: "ClickHouse" }, { value: "athena", label: "Athena" }, ]; const DEFAULT_SQL = `-- SQL Analyzer Demo -- Press Ctrl+Enter (or Cmd+Enter) to analyze SELECT u.id, u.name, u.email, o.total, o.created_at FROM users u INNER JOIN orders o ON u.id = o.user_id WHERE u.active = 1 AND o.created_at >= '2024-01-01' ORDER BY o.created_at DESC LIMIT 50;`; const INJECTION_SQL = `-- SQL Injection Demo -- This query contains several injection patterns SELECT * FROM users WHERE username = 'admin' OR 1=1 -- AND password = 'anything'; -- Stacked query attempt SELECT * FROM products WHERE id = 1; DROP TABLE users; -- UNION-based exfiltration SELECT id, name FROM products UNION SELECT username, password FROM users;`; // ── Status indicator ─────────────────────────────────────────────────────── function StatusDot({ health }: { health: HealthResult | null }) { const isOk = health?.status === "ok"; return (
{isOk ? `SQLFluff ${health!.version}` : "starting…"}
); } // ── Main page ────────────────────────────────────────────────────────────── export default function Analyzer() { const [sql, setSql] = useState(DEFAULT_SQL); const [dialect, setDialect] = useState("ansi"); const [activeTab, setActiveTab] = useState("ast"); const [isAnalyzing, setIsAnalyzing] = useState(false); const [health, setHealth] = useState(null); const [lintResult, setLintResult] = useState(null); const [parseResult, setParseResult] = useState(null); const [formatResult, setFormatResult] = useState(null); const [injectResult, setInjectResult] = useState(null); const [analysisTime, setAnalysisTime] = useState(null); const editorContainerRef = useRef(null); // Poll health endpoint every 6 s until ready useEffect(() => { let cancelled = false; async function poll() { try { const h = await api.health(); if (!cancelled) setHealth(h); } catch { // still starting } } poll(); const id = setInterval(poll, 6000); return () => { cancelled = true; clearInterval(id); }; }, []); // ── Analyze ──────────────────────────────────────────────────────────── const handleAnalyze = useCallback(async () => { if (!sql.trim()) { toast.error("Please enter some SQL to analyze"); return; } if (health?.status !== "ok") { toast.error("API is not ready yet — please wait a moment"); return; } setIsAnalyzing(true); const start = Date.now(); try { const [lint, parse, format, inject] = await Promise.allSettled([ api.lint(sql, dialect), api.parse(sql, dialect), api.format(sql, dialect), api.inject(sql, dialect), ]); if (lint.status === "fulfilled") setLintResult(lint.value); if (parse.status === "fulfilled") setParseResult(parse.value); if (format.status === "fulfilled") setFormatResult(format.value); if (inject.status === "fulfilled") setInjectResult(inject.value); setAnalysisTime(Date.now() - start); const lv = lint.status === "fulfilled" ? lint.value : null; const iv = inject.status === "fulfilled" ? inject.value : null; if (lv && iv) { if (!lv.passed || !iv.safe) { toast.warning( `Analysis complete — ${lv.stats.total} lint issue${lv.stats.total !== 1 ? "s" : ""}${!iv.safe ? `, risk score ${iv.risk_score}/100` : ""}`, { duration: 4000 } ); } else { toast.success("Analysis complete — no issues found", { duration: 3000 }); } } } catch (err) { toast.error((err instanceof Error ? err.message : "Analysis failed").slice(0, 120)); } finally { setIsAnalyzing(false); } }, [sql, dialect, health]); // ── Jump to line ─────────────────────────────────────────────────────── const jumpToLine = useCallback((lineNo: number) => { const container = editorContainerRef.current; if (!container) return; const editorEl = container.querySelector("[data-editor-container]"); if (!editorEl) return; editorEl.dispatchEvent(new CustomEvent("jump-to-line", { detail: lineNo })); }, []); const lintBadge = lintResult && !lintResult.passed ? lintResult.stats.total : null; const injectBadge = injectResult && !injectResult.safe ? injectResult.patterns.length : null; // ── Render ───────────────────────────────────────────────────────────── return (
{/* Header */}
{/* Logo */}
SQL Analyzer
{/* Dialect selector */} {/* Example loader */}
{analysisTime !== null && ( {analysisTime}ms )} {/* API Docs link */} {/* Analyze button */}
{/* Split panels */}
{/* Left: SQL Editor */}
SQL Editor Ctrl+Enter to analyze
{/* Resize handle */} {/* Right: Results */}
{/* Tab bar */}
{TABS.map((tab) => { const badge = tab.id === "lint" ? lintBadge : tab.id === "injection" ? injectBadge : null; const isActive = activeTab === tab.id; return ( ); })}
{/* Tab content */}
{activeTab === "ast" && ( )} {activeTab === "lint" && ( )} {activeTab === "injection" && ( )} {activeTab === "formatted" && ( )}
); }