feat: add column sort, SQL formatter, table stats, insert dialog, saved queries & sessions monitor

- Column sort by header click in table view (ASC/DESC/none cycle, server-side)
- SQL formatter with Format button and Shift+Alt+F keybinding (sql-formatter)
- Table size and row count display in schema tree via pg_class
- Insert row dialog with column type hints and auto-skip for identity columns
- Saved queries (bookmarks) with CRUD backend, sidebar panel, and save dialog
- Active sessions monitor (pg_stat_activity) with auto-refresh, cancel & terminate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 11:52:05 +03:00
parent ab72eeee80
commit 9d54167023
29 changed files with 1223 additions and 18 deletions

View File

@@ -13,7 +13,9 @@ 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 } from "lucide-react";
import { Play, Loader2, Lock, BarChart3, Download, AlignLeft, Bookmark } from "lucide-react";
import { format as formatSql } from "sql-formatter";
import { SaveQueryDialog } from "@/components/saved-queries/SaveQueryDialog";
import {
DropdownMenu,
DropdownMenuContent,
@@ -47,6 +49,7 @@ export function WorkspacePanel({
const [error, setError] = useState<string | null>(null);
const [explainData, setExplainData] = useState<ExplainResult | null>(null);
const [resultView, setResultView] = useState<"results" | "explain">("results");
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const queryMutation = useQueryExecution();
const addHistoryMutation = useAddHistory();
@@ -150,6 +153,17 @@ export function WorkspacePanel({
);
}, [connectionId, sqlValue, queryMutation]);
const handleFormat = useCallback(() => {
if (!sqlValue.trim()) return;
try {
const formatted = formatSql(sqlValue, { language: "postgresql" });
setSqlValue(formatted);
onSqlChange?.(formatted);
} catch {
// Silently ignore format errors on invalid SQL
}
}, [sqlValue, onSqlChange]);
const handleExport = useCallback(
async (format: "csv" | "json") => {
if (!result || result.columns.length === 0) return;
@@ -175,6 +189,7 @@ export function WorkspacePanel({
);
return (
<>
<ResizablePanelGroup orientation="vertical">
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
<div className="flex h-full flex-col">
@@ -207,6 +222,28 @@ export function WorkspacePanel({
)}
Explain
</Button>
<Button
size="sm"
variant="ghost"
className="h-6 gap-1 text-xs"
onClick={handleFormat}
disabled={!sqlValue.trim()}
title="Format SQL (Shift+Alt+F)"
>
<AlignLeft className="h-3 w-3" />
Format
</Button>
<Button
size="sm"
variant="ghost"
className="h-6 gap-1 text-xs"
onClick={() => setSaveDialogOpen(true)}
disabled={!sqlValue.trim()}
title="Save query"
>
<Bookmark className="h-3 w-3" />
Save
</Button>
{result && result.columns.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -244,6 +281,7 @@ export function WorkspacePanel({
value={sqlValue}
onChange={handleChange}
onExecute={handleExecute}
onFormat={handleFormat}
schema={completionSchema}
/>
</div>
@@ -288,5 +326,13 @@ export function WorkspacePanel({
)}
</ResizablePanel>
</ResizablePanelGroup>
<SaveQueryDialog
open={saveDialogOpen}
onOpenChange={setSaveDialogOpen}
sql={sqlValue}
connectionId={connectionId}
/>
</>
);
}