feat: add table data viewer, structure inspector, and export

Add TableDataView with pagination, filtering, and inline editing,
TableStructure with columns/constraints/indexes tabs,
PaginationControls, and ExportDialog for CSV/JSON export.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 19:06:54 +03:00
parent 13a8535b5c
commit 9b9d2cee94
4 changed files with 590 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { save } from "@tauri-apps/plugin-dialog";
import { exportCsv, exportJson } from "@/lib/tauri";
import { toast } from "sonner";
import { Loader2 } from "lucide-react";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
columns: string[];
rows: unknown[][];
}
export function ExportDialog({ open, onOpenChange, columns, rows }: Props) {
const [format, setFormat] = useState<"csv" | "json">("csv");
const [isExporting, setIsExporting] = useState(false);
const handleExport = async () => {
const ext = format === "csv" ? "csv" : "json";
const path = await save({
defaultPath: `export.${ext}`,
filters: [
{
name: format.toUpperCase(),
extensions: [ext],
},
],
});
if (!path) return;
setIsExporting(true);
try {
if (format === "csv") {
await exportCsv(path, columns, rows);
} else {
await exportJson(path, columns, rows);
}
toast.success(`Exported ${rows.length} rows to ${ext.toUpperCase()}`);
onOpenChange(false);
} catch (err) {
toast.error("Export failed", { description: String(err) });
} finally {
setIsExporting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[360px]">
<DialogHeader>
<DialogTitle>Export Data</DialogTitle>
</DialogHeader>
<div className="grid gap-3 py-4">
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
Format
</label>
<Select
value={format}
onValueChange={(v) => setFormat(v as "csv" | "json")}
>
<SelectTrigger className="col-span-3">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="csv">CSV</SelectItem>
<SelectItem value="json">JSON</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-3">
<label className="text-right text-sm text-muted-foreground">
Rows
</label>
<span className="col-span-3 text-sm">
{rows.length.toLocaleString()} rows, {columns.length} columns
</span>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
>
Cancel
</Button>
<Button onClick={handleExport} disabled={isExporting}>
{isExporting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Export
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}