diff --git a/src/components/results/ResultsJsonView.tsx b/src/components/results/ResultsJsonView.tsx new file mode 100644 index 0000000..9c37c6d --- /dev/null +++ b/src/components/results/ResultsJsonView.tsx @@ -0,0 +1,60 @@ +import { useMemo } from "react"; + +interface Props { + columns: string[]; + rows: unknown[][]; +} + +function syntaxHighlight(json: string): string { + return json.replace( + /("(\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g, + (match) => { + let cls = "text-blue-500 dark:text-blue-400"; // number + if (match.startsWith('"')) { + if (match.endsWith(":")) { + cls = "text-foreground"; // key + } else { + cls = "text-green-600 dark:text-green-400"; // string + } + } else if (/true|false/.test(match)) { + cls = "text-purple-500 dark:text-purple-400"; // boolean + } else if (match === "null") { + cls = "text-muted-foreground italic"; // null + } + return `${match}`; + } + ); +} + +export function ResultsJsonView({ columns, rows }: Props) { + const jsonData = useMemo(() => { + return rows.map((row) => { + const obj: Record = {}; + columns.forEach((col, i) => { + obj[col] = row[i]; + }); + return obj; + }); + }, [columns, rows]); + + const highlighted = useMemo( + () => syntaxHighlight(JSON.stringify(jsonData, null, 2)), + [jsonData] + ); + + return ( + + + + {rows.length} row{rows.length !== 1 ? "s" : ""} + + + + + + + ); +} diff --git a/src/components/results/ResultsPanel.tsx b/src/components/results/ResultsPanel.tsx index 9566b15..bed3c93 100644 --- a/src/components/results/ResultsPanel.tsx +++ b/src/components/results/ResultsPanel.tsx @@ -1,4 +1,5 @@ import { ResultsTable } from "./ResultsTable"; +import { ResultsJsonView } from "./ResultsJsonView"; import type { QueryResult } from "@/types"; import { Loader2, AlertCircle } from "lucide-react"; @@ -6,6 +7,7 @@ interface Props { result?: QueryResult | null; error?: string | null; isLoading?: boolean; + viewMode?: "table" | "json"; onCellDoubleClick?: ( rowIndex: number, colIndex: number, @@ -18,6 +20,7 @@ export function ResultsPanel({ result, error, isLoading, + viewMode = "table", onCellDoubleClick, highlightedCells, }: Props) { @@ -57,6 +60,15 @@ export function ResultsPanel({ ); } + if (viewMode === "json") { + return ( + + ); + } + return ( (new Map()); const [isSaving, setIsSaving] = useState(false); const [insertDialogOpen, setInsertDialogOpen] = useState(false); + const [viewMode, setViewMode] = useState<"table" | "json">("table"); const queryClient = useQueryClient(); const { data, isLoading, error } = useTableData({ @@ -197,26 +199,54 @@ export function TableDataView({ connectionId, schema, table }: Props) { Apply {data && data.columns.length > 0 && ( - - - + + + + + Export + + + + handleExport("csv")}> + Export CSV + + handleExport("json")}> + Export JSON + + + + + setViewMode("table")} + title="Table view" > - - Export - - - - handleExport("csv")}> - Export CSV - - handleExport("json")}> - Export JSON - - - + + Table + + setViewMode("json")} + title="JSON view" + > + + JSON + + + > )} {!isReadOnly && ( ) : data ? ( - + viewMode === "json" ? ( + + ) : ( + + ) ) : null} diff --git a/src/components/workspace/WorkspacePanel.tsx b/src/components/workspace/WorkspacePanel.tsx index 8e2944a..bcadfa0 100644 --- a/src/components/workspace/WorkspacePanel.tsx +++ b/src/components/workspace/WorkspacePanel.tsx @@ -13,7 +13,7 @@ import { useCompletionSchema } from "@/hooks/use-completion-schema"; import { useConnections } from "@/hooks/use-connections"; import { useAppStore } from "@/stores/app-store"; import { Button } from "@/components/ui/button"; -import { Play, Loader2, Lock, BarChart3, Download, AlignLeft, Bookmark } from "lucide-react"; +import { Play, Loader2, Lock, BarChart3, Download, AlignLeft, Bookmark, Table2, Braces } from "lucide-react"; import { format as formatSql } from "sql-formatter"; import { SaveQueryDialog } from "@/components/saved-queries/SaveQueryDialog"; import { @@ -49,6 +49,7 @@ export function WorkspacePanel({ const [error, setError] = useState(null); const [explainData, setExplainData] = useState(null); const [resultView, setResultView] = useState<"results" | "explain">("results"); + const [resultViewMode, setResultViewMode] = useState<"table" | "json">("table"); const [saveDialogOpen, setSaveDialogOpen] = useState(false); const queryMutation = useQueryExecution(); @@ -290,7 +291,7 @@ export function WorkspacePanel({ {(explainData || result || error) && ( - + )} + {resultView === "results" && result && result.columns.length > 0 && ( + + setResultViewMode("table")} + title="Table view" + > + + Table + + setResultViewMode("json")} + title="JSON view" + > + + JSON + + + )} )} {resultView === "explain" && explainData ? ( @@ -322,6 +351,7 @@ export function WorkspacePanel({ result={result} error={error} isLoading={queryMutation.isPending && resultView === "results"} + viewMode={resultViewMode} /> )}