feat: add CSV/JSON export buttons to query results and table data view
Export dropdown (CSV/JSON) appears in the WorkspacePanel toolbar when query results are available, and in the TableDataView toolbar for table data. Uses the Tauri save dialog for file path selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,15 @@ import { getTableColumns } from "@/lib/tauri";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useAppStore } from "@/stores/app-store";
|
import { useAppStore } from "@/stores/app-store";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Save, RotateCcw, Filter, Loader2, Lock } from "lucide-react";
|
import { Save, RotateCcw, Filter, Loader2, Lock, Download } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { exportCsv, exportJson } from "@/lib/tauri";
|
||||||
|
import { save } from "@tauri-apps/plugin-dialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
@@ -123,6 +131,30 @@ export function TableDataView({ connectionId, schema, table }: Props) {
|
|||||||
setPendingChanges(new Map());
|
setPendingChanges(new Map());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleExport = useCallback(
|
||||||
|
async (format: "csv" | "json") => {
|
||||||
|
if (!data || data.columns.length === 0) return;
|
||||||
|
const ext = format === "csv" ? "csv" : "json";
|
||||||
|
const path = await save({
|
||||||
|
title: `Export as ${ext.toUpperCase()}`,
|
||||||
|
defaultPath: `${table}.${ext}`,
|
||||||
|
filters: [{ name: ext.toUpperCase(), extensions: [ext] }],
|
||||||
|
});
|
||||||
|
if (!path) return;
|
||||||
|
try {
|
||||||
|
if (format === "csv") {
|
||||||
|
await exportCsv(path, data.columns, data.rows as unknown[][]);
|
||||||
|
} else {
|
||||||
|
await exportJson(path, data.columns, data.rows as unknown[][]);
|
||||||
|
}
|
||||||
|
toast.success(`Exported to ${path}`);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Export failed", { description: String(err) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[data, table]
|
||||||
|
);
|
||||||
|
|
||||||
const handleApplyFilter = () => {
|
const handleApplyFilter = () => {
|
||||||
setAppliedFilter(filter || undefined);
|
setAppliedFilter(filter || undefined);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@@ -153,6 +185,28 @@ export function TableDataView({ connectionId, schema, table }: Props) {
|
|||||||
>
|
>
|
||||||
Apply
|
Apply
|
||||||
</Button>
|
</Button>
|
||||||
|
{data && data.columns.length > 0 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<Download className="h-3 w-3" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuItem onClick={() => handleExport("csv")}>
|
||||||
|
Export CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleExport("json")}>
|
||||||
|
Export JSON
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
{pendingChanges.size > 0 && (
|
{pendingChanges.size > 0 && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -13,7 +13,16 @@ import { useCompletionSchema } from "@/hooks/use-completion-schema";
|
|||||||
import { useConnections } from "@/hooks/use-connections";
|
import { useConnections } from "@/hooks/use-connections";
|
||||||
import { useAppStore } from "@/stores/app-store";
|
import { useAppStore } from "@/stores/app-store";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Play, Loader2, Lock, BarChart3 } from "lucide-react";
|
import { Play, Loader2, Lock, BarChart3, Download } from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { exportCsv, exportJson } from "@/lib/tauri";
|
||||||
|
import { save } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { toast } from "sonner";
|
||||||
import type { QueryResult, ExplainResult } from "@/types";
|
import type { QueryResult, ExplainResult } from "@/types";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -141,6 +150,30 @@ export function WorkspacePanel({
|
|||||||
);
|
);
|
||||||
}, [connectionId, sqlValue, queryMutation]);
|
}, [connectionId, sqlValue, queryMutation]);
|
||||||
|
|
||||||
|
const handleExport = useCallback(
|
||||||
|
async (format: "csv" | "json") => {
|
||||||
|
if (!result || result.columns.length === 0) return;
|
||||||
|
const ext = format === "csv" ? "csv" : "json";
|
||||||
|
const path = await save({
|
||||||
|
title: `Export as ${ext.toUpperCase()}`,
|
||||||
|
defaultPath: `query_result.${ext}`,
|
||||||
|
filters: [{ name: ext.toUpperCase(), extensions: [ext] }],
|
||||||
|
});
|
||||||
|
if (!path) return;
|
||||||
|
try {
|
||||||
|
if (format === "csv") {
|
||||||
|
await exportCsv(path, result.columns, result.rows as unknown[][]);
|
||||||
|
} else {
|
||||||
|
await exportJson(path, result.columns, result.rows as unknown[][]);
|
||||||
|
}
|
||||||
|
toast.success(`Exported to ${path}`);
|
||||||
|
} catch (err) {
|
||||||
|
toast.error("Export failed", { description: String(err) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[result]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizablePanelGroup orientation="vertical">
|
<ResizablePanelGroup orientation="vertical">
|
||||||
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
|
<ResizablePanel id="editor" defaultSize="40%" minSize="15%">
|
||||||
@@ -174,6 +207,28 @@ export function WorkspacePanel({
|
|||||||
)}
|
)}
|
||||||
Explain
|
Explain
|
||||||
</Button>
|
</Button>
|
||||||
|
{result && result.columns.length > 0 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-6 gap-1 text-xs"
|
||||||
|
>
|
||||||
|
<Download className="h-3 w-3" />
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuItem onClick={() => handleExport("csv")}>
|
||||||
|
Export CSV
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => handleExport("json")}>
|
||||||
|
Export JSON
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
<span className="text-[11px] text-muted-foreground">
|
<span className="text-[11px] text-muted-foreground">
|
||||||
Ctrl+Enter to execute
|
Ctrl+Enter to execute
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user