From a2371f00df48ebd6ab5b5552813f48f54d05340e Mon Sep 17 00:00:00 2001 From: "A.Shakhmatov" Date: Fri, 13 Feb 2026 13:05:52 +0300 Subject: [PATCH] feat: add JSON view mode toggle for query results Add a Table/JSON segmented toggle to both the query workspace and table data viewer, allowing users to switch between tabular and pretty-printed JSON display of results without exporting to a file. Co-Authored-By: Claude Opus 4.6 --- src/components/results/ResultsJsonView.tsx | 60 +++++++++++ src/components/results/ResultsPanel.tsx | 12 +++ src/components/table-viewer/TableDataView.tsx | 101 ++++++++++++------ src/components/workspace/WorkspacePanel.tsx | 34 +++++- 4 files changed, 173 insertions(+), 34 deletions(-) create mode 100644 src/components/results/ResultsJsonView.tsx 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 && ( - - - + + + handleExport("csv")}> + Export CSV + + handleExport("json")}> + Export JSON + + + +
+ - - - handleExport("csv")}> - Export CSV - - handleExport("json")}> - Export JSON - - - + + Table + + +
+ )} {!isReadOnly && ( + + + )} )} {resultView === "explain" && explainData ? ( @@ -322,6 +351,7 @@ export function WorkspacePanel({ result={result} error={error} isLoading={queryMutation.isPending && resultView === "results"} + viewMode={resultViewMode} /> )}