Leon4gr45/builder
0
1'use client';2 3import React, { useState, useEffect } from 'react';4import { TableInfo } from '@/lib/vfs/types';5import { ChevronRight, ChevronDown, Table2, KeyRound, Loader2, AlertCircle, Eye, EyeOff } from 'lucide-react';6import { cn } from '@/lib/utils';7import { Button } from '@/components/ui/button';8 9interface SchemaViewerProps {10 deploymentId?: string;11 schemaEndpoint?: string;12 showSystemTablesToggle?: boolean;13 workspaceId?: string;14}15 16export function SchemaViewer({ deploymentId, schemaEndpoint, showSystemTablesToggle = true, workspaceId }: SchemaViewerProps) {17 const apiBase = workspaceId ? `/api/w/${workspaceId}` : '/api';18 const [tables, setTables] = useState<TableInfo[]>([]);19 const [loading, setLoading] = useState(true);20 const [error, setError] = useState<string | null>(null);21 const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());22 const [showSystemTables, setShowSystemTables] = useState(false);23 24 const endpoint = schemaEndpoint || `${apiBase}/admin/deployments/${deploymentId}/database/schema`;25 26 useEffect(() => {27 loadSchema();28 }, [endpoint]);29 30 const loadSchema = async () => {31 try {32 setLoading(true);33 setError(null);34 const res = await fetch(endpoint);35 if (!res.ok) {36 const data = await res.json();37 throw new Error(data.error || 'Failed to load schema');38 }39 const data = await res.json();40 setTables(data.tables);41 } catch (err) {42 setError(err instanceof Error ? err.message : 'Failed to load schema');43 } finally {44 setLoading(false);45 }46 };47 48 const toggleTable = (tableName: string) => {49 setExpandedTables(prev => {50 const next = new Set(prev);51 if (next.has(tableName)) {52 next.delete(tableName);53 } else {54 next.add(tableName);55 }56 return next;57 });58 };59 60 const filteredTables = showSystemTables61 ? tables62 : tables.filter(t => !t.isSystemTable);63 64 if (loading) {65 return (66 <div className="flex items-center justify-center h-full">67 <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />68 </div>69 );70 }71 72 if (error) {73 return (74 <div className="flex flex-col items-center justify-center h-full gap-4">75 <AlertCircle className="h-8 w-8 text-destructive" />76 <p className="text-sm text-muted-foreground">{error}</p>77 <Button variant="outline" onClick={loadSchema}>78 Retry79 </Button>80 </div>81 );82 }83 84 return (85 <div className="h-full flex flex-col">86 <div className="flex items-center justify-between mb-4">87 <h3 className="text-sm font-medium">Database Tables</h3>88 {showSystemTablesToggle && (89 <Button90 variant="ghost"91 size="sm"92 onClick={() => setShowSystemTables(!showSystemTables)}93 className="text-xs"94 >95 {showSystemTables ? (96 <>97 <EyeOff className="h-3.5 w-3.5 mr-1" />98 Hide System Tables99 </>100 ) : (101 <>102 <Eye className="h-3.5 w-3.5 mr-1" />103 Show System Tables104 </>105 )}106 </Button>107 )}108 </div>109 110 <div className="flex-1 overflow-auto border rounded-lg">111 {filteredTables.length === 0 ? (112 <div className="flex flex-col items-center justify-center h-full p-8 text-center">113 <Table2 className="h-8 w-8 text-muted-foreground mb-2" />114 <p className="text-sm text-muted-foreground">No user tables found</p>115 <p className="text-xs text-muted-foreground mt-1">116 Create tables using the SQL editor117 </p>118 </div>119 ) : (120 <div className="divide-y">121 {filteredTables.map(table => (122 <div key={table.name} className={cn(123 "transition-colors",124 table.isSystemTable && "bg-muted/30"125 )}>126 <button127 onClick={() => toggleTable(table.name)}128 className="w-full flex items-center gap-2 p-3 text-left hover:bg-muted/50 transition-colors"129 >130 {expandedTables.has(table.name) ? (131 <ChevronDown className="h-4 w-4 text-muted-foreground" />132 ) : (133 <ChevronRight className="h-4 w-4 text-muted-foreground" />134 )}135 <Table2 className="h-4 w-4 text-blue-500" />136 <span className="flex-1 font-mono text-sm">{table.name}</span>137 <span className="text-xs text-muted-foreground">138 {table.rowCount} row{table.rowCount !== 1 ? 's' : ''}139 </span>140 {table.isSystemTable && (141 <span className="text-xs bg-muted px-1.5 py-0.5 rounded">system</span>142 )}143 </button>144 145 {expandedTables.has(table.name) && (146 <div className="bg-muted/20 border-t">147 <table className="w-full text-sm">148 <thead>149 <tr className="border-b bg-muted/30">150 <th className="text-left p-2 font-medium">Column</th>151 <th className="text-left p-2 font-medium">Type</th>152 <th className="text-left p-2 font-medium">Nullable</th>153 <th className="text-left p-2 font-medium">Default</th>154 </tr>155 </thead>156 <tbody>157 {table.columns.map(col => (158 <tr key={col.name} className="border-b last:border-0">159 <td className="p-2 font-mono flex items-center gap-1.5">160 {col.primaryKey && (161 <KeyRound className="h-3 w-3 text-yellow-500" />162 )}163 {col.name}164 </td>165 <td className="p-2 font-mono text-muted-foreground">166 {col.type || 'TEXT'}167 </td>168 <td className="p-2 text-muted-foreground">169 {col.nullable ? 'Yes' : 'No'}170 </td>171 <td className="p-2 font-mono text-muted-foreground text-xs">172 {col.defaultValue || '-'}173 </td>174 </tr>175 ))}176 </tbody>177 </table>178 </div>179 )}180 </div>181 ))}182 </div>183 )}184 </div>185 </div>186 );187}188 