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:
168
src/components/table-viewer/TableStructure.tsx
Normal file
168
src/components/table-viewer/TableStructure.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getTableColumns,
|
||||
getTableConstraints,
|
||||
getTableIndexes,
|
||||
} from "@/lib/tauri";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
interface Props {
|
||||
connectionId: string;
|
||||
schema: string;
|
||||
table: string;
|
||||
}
|
||||
|
||||
export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
const { data: columns } = useQuery({
|
||||
queryKey: ["table-columns", connectionId, schema, table],
|
||||
queryFn: () => getTableColumns(connectionId, schema, table),
|
||||
});
|
||||
|
||||
const { data: constraints } = useQuery({
|
||||
queryKey: ["table-constraints", connectionId, schema, table],
|
||||
queryFn: () => getTableConstraints(connectionId, schema, table),
|
||||
});
|
||||
|
||||
const { data: indexes } = useQuery({
|
||||
queryKey: ["table-indexes", connectionId, schema, table],
|
||||
queryFn: () => getTableIndexes(connectionId, schema, table),
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="columns" className="flex h-full flex-col">
|
||||
<TabsList className="mx-2 mt-2 w-fit">
|
||||
<TabsTrigger value="columns" className="text-xs">
|
||||
Columns{columns ? ` (${columns.length})` : ""}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="constraints" className="text-xs">
|
||||
Constraints{constraints ? ` (${constraints.length})` : ""}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="indexes" className="text-xs">
|
||||
Indexes{indexes ? ` (${indexes.length})` : ""}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="columns" className="flex-1 overflow-hidden mt-0">
|
||||
<ScrollArea className="h-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">#</TableHead>
|
||||
<TableHead className="text-xs">Name</TableHead>
|
||||
<TableHead className="text-xs">Type</TableHead>
|
||||
<TableHead className="text-xs">Nullable</TableHead>
|
||||
<TableHead className="text-xs">Default</TableHead>
|
||||
<TableHead className="text-xs">Key</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{columns?.map((col) => (
|
||||
<TableRow key={col.name}>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{col.ordinal_position}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs font-medium">
|
||||
{col.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{col.data_type}
|
||||
{col.character_maximum_length
|
||||
? `(${col.character_maximum_length})`
|
||||
: ""}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{col.is_nullable ? "YES" : "NO"}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate text-xs text-muted-foreground">
|
||||
{col.column_default ?? "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{col.is_primary_key && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
PK
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="constraints" className="flex-1 overflow-hidden mt-0">
|
||||
<ScrollArea className="h-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">Name</TableHead>
|
||||
<TableHead className="text-xs">Type</TableHead>
|
||||
<TableHead className="text-xs">Columns</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{constraints?.map((c) => (
|
||||
<TableRow key={c.name}>
|
||||
<TableCell className="text-xs font-medium">
|
||||
{c.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
{c.constraint_type}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{c.columns.join(", ")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="indexes" className="flex-1 overflow-hidden mt-0">
|
||||
<ScrollArea className="h-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">Name</TableHead>
|
||||
<TableHead className="text-xs">Definition</TableHead>
|
||||
<TableHead className="text-xs">Unique</TableHead>
|
||||
<TableHead className="text-xs">Primary</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{indexes?.map((idx) => (
|
||||
<TableRow key={idx.name}>
|
||||
<TableCell className="text-xs font-medium">
|
||||
{idx.name}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[400px] truncate text-xs text-muted-foreground">
|
||||
{idx.definition}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{idx.is_unique ? "YES" : "NO"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{idx.is_primary ? "YES" : "NO"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user