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:
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user