1use crate::{error::Result, state::AppState};
4use axum::{
5 extract::{Path, State},
6 response::{Html, IntoResponse},
7 routing::{get, post},
8 Json, Router,
9};
10use serde::{Deserialize, Serialize};
11use std::sync::Arc;
12
13pub fn routes(state: Arc<AppState>) -> Router {
14 Router::new()
15 .route("/", get(database_list_page))
16 .route("/list", get(list_databases))
17 .route("/:db_id", get(database_detail))
18 .route("/:db_id/query", post(execute_query))
19 .route("/:db_id/collections", get(list_collections))
20 .with_state(state)
21}
22
23async fn database_list_page() -> impl IntoResponse {
24 Html(DATABASE_LIST_HTML)
25}
26
27#[derive(Serialize)]
28struct DatabaseInfo {
29 id: String,
30 name: String,
31 region: String,
32 collections: usize,
33 size_bytes: u64,
34 created_at: String,
35}
36
37async fn list_databases(State(_state): State<Arc<AppState>>) -> Result<Json<Vec<DatabaseInfo>>> {
38 let databases = vec![
40 DatabaseInfo {
41 id: "db_prod_001".to_string(),
42 name: "production".to_string(),
43 region: "sa-east-1".to_string(),
44 collections: 15,
45 size_bytes: 2_500_000_000,
46 created_at: "2024-11-01T10:00:00Z".to_string(),
47 },
48 DatabaseInfo {
49 id: "db_dev_001".to_string(),
50 name: "development".to_string(),
51 region: "sa-east-1".to_string(),
52 collections: 8,
53 size_bytes: 500_000_000,
54 created_at: "2024-11-15T14:30:00Z".to_string(),
55 },
56 DatabaseInfo {
57 id: "db_test_001".to_string(),
58 name: "testing".to_string(),
59 region: "sa-east-1".to_string(),
60 collections: 5,
61 size_bytes: 100_000_000,
62 created_at: "2024-11-20T09:00:00Z".to_string(),
63 },
64 ];
65
66 Ok(Json(databases))
67}
68
69async fn database_detail(
70 Path(db_id): Path<String>,
71 State(_state): State<Arc<AppState>>,
72) -> Result<Json<DatabaseInfo>> {
73 Ok(Json(DatabaseInfo {
75 id: db_id,
76 name: "production".to_string(),
77 region: "sa-east-1".to_string(),
78 collections: 15,
79 size_bytes: 2_500_000_000,
80 created_at: "2024-11-01T10:00:00Z".to_string(),
81 }))
82}
83
84#[derive(Deserialize)]
85struct QueryRequest {
86 query: String,
87}
88
89#[derive(Serialize)]
90struct QueryResult {
91 rows: Vec<serde_json::Value>,
92 count: usize,
93 execution_time_ms: u64,
94}
95
96async fn execute_query(
97 Path(_db_id): Path<String>,
98 State(_state): State<Arc<AppState>>,
99 Json(req): Json<QueryRequest>,
100) -> Result<Json<QueryResult>> {
101 tracing::info!("Executing query: {}", req.query);
103
104 Ok(Json(QueryResult {
105 rows: vec![
106 serde_json::json!({"id": 1, "name": "Item 1"}),
107 serde_json::json!({"id": 2, "name": "Item 2"}),
108 ],
109 count: 2,
110 execution_time_ms: 15,
111 }))
112}
113
114#[derive(Serialize)]
115struct CollectionInfo {
116 name: String,
117 partition_key: String,
118 document_count: usize,
119 size_bytes: u64,
120}
121
122async fn list_collections(
123 Path(_db_id): Path<String>,
124 State(_state): State<Arc<AppState>>,
125) -> Result<Json<Vec<CollectionInfo>>> {
126 let collections = vec![
128 CollectionInfo {
129 name: "users".to_string(),
130 partition_key: "userId".to_string(),
131 document_count: 10_000,
132 size_bytes: 50_000_000,
133 },
134 CollectionInfo {
135 name: "orders".to_string(),
136 partition_key: "customerId".to_string(),
137 document_count: 50_000,
138 size_bytes: 200_000_000,
139 },
140 ];
141
142 Ok(Json(collections))
143}
144
145const DATABASE_LIST_HTML: &str = r#"<!DOCTYPE html>
146<html lang="pt-BR">
147<head>
148 <meta charset="UTF-8">
149 <meta name="viewport" content="width=device-width, initial-scale=1.0">
150 <title>AvilaDB Explorer - AVL Console</title>
151 <style>
152 * { margin: 0; padding: 0; box-sizing: border-box; }
153 body {
154 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
155 background: #0a0e1a;
156 color: #e0e6ed;
157 }
158 .header {
159 background: #0f1419;
160 border-bottom: 1px solid #1a1f2e;
161 padding: 1rem 2rem;
162 }
163 .container { max-width: 1400px; margin: 2rem auto; padding: 0 2rem; }
164 .db-card {
165 background: #0f1419;
166 border: 1px solid #1a1f2e;
167 border-radius: 8px;
168 padding: 1.5rem;
169 margin-bottom: 1rem;
170 cursor: pointer;
171 transition: all 0.2s;
172 }
173 .db-card:hover {
174 border-color: #00d4ff;
175 transform: translateX(4px);
176 }
177 .db-name {
178 font-size: 1.25rem;
179 font-weight: bold;
180 color: #00d4ff;
181 margin-bottom: 0.5rem;
182 }
183 .db-info {
184 display: flex;
185 gap: 2rem;
186 font-size: 0.875rem;
187 color: #8b92a0;
188 }
189 .query-editor {
190 background: #0f1419;
191 border: 1px solid #1a1f2e;
192 border-radius: 8px;
193 padding: 1.5rem;
194 margin-top: 2rem;
195 }
196 textarea {
197 width: 100%;
198 min-height: 150px;
199 background: #0a0e1a;
200 border: 1px solid #1a1f2e;
201 border-radius: 4px;
202 padding: 1rem;
203 color: #e0e6ed;
204 font-family: 'Courier New', monospace;
205 font-size: 0.875rem;
206 resize: vertical;
207 }
208 button {
209 background: #00d4ff;
210 color: #0a0e1a;
211 border: none;
212 padding: 0.75rem 1.5rem;
213 border-radius: 4px;
214 font-weight: bold;
215 cursor: pointer;
216 margin-top: 1rem;
217 }
218 button:hover { background: #00b8e6; }
219 .results {
220 background: #0f1419;
221 border: 1px solid #1a1f2e;
222 border-radius: 8px;
223 padding: 1.5rem;
224 margin-top: 1rem;
225 }
226 pre {
227 background: #0a0e1a;
228 padding: 1rem;
229 border-radius: 4px;
230 overflow-x: auto;
231 }
232 </style>
233</head>
234<body>
235 <div class="header">
236 <h1>🗄️ AvilaDB Explorer</h1>
237 </div>
238
239 <div class="container">
240 <h2 style="margin-bottom: 1rem;">Seus Bancos de Dados</h2>
241 <div id="databases"></div>
242
243 <div class="query-editor">
244 <h3 style="margin-bottom: 1rem;">Query Editor</h3>
245 <textarea id="query" placeholder="SELECT * FROM users WHERE active = true">SELECT * FROM users LIMIT 10</textarea>
246 <button onclick="executeQuery()">Executar Query</button>
247 </div>
248
249 <div class="results" id="results" style="display: none;">
250 <h3>Resultados</h3>
251 <pre id="resultData"></pre>
252 </div>
253 </div>
254
255 <script>
256 async function loadDatabases() {
257 const res = await fetch('/databases/list');
258 const databases = await res.json();
259 const container = document.getElementById('databases');
260 container.innerHTML = databases.map(db => `
261 <div class="db-card">
262 <div class="db-name">${db.name}</div>
263 <div class="db-info">
264 <span>ID: ${db.id}</span>
265 <span>Região: ${db.region}</span>
266 <span>Coleções: ${db.collections}</span>
267 <span>Tamanho: ${(db.size_bytes / 1_000_000).toFixed(1)} MB</span>
268 </div>
269 </div>
270 `).join('');
271 }
272
273 async function executeQuery() {
274 const query = document.getElementById('query').value;
275 const res = await fetch('/databases/db_prod_001/query', {
276 method: 'POST',
277 headers: { 'Content-Type': 'application/json' },
278 body: JSON.stringify({ query })
279 });
280 const result = await res.json();
281 document.getElementById('results').style.display = 'block';
282 document.getElementById('resultData').textContent = JSON.stringify(result, null, 2);
283 }
284
285 loadDatabases();
286 </script>
287</body>
288</html>"#;