sirius-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sirius_cli/__init__.py +1 -0
- sirius_cli/cli.py +335 -0
- sirius_cli/generator.py +125 -0
- sirius_cli/parser.py +322 -0
- sirius_cli/templates/backend/Dockerfile.jinja2 +17 -0
- sirius_cli/templates/backend/alembic/env.py.jinja2 +70 -0
- sirius_cli/templates/backend/alembic/script.py.mako.jinja2 +26 -0
- sirius_cli/templates/backend/database.py.jinja2 +39 -0
- sirius_cli/templates/backend/main.py.jinja2 +178 -0
- sirius_cli/templates/backend/models.py.jinja2 +31 -0
- sirius_cli/templates/backend/requirements.txt.jinja2 +13 -0
- sirius_cli/templates/backend/schemas.py.jinja2 +80 -0
- sirius_cli/templates/docker-compose.yml.jinja2 +27 -0
- sirius_cli/templates/frontend/.env.jinja2 +3 -0
- sirius_cli/templates/frontend/Dockerfile.jinja2 +12 -0
- sirius_cli/templates/frontend/index.html.jinja2 +16 -0
- sirius_cli/templates/frontend/package.json.jinja2 +28 -0
- sirius_cli/templates/frontend/postcss.config.js.jinja2 +6 -0
- sirius_cli/templates/frontend/src/App.tsx.jinja2 +89 -0
- sirius_cli/templates/frontend/src/Dashboard.tsx.jinja2 +210 -0
- sirius_cli/templates/frontend/src/TableCrud.tsx.jinja2 +567 -0
- sirius_cli/templates/frontend/src/index.css.jinja2 +25 -0
- sirius_cli/templates/frontend/src/main.tsx.jinja2 +10 -0
- sirius_cli/templates/frontend/tailwind.config.js.jinja2 +21 -0
- sirius_cli/templates/frontend/tsconfig.json.jinja2 +25 -0
- sirius_cli/templates/frontend/vite.config.ts.jinja2 +10 -0
- sirius_cli-0.1.0.dist-info/METADATA +165 -0
- sirius_cli-0.1.0.dist-info/RECORD +32 -0
- sirius_cli-0.1.0.dist-info/WHEEL +5 -0
- sirius_cli-0.1.0.dist-info/entry_points.txt +2 -0
- sirius_cli-0.1.0.dist-info/licenses/LICENSE +619 -0
- sirius_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
fastapi>=0.100.0
|
|
2
|
+
uvicorn>=0.20.0
|
|
3
|
+
sqlalchemy>=2.0.0
|
|
4
|
+
alembic>=1.11.0
|
|
5
|
+
pydantic>=2.0.0
|
|
6
|
+
pandas>=1.5.0
|
|
7
|
+
openpyxl>=3.0.0
|
|
8
|
+
email-validator>=2.0.0
|
|
9
|
+
{% if db_type == 'pg' %}
|
|
10
|
+
psycopg2-binary>=2.9.0
|
|
11
|
+
{% elif db_type == 'mysql' %}
|
|
12
|
+
pymysql>=1.1.0
|
|
13
|
+
{% endif %}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from pydantic import EmailStr
|
|
7
|
+
_has_email_validator = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
_has_email_validator = False
|
|
10
|
+
EmailStr = str # graceful fallback if email-validator not installed
|
|
11
|
+
|
|
12
|
+
{% for table_name, columns in schemas.items() %}
|
|
13
|
+
{% set class_name = table_name.replace('_', ' ').title().replace(' ', '') %}
|
|
14
|
+
class {{ class_name }}Base(BaseModel):
|
|
15
|
+
{% for col in columns %}
|
|
16
|
+
{% if not col.is_pk %}
|
|
17
|
+
{% if col.type == 'Integer' %}
|
|
18
|
+
{% if col.name in ['amount', 'price', 'quantity', 'count', 'total', 'balance', 'score', 'rating', 'age', 'year'] or col.name.endswith(('_amount', '_price', '_qty', '_count', '_total', '_score')) %}
|
|
19
|
+
{{ col.name }}: Optional[int] = Field(default=None, ge=0)
|
|
20
|
+
{% else %}
|
|
21
|
+
{{ col.name }}: Optional[int] = None
|
|
22
|
+
{% endif %}
|
|
23
|
+
{% elif col.type == 'Float' %}
|
|
24
|
+
{% if col.name in ['amount', 'price', 'quantity', 'total', 'balance', 'score', 'rating', 'weight', 'cost'] or col.name.endswith(('_amount', '_price', '_qty', '_total', '_score', '_cost')) %}
|
|
25
|
+
{{ col.name }}: Optional[float] = Field(default=None, ge=0)
|
|
26
|
+
{% else %}
|
|
27
|
+
{{ col.name }}: Optional[float] = None
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% elif col.type == 'Boolean' %}
|
|
30
|
+
{{ col.name }}: Optional[bool] = None
|
|
31
|
+
{% elif col.type == 'DateTime' %}
|
|
32
|
+
{{ col.name }}: Optional[datetime] = None
|
|
33
|
+
{% else %}
|
|
34
|
+
{% if 'email' in col.name %}
|
|
35
|
+
{{ col.name }}: Optional[EmailStr] = None
|
|
36
|
+
{% else %}
|
|
37
|
+
{{ col.name }}: Optional[str] = None
|
|
38
|
+
{% endif %}
|
|
39
|
+
{% endif %}
|
|
40
|
+
{% endif %}
|
|
41
|
+
{% endfor %}
|
|
42
|
+
|
|
43
|
+
class {{ class_name }}Create({{ class_name }}Base):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
class {{ class_name }}Update(BaseModel):
|
|
47
|
+
{% for col in columns %}
|
|
48
|
+
{% if not col.is_pk %}
|
|
49
|
+
{% if col.type == 'Integer' %}
|
|
50
|
+
{% if col.name in ['amount', 'price', 'quantity', 'count', 'total', 'balance', 'score', 'rating', 'age', 'year'] or col.name.endswith(('_amount', '_price', '_qty', '_count', '_total', '_score')) %}
|
|
51
|
+
{{ col.name }}: Optional[int] = Field(default=None, ge=0)
|
|
52
|
+
{% else %}
|
|
53
|
+
{{ col.name }}: Optional[int] = None
|
|
54
|
+
{% endif %}
|
|
55
|
+
{% elif col.type == 'Float' %}
|
|
56
|
+
{% if col.name in ['amount', 'price', 'quantity', 'total', 'balance', 'score', 'rating', 'weight', 'cost'] or col.name.endswith(('_amount', '_price', '_qty', '_total', '_score', '_cost')) %}
|
|
57
|
+
{{ col.name }}: Optional[float] = Field(default=None, ge=0)
|
|
58
|
+
{% else %}
|
|
59
|
+
{{ col.name }}: Optional[float] = None
|
|
60
|
+
{% endif %}
|
|
61
|
+
{% elif col.type == 'Boolean' %}
|
|
62
|
+
{{ col.name }}: Optional[bool] = None
|
|
63
|
+
{% elif col.type == 'DateTime' %}
|
|
64
|
+
{{ col.name }}: Optional[datetime] = None
|
|
65
|
+
{% else %}
|
|
66
|
+
{% if 'email' in col.name %}
|
|
67
|
+
{{ col.name }}: Optional[EmailStr] = None
|
|
68
|
+
{% else %}
|
|
69
|
+
{{ col.name }}: Optional[str] = None
|
|
70
|
+
{% endif %}
|
|
71
|
+
{% endif %}
|
|
72
|
+
{% endif %}
|
|
73
|
+
{% endfor %}
|
|
74
|
+
|
|
75
|
+
class {{ class_name }}Response({{ class_name }}Base):
|
|
76
|
+
id: int
|
|
77
|
+
|
|
78
|
+
model_config = ConfigDict(from_attributes=True)
|
|
79
|
+
|
|
80
|
+
{% endfor %}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
backend:
|
|
5
|
+
build:
|
|
6
|
+
context: ./backend
|
|
7
|
+
dockerfile: Dockerfile
|
|
8
|
+
ports:
|
|
9
|
+
- "{{ port }}:{{ port }}"
|
|
10
|
+
volumes:
|
|
11
|
+
- ./backend:/app
|
|
12
|
+
environment:
|
|
13
|
+
- PORT={{ port }}
|
|
14
|
+
|
|
15
|
+
frontend:
|
|
16
|
+
build:
|
|
17
|
+
context: ./frontend
|
|
18
|
+
dockerfile: Dockerfile
|
|
19
|
+
ports:
|
|
20
|
+
- "5173:5173"
|
|
21
|
+
volumes:
|
|
22
|
+
- ./frontend:/app
|
|
23
|
+
- /app/node_modules
|
|
24
|
+
environment:
|
|
25
|
+
- VITE_API_URL={{ api_url }}
|
|
26
|
+
depends_on:
|
|
27
|
+
- backend
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Proto-CLI Generated UI</title>
|
|
7
|
+
<!-- Google Fonts for premium typography -->
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
11
|
+
</head>
|
|
12
|
+
<body class="bg-slate-950 text-slate-100 antialiased font-sans">
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "proto-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^18.2.0",
|
|
13
|
+
"react-dom": "^18.2.0",
|
|
14
|
+
"react-router-dom": "^6.22.0",
|
|
15
|
+
"lucide-react": "^0.320.0",
|
|
16
|
+
"recharts": "^2.12.0"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/react": "^18.2.55",
|
|
20
|
+
"@types/react-dom": "^18.2.19",
|
|
21
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
22
|
+
"autoprefixer": "^10.4.17",
|
|
23
|
+
"postcss": "^8.4.35",
|
|
24
|
+
"tailwindcss": "^3.4.1",
|
|
25
|
+
"typescript": "^5.2.2",
|
|
26
|
+
"vite": "^5.1.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { BrowserRouter as Router, Routes, Route, Link, useLocation } from 'react-router-dom'
|
|
3
|
+
import { Database, Table, LayoutDashboard, ChevronRight } from 'lucide-react'
|
|
4
|
+
import Dashboard from './Dashboard'
|
|
5
|
+
{% for table_name, columns in schemas.items() %}
|
|
6
|
+
import {{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud from './pages/{{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud'
|
|
7
|
+
{% endfor %}
|
|
8
|
+
|
|
9
|
+
const SidebarLink = ({ to, children, icon: Icon }: { to: string; children: React.ReactNode; icon: any }) => {
|
|
10
|
+
const location = useLocation()
|
|
11
|
+
const isActive = location.pathname === to
|
|
12
|
+
return (
|
|
13
|
+
<Link
|
|
14
|
+
to={to}
|
|
15
|
+
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group ${
|
|
16
|
+
isActive
|
|
17
|
+
? 'bg-gradient-to-r from-brand-600 to-brand-700 text-white font-medium shadow-lg shadow-brand-500/20'
|
|
18
|
+
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-900/60'
|
|
19
|
+
}`}
|
|
20
|
+
>
|
|
21
|
+
<Icon className={`w-5 h-5 transition-transform duration-200 ${isActive ? 'scale-110' : 'group-hover:scale-110'}`} />
|
|
22
|
+
<span className="text-sm font-outfit">{children}</span>
|
|
23
|
+
{isActive && <ChevronRight className="w-4 h-4 ml-auto" />}
|
|
24
|
+
</Link>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function AppContent() {
|
|
29
|
+
return (
|
|
30
|
+
<div className="flex min-h-screen bg-slate-950 text-slate-100 font-sans">
|
|
31
|
+
{/* Sidebar */}
|
|
32
|
+
<aside className="w-64 border-r border-slate-900 bg-slate-950/70 backdrop-blur-xl flex flex-col p-6 fixed h-full z-10">
|
|
33
|
+
<div className="flex items-center gap-3 mb-8 px-2">
|
|
34
|
+
<div className="p-2.5 bg-brand-500/10 text-brand-400 rounded-xl border border-brand-500/20">
|
|
35
|
+
<Database className="w-6 h-6 animate-pulse" />
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<h1 className="font-outfit font-bold text-lg bg-gradient-to-r from-brand-400 to-brand-300 bg-clip-text text-transparent">Proto Console</h1>
|
|
39
|
+
<p className="text-[10px] text-slate-500 tracking-wider uppercase font-semibold">Generated Admin</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<nav className="flex-1 space-y-1.5 overflow-y-auto pr-1">
|
|
44
|
+
<SidebarLink to="/" icon={LayoutDashboard}>Dashboard</SidebarLink>
|
|
45
|
+
<div className="pt-4 pb-2">
|
|
46
|
+
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-widest px-4">Tables</p>
|
|
47
|
+
</div>
|
|
48
|
+
{% for table_name, columns in schemas.items() %}
|
|
49
|
+
<SidebarLink to="/{{ table_name }}" icon={Table}>
|
|
50
|
+
{{ table_name.replace('_', ' ').title() }}
|
|
51
|
+
</SidebarLink>
|
|
52
|
+
{% endfor %}
|
|
53
|
+
</nav>
|
|
54
|
+
|
|
55
|
+
<div className="mt-auto border-t border-slate-900 pt-4 px-2">
|
|
56
|
+
<p className="text-xs text-slate-500">FastAPI + React SPA</p>
|
|
57
|
+
<p className="text-[10px] text-slate-600 mt-1">Status: <span className="text-emerald-500 font-semibold">Online</span></p>
|
|
58
|
+
</div>
|
|
59
|
+
</aside>
|
|
60
|
+
|
|
61
|
+
{/* Main Content Area */}
|
|
62
|
+
<main className="flex-1 ml-64 min-h-screen flex flex-col bg-gradient-to-b from-slate-900/50 to-slate-950">
|
|
63
|
+
<header className="h-16 border-b border-slate-900/50 flex items-center px-8 bg-slate-950/50 backdrop-blur-md sticky top-0 z-20 justify-between">
|
|
64
|
+
<div className="flex items-center gap-2">
|
|
65
|
+
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-ping"></span>
|
|
66
|
+
<span className="text-xs text-slate-400 font-medium">Development Instance</span>
|
|
67
|
+
</div>
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<div className="p-8 flex-1">
|
|
71
|
+
<Routes>
|
|
72
|
+
<Route path="/" element={<Dashboard />} />
|
|
73
|
+
{% for table_name, columns in schemas.items() %}
|
|
74
|
+
<Route path="/{{ table_name }}" element={<{{ table_name.replace('_', ' ').title().replace(' ', '') }}Crud />} />
|
|
75
|
+
{% endfor %}
|
|
76
|
+
</Routes>
|
|
77
|
+
</div>
|
|
78
|
+
</main>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default function App() {
|
|
84
|
+
return (
|
|
85
|
+
<Router>
|
|
86
|
+
<AppContent />
|
|
87
|
+
</Router>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { Link } from 'react-router-dom'
|
|
3
|
+
import { Table, Layers, Hash, Calendar, ArrowRight, RefreshCw, BarChart2 } from 'lucide-react'
|
|
4
|
+
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'
|
|
5
|
+
|
|
6
|
+
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
|
7
|
+
|
|
8
|
+
interface TableMetadata {
|
|
9
|
+
name: string
|
|
10
|
+
count: number
|
|
11
|
+
columns: Array<{ name: string; type: string; is_pk: boolean }>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function Dashboard() {
|
|
15
|
+
const [metadata, setMetadata] = useState<TableMetadata[]>([])
|
|
16
|
+
const [loading, setLoading] = useState(true)
|
|
17
|
+
const [error, setError] = useState<string | null>(null)
|
|
18
|
+
|
|
19
|
+
const fetchMetadata = async () => {
|
|
20
|
+
try {
|
|
21
|
+
setLoading(true)
|
|
22
|
+
const res = await fetch(`${API_BASE}/api/metadata`)
|
|
23
|
+
if (!res.ok) throw new Error('Failed to fetch system metadata')
|
|
24
|
+
const data = await res.json()
|
|
25
|
+
setMetadata(data)
|
|
26
|
+
setError(null)
|
|
27
|
+
} catch (err: any) {
|
|
28
|
+
setError(err.message || 'Something went wrong')
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
fetchMetadata()
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const totalTables = metadata.length
|
|
39
|
+
const totalRows = metadata.reduce((acc, curr) => acc + curr.count, 0)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-8">
|
|
43
|
+
{/* Top Banner */}
|
|
44
|
+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
45
|
+
<div>
|
|
46
|
+
<h2 className="text-3xl font-outfit font-bold tracking-tight bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">
|
|
47
|
+
System Overview
|
|
48
|
+
</h2>
|
|
49
|
+
<p className="text-slate-400 mt-1 text-sm font-sans">
|
|
50
|
+
Overview of autogenerated tables, records, and database stats.
|
|
51
|
+
</p>
|
|
52
|
+
</div>
|
|
53
|
+
<button
|
|
54
|
+
onClick={fetchMetadata}
|
|
55
|
+
className="flex items-center gap-2 px-4 py-2 bg-slate-900 border border-slate-800 rounded-xl text-xs font-semibold hover:bg-slate-800 transition-all text-slate-300"
|
|
56
|
+
>
|
|
57
|
+
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
|
|
58
|
+
Refresh Stats
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Stats Grid */}
|
|
63
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
64
|
+
<div className="p-6 bg-slate-900/40 border border-slate-900 rounded-2xl relative overflow-hidden group hover:border-slate-800 transition-all duration-300">
|
|
65
|
+
<div className="absolute top-0 right-0 p-8 text-brand-500/10 transition-transform duration-300 group-hover:scale-110">
|
|
66
|
+
<Layers className="w-24 h-24" />
|
|
67
|
+
</div>
|
|
68
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-widest">Total Tables</p>
|
|
69
|
+
<p className="text-4xl font-bold font-outfit mt-2">{totalTables}</p>
|
|
70
|
+
<p className="text-xs text-slate-400 mt-2">Discovered in schema parse</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div className="p-6 bg-slate-900/40 border border-slate-900 rounded-2xl relative overflow-hidden group hover:border-slate-800 transition-all duration-300">
|
|
74
|
+
<div className="absolute top-0 right-0 p-8 text-brand-500/10 transition-transform duration-300 group-hover:scale-110">
|
|
75
|
+
<Hash className="w-24 h-24" />
|
|
76
|
+
</div>
|
|
77
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-widest">Total Rows</p>
|
|
78
|
+
<p className="text-4xl font-bold font-outfit mt-2">{totalRows}</p>
|
|
79
|
+
<p className="text-xs text-slate-400 mt-2">Stored across all tables</p>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="p-6 bg-slate-900/40 border border-slate-900 rounded-2xl relative overflow-hidden group hover:border-slate-800 transition-all duration-300 md:col-span-2 lg:col-span-1">
|
|
83
|
+
<div className="absolute top-0 right-0 p-8 text-emerald-500/10 transition-transform duration-300 group-hover:scale-110">
|
|
84
|
+
<Calendar className="w-24 h-24" />
|
|
85
|
+
</div>
|
|
86
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-widest">Server Status</p>
|
|
87
|
+
<p className="text-4xl font-bold font-outfit mt-2">Active</p>
|
|
88
|
+
<p className="text-xs text-slate-400 mt-2 flex items-center gap-1.5">
|
|
89
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500"></span> Live sync connection
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Row Distribution Chart */}
|
|
95
|
+
{!loading && !error && metadata.length > 0 && (
|
|
96
|
+
<div className="p-6 bg-slate-900/30 border border-slate-900 rounded-2xl">
|
|
97
|
+
<h3 className="text-lg font-bold font-outfit text-slate-200 mb-6 flex items-center gap-2">
|
|
98
|
+
<BarChart2 className="w-5 h-5 text-brand-400" />
|
|
99
|
+
Record Distribution
|
|
100
|
+
</h3>
|
|
101
|
+
<div className="h-72 w-full">
|
|
102
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
103
|
+
<BarChart data={metadata} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
|
104
|
+
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
|
|
105
|
+
<XAxis
|
|
106
|
+
dataKey="name"
|
|
107
|
+
stroke="#64748b"
|
|
108
|
+
fontSize={12}
|
|
109
|
+
tickLine={false}
|
|
110
|
+
axisLine={false}
|
|
111
|
+
tickFormatter={(val) => val.replace('_', ' ')}
|
|
112
|
+
/>
|
|
113
|
+
<YAxis
|
|
114
|
+
stroke="#64748b"
|
|
115
|
+
fontSize={12}
|
|
116
|
+
tickLine={false}
|
|
117
|
+
axisLine={false}
|
|
118
|
+
tickFormatter={(val) => val >= 1000 ? `${(val / 1000).toFixed(1)}k` : val}
|
|
119
|
+
/>
|
|
120
|
+
<Tooltip
|
|
121
|
+
cursor={{ fill: '#0f172a' }}
|
|
122
|
+
contentStyle={{ backgroundColor: '#0f172a', borderColor: '#1e293b', borderRadius: '0.75rem' }}
|
|
123
|
+
itemStyle={{ color: '#38bdf8' }}
|
|
124
|
+
labelStyle={{ color: '#94a3b8', textTransform: 'capitalize', marginBottom: '4px' }}
|
|
125
|
+
formatter={(value: number) => [value, 'Records']}
|
|
126
|
+
labelFormatter={(label: string) => label.replace('_', ' ')}
|
|
127
|
+
/>
|
|
128
|
+
<Bar
|
|
129
|
+
dataKey="count"
|
|
130
|
+
fill="#0ea5e9"
|
|
131
|
+
radius={[4, 4, 0, 0]}
|
|
132
|
+
barSize={40}
|
|
133
|
+
animationDuration={1500}
|
|
134
|
+
/>
|
|
135
|
+
</BarChart>
|
|
136
|
+
</ResponsiveContainer>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{/* Tables section */}
|
|
142
|
+
<div>
|
|
143
|
+
<h3 className="text-lg font-bold font-outfit text-slate-200 mb-6 flex items-center gap-2">
|
|
144
|
+
<Table className="w-5 h-5 text-brand-400" />
|
|
145
|
+
Entity Navigator
|
|
146
|
+
</h3>
|
|
147
|
+
|
|
148
|
+
{loading ? (
|
|
149
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
150
|
+
{[1, 2].map((n) => (
|
|
151
|
+
<div key={n} className="h-48 rounded-2xl bg-slate-900/20 border border-slate-900/50 animate-pulse" />
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
) : error ? (
|
|
155
|
+
<div className="p-6 border border-red-500/20 bg-red-500/5 text-red-400 rounded-2xl">
|
|
156
|
+
<p className="font-semibold text-sm">Failed to connect to backend server</p>
|
|
157
|
+
<p className="text-xs mt-1 text-red-400/80">Make sure the FastAPI backend is running on {API_BASE}.</p>
|
|
158
|
+
</div>
|
|
159
|
+
) : (
|
|
160
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
161
|
+
{metadata.map((table) => (
|
|
162
|
+
<div
|
|
163
|
+
key={table.name}
|
|
164
|
+
className="p-6 bg-slate-900/30 border border-slate-900 rounded-2xl hover:border-slate-800 transition-all duration-300 flex flex-col justify-between group"
|
|
165
|
+
>
|
|
166
|
+
<div>
|
|
167
|
+
<div className="flex items-center justify-between mb-4">
|
|
168
|
+
<h4 className="text-lg font-bold text-white group-hover:text-brand-400 transition-colors capitalize font-outfit">
|
|
169
|
+
{table.name.replace('_', ' ')}
|
|
170
|
+
</h4>
|
|
171
|
+
<span className="px-2.5 py-1 bg-brand-500/10 text-brand-400 border border-brand-500/10 rounded-lg text-xs font-semibold">
|
|
172
|
+
{table.count} row{table.count !== 1 ? 's' : ''}
|
|
173
|
+
</span>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<p className="text-xs text-slate-400 mb-4">
|
|
177
|
+
Contains {table.columns.length} columns:
|
|
178
|
+
</p>
|
|
179
|
+
|
|
180
|
+
<div className="flex flex-wrap gap-1.5 mb-6">
|
|
181
|
+
{table.columns.map((col) => (
|
|
182
|
+
<span
|
|
183
|
+
key={col.name}
|
|
184
|
+
className={`text-[10px] px-2 py-0.5 rounded-md font-mono ${
|
|
185
|
+
col.is_pk
|
|
186
|
+
? 'bg-amber-500/10 text-amber-400 border border-amber-500/20'
|
|
187
|
+
: 'bg-slate-800 text-slate-300'
|
|
188
|
+
}`}
|
|
189
|
+
>
|
|
190
|
+
{col.name}: {col.type}
|
|
191
|
+
</span>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<Link
|
|
197
|
+
to={`/${table.name}`}
|
|
198
|
+
className="flex items-center gap-2 text-xs font-semibold text-slate-300 hover:text-white mt-auto group/btn transition-colors"
|
|
199
|
+
>
|
|
200
|
+
Manage Records
|
|
201
|
+
<ArrowRight className="w-3.5 h-3.5 transform group-hover/btn:translate-x-1 transition-transform" />
|
|
202
|
+
</Link>
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
}
|