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 */}
{/* 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" && (
)}
);
}