feat: add ER diagram and enhance TableStructure with FK details, triggers, comments
- Add interactive ER diagram with ReactFlow + dagre auto-layout, accessible via right-click context menu on schema nodes in the sidebar - Enhance TableStructure: column comments, FK referenced table/columns, ON UPDATE/DELETE rules, new Triggers tab - Backend: rewrite get_table_constraints using pg_constraint for proper composite FK support, add get_table_triggers and get_schema_erd commands Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
186
src/components/erd/ErdDiagram.tsx
Normal file
186
src/components/erd/ErdDiagram.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import { useMemo, useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
MarkerType,
|
||||
PanOnScrollMode,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
type Node,
|
||||
type Edge,
|
||||
type NodeTypes,
|
||||
type NodeChange,
|
||||
type EdgeChange,
|
||||
} from "@xyflow/react";
|
||||
import dagre from "dagre";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
|
||||
import { useSchemaErd } from "@/hooks/use-schema";
|
||||
import { ErdTableNode, type ErdTableNodeData } from "./ErdTableNode";
|
||||
import type { ErdData } from "@/types";
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
erdTable: ErdTableNode,
|
||||
};
|
||||
|
||||
const NODE_WIDTH = 250;
|
||||
const NODE_ROW_HEIGHT = 24;
|
||||
const NODE_HEADER_HEIGHT = 36;
|
||||
|
||||
function buildLayout(data: ErdData): { nodes: Node[]; edges: Edge[] } {
|
||||
const g = new dagre.graphlib.Graph();
|
||||
g.setDefaultEdgeLabel(() => ({}));
|
||||
g.setGraph({ rankdir: "LR", nodesep: 60, ranksep: 150 });
|
||||
|
||||
// Build list of FK column names per table for icon display
|
||||
const fkColumnsPerTable = new Map<string, string[]>();
|
||||
for (const rel of data.relationships) {
|
||||
const key = `${rel.source_schema}.${rel.source_table}`;
|
||||
if (!fkColumnsPerTable.has(key)) fkColumnsPerTable.set(key, []);
|
||||
for (const col of rel.source_columns) {
|
||||
const arr = fkColumnsPerTable.get(key)!;
|
||||
if (!arr.includes(col)) arr.push(col);
|
||||
}
|
||||
}
|
||||
|
||||
for (const table of data.tables) {
|
||||
const height = NODE_HEADER_HEIGHT + table.columns.length * NODE_ROW_HEIGHT;
|
||||
g.setNode(table.name, { width: NODE_WIDTH, height });
|
||||
}
|
||||
|
||||
for (const rel of data.relationships) {
|
||||
g.setEdge(rel.source_table, rel.target_table);
|
||||
}
|
||||
|
||||
dagre.layout(g);
|
||||
|
||||
const nodes: Node[] = data.tables.map((table) => {
|
||||
const pos = g.node(table.name);
|
||||
const tableKey = `${table.schema}.${table.name}`;
|
||||
return {
|
||||
id: table.name,
|
||||
type: "erdTable",
|
||||
position: { x: pos.x - NODE_WIDTH / 2, y: pos.y - pos.height / 2 },
|
||||
data: {
|
||||
label: table.name,
|
||||
schema: table.schema,
|
||||
columns: table.columns,
|
||||
fkColumnNames: fkColumnsPerTable.get(tableKey) ?? [],
|
||||
} satisfies ErdTableNodeData,
|
||||
};
|
||||
});
|
||||
|
||||
const edges: Edge[] = data.relationships.map((rel) => ({
|
||||
id: rel.constraint_name,
|
||||
source: rel.source_table,
|
||||
target: rel.target_table,
|
||||
type: "smoothstep",
|
||||
label: rel.constraint_name,
|
||||
labelStyle: { fontSize: 10, fill: "var(--muted-foreground)" },
|
||||
labelBgStyle: { fill: "var(--card)", fillOpacity: 0.8 },
|
||||
labelBgPadding: [4, 2] as [number, number],
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: "var(--muted-foreground)",
|
||||
},
|
||||
style: { stroke: "var(--muted-foreground)", strokeWidth: 1.5 },
|
||||
}));
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
interface Props {
|
||||
connectionId: string;
|
||||
schema: string;
|
||||
}
|
||||
|
||||
export function ErdDiagram({ connectionId, schema }: Props) {
|
||||
const { data: erdData, isLoading, error } = useSchemaErd(connectionId, schema);
|
||||
|
||||
const layout = useMemo(() => {
|
||||
if (!erdData) return null;
|
||||
return buildLayout(erdData);
|
||||
}, [erdData]);
|
||||
|
||||
const [nodes, setNodes] = useState<Node[]>([]);
|
||||
const [edges, setEdges] = useState<Edge[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (layout) {
|
||||
setNodes(layout.nodes);
|
||||
setEdges(layout.edges);
|
||||
}
|
||||
}, [layout]);
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[],
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[],
|
||||
);
|
||||
|
||||
const onInit = useCallback((instance: { fitView: () => void }) => {
|
||||
setTimeout(() => instance.fitView(), 50);
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Loading ER diagram...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-sm text-destructive">
|
||||
Error loading ER diagram: {String(error)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!erdData || erdData.tables.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
No tables found in schema "{schema}".
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onInit={onInit}
|
||||
fitView
|
||||
colorMode="dark"
|
||||
minZoom={0.05}
|
||||
maxZoom={3}
|
||||
zoomOnScroll
|
||||
zoomOnPinch
|
||||
panOnScroll
|
||||
panOnScrollMode={PanOnScrollMode.Free}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background gap={16} size={1} />
|
||||
<Controls className="!bg-card !border !shadow-sm [&>button]:!bg-card [&>button]:!border-border [&>button]:!text-foreground" />
|
||||
<MiniMap
|
||||
className="!bg-card !border"
|
||||
nodeColor="var(--muted)"
|
||||
maskColor="rgba(0, 0, 0, 0.7)"
|
||||
/>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
src/components/erd/ErdTableNode.tsx
Normal file
54
src/components/erd/ErdTableNode.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { memo } from "react";
|
||||
import { Handle, Position, type NodeProps } from "@xyflow/react";
|
||||
import type { ErdColumn } from "@/types";
|
||||
import { KeyRound, Link } from "lucide-react";
|
||||
|
||||
export interface ErdTableNodeData {
|
||||
label: string;
|
||||
schema: string;
|
||||
columns: ErdColumn[];
|
||||
fkColumnNames: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function ErdTableNodeComponent({ data }: NodeProps) {
|
||||
const { label, columns, fkColumnNames } = data as unknown as ErdTableNodeData;
|
||||
|
||||
return (
|
||||
<div className="min-w-[220px] rounded-lg border border-border bg-card text-card-foreground shadow-md">
|
||||
<div className="rounded-t-lg border-b bg-primary/10 px-3 py-2 text-xs font-bold tracking-wide text-primary">
|
||||
{label}
|
||||
</div>
|
||||
<div className="divide-y divide-border/50">
|
||||
{(columns as ErdColumn[]).map((col, i) => (
|
||||
<div key={i} className="flex items-center gap-1.5 px-3 py-1 text-[11px]">
|
||||
{col.is_primary_key ? (
|
||||
<KeyRound className="h-3 w-3 shrink-0 text-amber-500" />
|
||||
) : (fkColumnNames as string[]).includes(col.name) ? (
|
||||
<Link className="h-3 w-3 shrink-0 text-blue-400" />
|
||||
) : (
|
||||
<span className="h-3 w-3 shrink-0" />
|
||||
)}
|
||||
<span className="font-medium">{col.name}</span>
|
||||
<span className="ml-auto text-muted-foreground">{col.data_type}</span>
|
||||
{col.is_nullable && (
|
||||
<span className="text-muted-foreground/60">?</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="!w-1.5 !h-1.5 !bg-primary !opacity-0 hover:!opacity-100 !border-none !min-w-0 !min-h-0"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="!w-1.5 !h-1.5 !bg-primary !opacity-0 hover:!opacity-100 !border-none !min-w-0 !min-h-0"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ErdTableNode = memo(ErdTableNodeComponent);
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { useConnections } from "@/hooks/use-connections";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { X, Table2, Code, Columns, Users, Activity, Search } from "lucide-react";
|
||||
import { X, Table2, Code, Columns, Users, Activity, Search, GitFork } from "lucide-react";
|
||||
|
||||
export function TabBar() {
|
||||
const { tabs, activeTabId, setActiveTabId, closeTab } = useAppStore();
|
||||
@@ -16,6 +16,7 @@ export function TabBar() {
|
||||
roles: <Users className="h-3 w-3" />,
|
||||
sessions: <Activity className="h-3 w-3" />,
|
||||
lookup: <Search className="h-3 w-3" />,
|
||||
erd: <GitFork className="h-3 w-3" />,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -136,6 +136,17 @@ export function SchemaTree() {
|
||||
};
|
||||
addTab(tab);
|
||||
}}
|
||||
onViewErd={(schema) => {
|
||||
const tab: Tab = {
|
||||
id: crypto.randomUUID(),
|
||||
type: "erd",
|
||||
title: `${schema} (ER Diagram)`,
|
||||
connectionId: activeConnectionId,
|
||||
database: currentDatabase ?? undefined,
|
||||
schema,
|
||||
};
|
||||
addTab(tab);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -150,6 +161,7 @@ function DatabaseNode({
|
||||
isSwitching,
|
||||
onOpenTable,
|
||||
onViewStructure,
|
||||
onViewErd,
|
||||
}: {
|
||||
name: string;
|
||||
isActive: boolean;
|
||||
@@ -158,6 +170,7 @@ function DatabaseNode({
|
||||
isSwitching: boolean;
|
||||
onOpenTable: (schema: string, table: string) => void;
|
||||
onViewStructure: (schema: string, table: string) => void;
|
||||
onViewErd: (schema: string) => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const readOnlyMap = useAppStore((s) => s.readOnlyMap);
|
||||
@@ -234,6 +247,7 @@ function DatabaseNode({
|
||||
connectionId={connectionId}
|
||||
onOpenTable={onOpenTable}
|
||||
onViewStructure={onViewStructure}
|
||||
onViewErd={onViewErd}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -250,10 +264,12 @@ function SchemasForCurrentDb({
|
||||
connectionId,
|
||||
onOpenTable,
|
||||
onViewStructure,
|
||||
onViewErd,
|
||||
}: {
|
||||
connectionId: string;
|
||||
onOpenTable: (schema: string, table: string) => void;
|
||||
onViewStructure: (schema: string, table: string) => void;
|
||||
onViewErd: (schema: string) => void;
|
||||
}) {
|
||||
const { data: schemas } = useSchemas(connectionId);
|
||||
|
||||
@@ -272,6 +288,7 @@ function SchemasForCurrentDb({
|
||||
connectionId={connectionId}
|
||||
onOpenTable={(table) => onOpenTable(schema, table)}
|
||||
onViewStructure={(table) => onViewStructure(schema, table)}
|
||||
onViewErd={() => onViewErd(schema)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -283,32 +300,43 @@ function SchemaNode({
|
||||
connectionId,
|
||||
onOpenTable,
|
||||
onViewStructure,
|
||||
onViewErd,
|
||||
}: {
|
||||
schema: string;
|
||||
connectionId: string;
|
||||
onOpenTable: (table: string) => void;
|
||||
onViewStructure: (table: string) => void;
|
||||
onViewErd: () => void;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="flex items-center gap-1 rounded-sm px-1 py-0.5 text-sm hover:bg-accent cursor-pointer select-none font-medium"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{expanded ? (
|
||||
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Database className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
<span>{schema}</span>
|
||||
</div>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<div
|
||||
className="flex items-center gap-1 rounded-sm px-1 py-0.5 text-sm hover:bg-accent cursor-pointer select-none font-medium"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{expanded ? (
|
||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
{expanded ? (
|
||||
<FolderOpen className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Database className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
)}
|
||||
<span>{schema}</span>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem onClick={onViewErd}>
|
||||
View ER Diagram
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
{expanded && (
|
||||
<div className="ml-4">
|
||||
<CategoryNode
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getTableColumns,
|
||||
getTableConstraints,
|
||||
getTableIndexes,
|
||||
getTableTriggers,
|
||||
} from "@/lib/tauri";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -38,6 +39,11 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
queryFn: () => getTableIndexes(connectionId, schema, table),
|
||||
});
|
||||
|
||||
const { data: triggers } = useQuery({
|
||||
queryKey: ["table-triggers", connectionId, schema, table],
|
||||
queryFn: () => getTableTriggers(connectionId, schema, table),
|
||||
});
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="columns" className="flex h-full flex-col">
|
||||
<TabsList className="mx-2 mt-2 w-fit">
|
||||
@@ -50,6 +56,9 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
<TabsTrigger value="indexes" className="text-xs">
|
||||
Indexes{indexes ? ` (${indexes.length})` : ""}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="triggers" className="text-xs">
|
||||
Triggers{triggers ? ` (${triggers.length})` : ""}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="columns" className="flex-1 overflow-hidden mt-0">
|
||||
@@ -63,6 +72,7 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
<TableHead className="text-xs">Nullable</TableHead>
|
||||
<TableHead className="text-xs">Default</TableHead>
|
||||
<TableHead className="text-xs">Key</TableHead>
|
||||
<TableHead className="text-xs">Comment</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -84,7 +94,7 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
{col.is_nullable ? "YES" : "NO"}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate text-xs text-muted-foreground">
|
||||
{col.column_default ?? "—"}
|
||||
{col.column_default ?? "\u2014"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{col.is_primary_key && (
|
||||
@@ -93,6 +103,9 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-[200px] truncate text-xs text-muted-foreground">
|
||||
{col.comment ?? "\u2014"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -108,6 +121,9 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
<TableHead className="text-xs">Name</TableHead>
|
||||
<TableHead className="text-xs">Type</TableHead>
|
||||
<TableHead className="text-xs">Columns</TableHead>
|
||||
<TableHead className="text-xs">References</TableHead>
|
||||
<TableHead className="text-xs">On Update</TableHead>
|
||||
<TableHead className="text-xs">On Delete</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -124,6 +140,17 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
<TableCell className="text-xs">
|
||||
{c.columns.join(", ")}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{c.referenced_table
|
||||
? `${c.referenced_schema}.${c.referenced_table}(${c.referenced_columns?.join(", ")})`
|
||||
: "\u2014"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{c.update_rule ?? "\u2014"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{c.delete_rule ?? "\u2014"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -163,6 +190,49 @@ export function TableStructure({ connectionId, schema, table }: Props) {
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="triggers" className="flex-1 overflow-hidden mt-0">
|
||||
<ScrollArea className="h-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="text-xs">Name</TableHead>
|
||||
<TableHead className="text-xs">Timing</TableHead>
|
||||
<TableHead className="text-xs">Event</TableHead>
|
||||
<TableHead className="text-xs">Level</TableHead>
|
||||
<TableHead className="text-xs">Function</TableHead>
|
||||
<TableHead className="text-xs">Enabled</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{triggers?.map((t) => (
|
||||
<TableRow key={t.name}>
|
||||
<TableCell className="text-xs font-medium">
|
||||
{t.name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{t.timing}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{t.event}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{t.orientation}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">
|
||||
{t.function_name}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{t.is_enabled ? "YES" : "NO"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TableStructure } from "@/components/table-viewer/TableStructure";
|
||||
import { RoleManagerView } from "@/components/management/RoleManagerView";
|
||||
import { SessionsView } from "@/components/management/SessionsView";
|
||||
import { EntityLookupPanel } from "@/components/lookup/EntityLookupPanel";
|
||||
import { ErdDiagram } from "@/components/erd/ErdDiagram";
|
||||
|
||||
export function TabContent() {
|
||||
const { tabs, activeTabId, updateTab } = useAppStore();
|
||||
@@ -72,6 +73,14 @@ export function TabContent() {
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "erd":
|
||||
content = (
|
||||
<ErdDiagram
|
||||
connectionId={tab.connectionId}
|
||||
schema={tab.schema!}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
content = null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
listSequences,
|
||||
switchDatabase,
|
||||
getColumnDetails,
|
||||
getSchemaErd,
|
||||
} from "@/lib/tauri";
|
||||
import type { ConnectionConfig } from "@/types";
|
||||
|
||||
@@ -88,3 +89,12 @@ export function useColumnDetails(connectionId: string | null, schema: string | n
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSchemaErd(connectionId: string | null, schema: string | null) {
|
||||
return useQuery({
|
||||
queryKey: ["schema-erd", connectionId, schema],
|
||||
queryFn: () => getSchemaErd(connectionId!, schema!),
|
||||
enabled: !!connectionId && !!schema,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import type {
|
||||
ColumnInfo,
|
||||
ConstraintInfo,
|
||||
IndexInfo,
|
||||
TriggerInfo,
|
||||
ErdData,
|
||||
HistoryEntry,
|
||||
SavedQuery,
|
||||
SessionInfo,
|
||||
@@ -115,6 +117,15 @@ export const getTableIndexes = (
|
||||
table: string
|
||||
) => invoke<IndexInfo[]>("get_table_indexes", { connectionId, schema, table });
|
||||
|
||||
export const getTableTriggers = (
|
||||
connectionId: string,
|
||||
schema: string,
|
||||
table: string
|
||||
) => invoke<TriggerInfo[]>("get_table_triggers", { connectionId, schema, table });
|
||||
|
||||
export const getSchemaErd = (connectionId: string, schema: string) =>
|
||||
invoke<ErdData>("get_schema_erd", { connectionId, schema });
|
||||
|
||||
// Data
|
||||
export const getTableData = (params: {
|
||||
connectionId: string;
|
||||
|
||||
@@ -56,12 +56,18 @@ export interface ColumnInfo {
|
||||
ordinal_position: number;
|
||||
character_maximum_length: number | null;
|
||||
is_primary_key: boolean;
|
||||
comment: string | null;
|
||||
}
|
||||
|
||||
export interface ConstraintInfo {
|
||||
name: string;
|
||||
constraint_type: string;
|
||||
columns: string[];
|
||||
referenced_schema: string | null;
|
||||
referenced_table: string | null;
|
||||
referenced_columns: string[] | null;
|
||||
update_rule: string | null;
|
||||
delete_rule: string | null;
|
||||
}
|
||||
|
||||
export interface IndexInfo {
|
||||
@@ -223,8 +229,13 @@ export interface SavedQuery {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export type AiProvider = "ollama" | "openai" | "anthropic";
|
||||
|
||||
export interface AiSettings {
|
||||
provider: AiProvider;
|
||||
ollama_url: string;
|
||||
openai_api_key?: string;
|
||||
anthropic_api_key?: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
@@ -272,7 +283,47 @@ export interface LookupProgress {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type TabType = "query" | "table" | "structure" | "roles" | "sessions" | "lookup";
|
||||
export interface TriggerInfo {
|
||||
name: string;
|
||||
event: string;
|
||||
timing: string;
|
||||
orientation: string;
|
||||
function_name: string;
|
||||
is_enabled: boolean;
|
||||
definition: string;
|
||||
}
|
||||
|
||||
export interface ErdColumn {
|
||||
name: string;
|
||||
data_type: string;
|
||||
is_nullable: boolean;
|
||||
is_primary_key: boolean;
|
||||
}
|
||||
|
||||
export interface ErdTable {
|
||||
schema: string;
|
||||
name: string;
|
||||
columns: ErdColumn[];
|
||||
}
|
||||
|
||||
export interface ErdRelationship {
|
||||
constraint_name: string;
|
||||
source_schema: string;
|
||||
source_table: string;
|
||||
source_columns: string[];
|
||||
target_schema: string;
|
||||
target_table: string;
|
||||
target_columns: string[];
|
||||
update_rule: string;
|
||||
delete_rule: string;
|
||||
}
|
||||
|
||||
export interface ErdData {
|
||||
tables: ErdTable[];
|
||||
relationships: ErdRelationship[];
|
||||
}
|
||||
|
||||
export type TabType = "query" | "table" | "structure" | "roles" | "sessions" | "lookup" | "erd";
|
||||
|
||||
export interface Tab {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user