import { useState, useRef, useCallback } from "react"; import { useLocation } from "react-router"; import { Dialog, DialogPanel, DialogTitle, } from "@headlessui/react"; import { PlayIcon, BookmarkIcon, SparklesIcon, TableCellsIcon, ChartBarIcon, XMarkIcon, ArrowPathIcon, PencilIcon, } from "@heroicons/react/24/outline"; import { formatBytes } from "../lib/format"; import { useResizeObserver } from "../hooks/effects"; import { type QueryResult, useInsightsQueryRunner, } from "../hooks/use-insights-query-runner"; type ResultView = "chart" | "table"; // ── Formatting helpers ── function formatNumber(n: number): string { return n.toLocaleString(); } // ── Chart component ── const BAR_COLORS = [ "rgba(90, 200, 168, 0.85)", "rgba(103, 178, 215, 0.85)", "rgba(181, 221, 239, 0.65)", "rgba(240, 164, 91, 0.75)", ]; function BarChart({ result }: { result: QueryResult }) { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); useResizeObserver(containerRef, (entries) => { const entry = entries[0]; if (entry) { setContainerWidth(entry.contentRect.width); } }); const labelCol = result.columns[0]; const valueCols = result.columns.slice(1).filter((col) => { const firstVal = result.rows[0]?.[col]; return typeof firstVal === "number"; }); if (valueCols.length === 0 || result.rows.length === 0) { return (
No numeric columns to chart
); } const valueCol = valueCols[0]; const maxVal = Math.max(...result.rows.map((r) => { const v = r[valueCol]; return typeof v === "number" ? v : 0; })); const chartHeight = 260; const yAxisWidth = 52; const padding = { top: 12, bottom: 48, right: 16 }; const plotHeight = chartHeight - padding.top - padding.bottom; const plotWidth = containerWidth - yAxisWidth - padding.right; const barCount = result.rows.length; const gap = Math.max(8, Math.min(16, plotWidth / barCount * 0.3)); const barWidth = Math.max(12, (plotWidth - gap * (barCount + 1)) / barCount); const tickCount = 5; const yTicks = Array.from({ length: tickCount }, (_, i) => Math.round((maxVal * (tickCount - 1 - i)) / (tickCount - 1)), ); return (
{containerWidth > 0 && ( {/* Y-axis gridlines + labels */} {yTicks.map((tick, i) => { const y = padding.top + (plotHeight * i) / (tickCount - 1); return ( {tick >= 1000 ? `${(tick / 1000).toFixed(tick >= 10000 ? 0 : 1)}k` : tick} ); })} {/* Bars */} {result.rows.map((row, i) => { const val = row[valueCol]; const numVal = typeof val === "number" ? val : 0; const barHeight = maxVal > 0 ? (numVal / maxVal) * plotHeight : 0; const x = yAxisWidth + gap + i * (barWidth + gap); const y = padding.top + plotHeight - barHeight; const label = String(row[labelCol]); const colorIndex = i % BAR_COLORS.length; const maxLabelLen = Math.floor(barWidth / 6); return ( {label}: {formatNumber(numVal)} {label.length > maxLabelLen ? label.slice(0, Math.max(3, maxLabelLen - 1)) + "\u2026" : label} ); })} )}
{valueCol.replace(/_/g, " ")}
); } // ── Table component ── function ResultTable({ result }: { result: QueryResult }) { return (
{result.columns.map((col) => ( ))} {result.rows.map((row) => ( {result.columns.map((col) => { const val = row[col]; const isNum = typeof val === "number"; return ( ); })} ))}
{col}
{isNum ? formatNumber(val) : String(val)}
); } // ── SQL Editor with line numbers ── function SqlEditor({ value, onChange, onRun, }: { value: string; onChange: (v: string) => void; onRun: () => void; }) { const textareaRef = useRef(null); const lineNumbersRef = useRef(null); const lineCount = value.split("\n").length; const syncScroll = useCallback(() => { if (textareaRef.current && lineNumbersRef.current) { lineNumbersRef.current.scrollTop = textareaRef.current.scrollTop; } }, []); const handleKeyDown = (e: React.KeyboardEvent) => { // Ctrl/Cmd + Enter to run if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); onRun(); return; } // Tab inserts spaces if (e.key === "Tab") { e.preventDefault(); const textarea = e.currentTarget; const start = textarea.selectionStart; const end = textarea.selectionEnd; const newValue = value.slice(0, start) + " " + value.slice(end); onChange(newValue); requestAnimationFrame(() => { textarea.selectionStart = start + 2; textarea.selectionEnd = start + 2; }); } }; return (
{/* Line numbers */} {/* Textarea */}