axiodb 22.2.2 → 22.4.2

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.
Files changed (74) hide show
  1. package/electron/electron-builder.json +58 -0
  2. package/electron/main/main.cts +585 -0
  3. package/electron/main/preload.cts +51 -0
  4. package/electron/package-lock.json +8123 -0
  5. package/electron/package.json +52 -0
  6. package/electron/public/AXioDB.png +0 -0
  7. package/electron/resources/after-install.sh +45 -0
  8. package/electron/resources/after-remove.sh +29 -0
  9. package/electron/resources/icon.png +0 -0
  10. package/electron/resources/icons/128x128.png +0 -0
  11. package/electron/resources/icons/16x16.png +0 -0
  12. package/electron/resources/icons/24x24.png +0 -0
  13. package/electron/resources/icons/256x256.png +0 -0
  14. package/electron/resources/icons/32x32.png +0 -0
  15. package/electron/resources/icons/48x48.png +0 -0
  16. package/electron/resources/icons/512x512.png +0 -0
  17. package/electron/resources/icons/64x64.png +0 -0
  18. package/electron/src/App.jsx +128 -0
  19. package/electron/src/api/authApi.js +83 -0
  20. package/electron/src/api/client.js +102 -0
  21. package/electron/src/assets/AXioDB.png +0 -0
  22. package/electron/src/components/auth/CreateRoleModal.jsx +189 -0
  23. package/electron/src/components/auth/CreateUserModal.jsx +131 -0
  24. package/electron/src/components/auth/ForcePasswordChangeModal.jsx +132 -0
  25. package/electron/src/components/auth/ProtectedRoute.jsx +42 -0
  26. package/electron/src/components/auth/ResetPasswordModal.jsx +90 -0
  27. package/electron/src/components/auth/UserAvatarMenu.jsx +103 -0
  28. package/electron/src/components/collection/CreateCollectionModal.jsx +116 -0
  29. package/electron/src/components/collection/DeleteCollectionModal.jsx +85 -0
  30. package/electron/src/components/collection/SchemaViewModal.jsx +369 -0
  31. package/electron/src/components/dashboard/CollectionsChart.jsx +94 -0
  32. package/electron/src/components/dashboard/DatabaseTreeView.jsx +130 -0
  33. package/electron/src/components/dashboard/InMemoryCacheCard.jsx +60 -0
  34. package/electron/src/components/dashboard/StorageDonut.jsx +76 -0
  35. package/electron/src/components/dashboard/StorageUsageCard.jsx +59 -0
  36. package/electron/src/components/dashboard/TotalCollectionsCard.jsx +47 -0
  37. package/electron/src/components/dashboard/TotalDatabasesCard.jsx +43 -0
  38. package/electron/src/components/dashboard/TotalDocumentsCard.jsx +44 -0
  39. package/electron/src/components/database/CreateDatabaseModal.jsx +109 -0
  40. package/electron/src/components/database/DeleteDatabaseModal.jsx +97 -0
  41. package/electron/src/components/query/CodeEditor.jsx +343 -0
  42. package/electron/src/components/query/ObjectEditor.jsx +115 -0
  43. package/electron/src/components/query/ObjectView.jsx +44 -0
  44. package/electron/src/components/query/QueryEditor.jsx +32 -0
  45. package/electron/src/components/query/queryLanguage.js +755 -0
  46. package/electron/src/components/ui/Button.jsx +58 -0
  47. package/electron/src/components/ui/Card.jsx +29 -0
  48. package/electron/src/components/ui/ErrorBoundary.jsx +108 -0
  49. package/electron/src/components/ui/Feedback.jsx +66 -0
  50. package/electron/src/components/ui/Field.jsx +65 -0
  51. package/electron/src/components/ui/MetricCard.jsx +104 -0
  52. package/electron/src/components/ui/Modal.jsx +119 -0
  53. package/electron/src/components/ui/Page.jsx +40 -0
  54. package/electron/src/config/key.js +11 -0
  55. package/electron/src/index.css +181 -0
  56. package/electron/src/index.html +13 -0
  57. package/electron/src/layout/Sidebar.jsx +478 -0
  58. package/electron/src/layout/StatusBar.jsx +70 -0
  59. package/electron/src/layout/Titlebar.jsx +128 -0
  60. package/electron/src/main.jsx +42 -0
  61. package/electron/src/pages/ConnectionHub.jsx +464 -0
  62. package/electron/src/pages/Dashboard.jsx +128 -0
  63. package/electron/src/pages/Documents.jsx +823 -0
  64. package/electron/src/pages/Import.jsx +527 -0
  65. package/electron/src/pages/UserManagement.jsx +294 -0
  66. package/electron/src/pages/Welcome.jsx +157 -0
  67. package/electron/src/store/authStore.js +30 -0
  68. package/electron/src/store/connectionStore.js +103 -0
  69. package/electron/src/store/dbStore.js +165 -0
  70. package/electron/src/store/store.js +8 -0
  71. package/electron/src/utils/format.js +39 -0
  72. package/electron/vite.config.js +28 -0
  73. package/lib/Services/Indexation.operation.js +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,464 @@
1
+ import React, { useState } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { useConnectionStore } from "../store/connectionStore";
4
+ import { useAuthStore } from "../store/authStore";
5
+ import authApi from "../api/authApi";
6
+ import axioLogo from "../assets/AXioDB.png";
7
+
8
+ const ConnectionHub = ({ onConnected, onForcePasswordChange }) => {
9
+ const {
10
+ protocol,
11
+ host,
12
+ port,
13
+ username,
14
+ password,
15
+ name,
16
+ savedConnections,
17
+ setConnectionParams,
18
+ saveConnection,
19
+ deleteSavedConnection,
20
+ setIsConnected,
21
+ setPingLatency,
22
+ } = useConnectionStore();
23
+
24
+ const { setSession } = useAuthStore();
25
+
26
+ const [formData, setFormData] = useState({
27
+ protocol: protocol || "http",
28
+ host: host || "localhost",
29
+ port: port || 27018,
30
+ username: username || "admin",
31
+ password: password || "",
32
+ name: name || "Local Instance",
33
+ saveToFavorites: true,
34
+ });
35
+
36
+ const [showPassword, setShowPassword] = useState(false);
37
+ const [isTesting, setIsTesting] = useState(false);
38
+ const [testResult, setTestResult] = useState(null);
39
+ const [isConnecting, setIsConnecting] = useState(false);
40
+ const [connectingStep, setConnectingStep] = useState("");
41
+ const [connectionError, setConnectionError] = useState("");
42
+
43
+ const handleChange = (e) => {
44
+ const { name, value, type, checked } = e.target;
45
+ setFormData((prev) => ({
46
+ ...prev,
47
+ [name]: type === "checkbox" ? checked : value,
48
+ }));
49
+ setConnectionError("");
50
+ setTestResult(null);
51
+ };
52
+
53
+ const handleSelectSaved = (conn) => {
54
+ setFormData({
55
+ protocol: conn.protocol || "http",
56
+ host: conn.host || "localhost",
57
+ port: conn.port || 27018,
58
+ username: conn.username || "admin",
59
+ password: conn.password || "",
60
+ name: conn.name || "Saved Connection",
61
+ saveToFavorites: true,
62
+ });
63
+ setConnectionError("");
64
+ setTestResult(null);
65
+ };
66
+
67
+ const handleTestConnection = async () => {
68
+ setIsTesting(true);
69
+ setTestResult(null);
70
+ setConnectionError("");
71
+
72
+ setConnectionParams({
73
+ protocol: formData.protocol,
74
+ host: formData.host,
75
+ port: Number(formData.port),
76
+ username: formData.username,
77
+ name: formData.name,
78
+ });
79
+
80
+ try {
81
+ const res = await authApi.ping();
82
+ setTestResult({
83
+ success: true,
84
+ message: `Connected successfully (${res.latency}ms)`,
85
+ latency: res.latency,
86
+ });
87
+ } catch (err) {
88
+ setTestResult({
89
+ success: false,
90
+ message: err.response?.data?.message || err.message || "Failed to reach server at specified host:port",
91
+ });
92
+ } finally {
93
+ setIsTesting(false);
94
+ }
95
+ };
96
+
97
+ const handleConnect = async (e) => {
98
+ e.preventDefault();
99
+ setConnectionError("");
100
+ setIsConnecting(true);
101
+
102
+ setConnectionParams({
103
+ protocol: formData.protocol,
104
+ host: formData.host,
105
+ port: Number(formData.port),
106
+ username: formData.username,
107
+ password: formData.password,
108
+ name: formData.name,
109
+ });
110
+
111
+ try {
112
+ setConnectingStep("Pinging AxioDB server...");
113
+ const pingRes = await authApi.ping();
114
+ setPingLatency(pingRes.latency);
115
+
116
+ setConnectingStep("Authenticating credentials...");
117
+ const loginRes = await authApi.login({
118
+ username: formData.username,
119
+ password: formData.password,
120
+ });
121
+
122
+ if (formData.saveToFavorites) {
123
+ saveConnection({
124
+ id: crypto.randomUUID(),
125
+ name: formData.name,
126
+ protocol: formData.protocol,
127
+ host: formData.host,
128
+ port: Number(formData.port),
129
+ username: formData.username,
130
+ password: formData.password,
131
+ });
132
+ }
133
+
134
+ setConnectingStep("Syncing session...");
135
+ setSession({
136
+ username: formData.username,
137
+ role: loginRes.role || "super_admin",
138
+ permissions: loginRes.permissions || [],
139
+ mustChangePassword: loginRes.mustChangePassword || false,
140
+ });
141
+
142
+ if (loginRes.mustChangePassword) {
143
+ setIsConnecting(false);
144
+ setIsConnected(true);
145
+ if (onForcePasswordChange) onForcePasswordChange();
146
+ return;
147
+ }
148
+
149
+ setIsConnecting(false);
150
+ setIsConnected(true);
151
+ if (onConnected) onConnected();
152
+ } catch (err) {
153
+ console.error("Connection error:", err);
154
+ setIsConnecting(false);
155
+ const serverMsg = err.response?.data?.message;
156
+ if (err.response?.status === 401) {
157
+ setConnectionError(serverMsg || "Invalid username or password. Please verify your credentials.");
158
+ } else if (err.response?.status === 403) {
159
+ setConnectionError(serverMsg || "Access denied (403). Account lacks connection permissions.");
160
+ } else {
161
+ setConnectionError(
162
+ serverMsg ||
163
+ err.message ||
164
+ "Could not connect to AxioDB. Ensure the HTTP server is running on the specified port."
165
+ );
166
+ }
167
+ }
168
+ };
169
+
170
+ return (
171
+ <div className="flex-1 flex overflow-hidden bg-slate-50 font-sans select-none">
172
+ {/* Left Sidebar: Saved Connections */}
173
+ <aside className="w-80 border-r border-slate-200 bg-white flex flex-col justify-between shrink-0">
174
+ <div className="p-4 border-b border-slate-200">
175
+ <div className="flex items-center justify-between">
176
+ <h2 className="text-xs font-bold uppercase tracking-wider text-slate-500">
177
+ Saved Connections
178
+ </h2>
179
+ <span className="text-[11px] font-mono text-slate-500 bg-slate-100 px-2 py-0.5 rounded-full border border-slate-200">
180
+ {savedConnections.length}
181
+ </span>
182
+ </div>
183
+ </div>
184
+
185
+ <div className="flex-1 overflow-y-auto p-3 space-y-2">
186
+ {savedConnections.length === 0 ? (
187
+ <div className="p-6 text-center">
188
+ <div className="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-2.5">
189
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
190
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M13 10V3L4 14h7v7l9-11h-7z" />
191
+ </svg>
192
+ </div>
193
+ <p className="text-xs text-slate-600 font-medium">No saved connections yet</p>
194
+ <p className="text-[11px] text-slate-400 mt-1 leading-relaxed">
195
+ Connections you save will appear here for 1-click access.
196
+ </p>
197
+ </div>
198
+ ) : (
199
+ savedConnections.map((conn) => (
200
+ <div
201
+ key={conn.id}
202
+ onClick={() => handleSelectSaved(conn)}
203
+ className="group relative p-3 rounded-xl bg-slate-50 border border-slate-200 hover:border-emerald-500/80 hover:bg-white transition-all cursor-pointer shadow-2xs"
204
+ >
205
+ <div className="flex items-start justify-between gap-2">
206
+ <div className="min-w-0">
207
+ <p className="text-xs font-bold text-slate-800 truncate group-hover:text-emerald-700 transition-colors">
208
+ {conn.name}
209
+ </p>
210
+ <p className="text-[11px] font-mono text-slate-500 truncate mt-0.5">
211
+ {conn.protocol}://{conn.host}:{conn.port}
212
+ </p>
213
+ <p className="text-[10px] text-slate-400 mt-1">User: {conn.username}</p>
214
+ {conn.password ? (
215
+ <div className="flex items-center gap-1 mt-1">
216
+ <svg className="w-3 h-3 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
217
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-7H2v7a2 2 0 002 2zM10 11V7a2 2 0 012-2h2a2 2 0 012 2v4m-6 0h6m-6 0l1.5-1.5" />
218
+ </svg>
219
+ <span className="text-[10px] text-emerald-600 font-medium">Password saved</span>
220
+ </div>
221
+ ) : null}
222
+ </div>
223
+ <button
224
+ onClick={(e) => {
225
+ e.stopPropagation();
226
+ deleteSavedConnection(conn.id);
227
+ }}
228
+ className="opacity-0 group-hover:opacity-100 p-1 rounded text-slate-400 hover:text-red-600 hover:bg-red-50 transition-all"
229
+ title="Delete saved connection"
230
+ >
231
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
232
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
233
+ </svg>
234
+ </button>
235
+ </div>
236
+ </div>
237
+ ))
238
+ )}
239
+ </div>
240
+ </aside>
241
+
242
+ {/* Main Connection Form Panel */}
243
+ <main className="flex-1 overflow-y-auto flex items-center justify-center p-8 bg-slate-50">
244
+ <div className="max-w-xl w-full">
245
+ {/* Card Header */}
246
+ <div className="mb-6 flex items-center justify-between">
247
+ <div className="flex items-center gap-3">
248
+ <div className="h-12 w-12 rounded-2xl bg-emerald-50 border border-emerald-200 flex items-center justify-center p-2 shadow-xs">
249
+ <img src={axioLogo} alt="AxioDB Logo" className="h-full w-full object-contain" />
250
+ </div>
251
+ <div>
252
+ <h1 className="text-xl font-bold tracking-tight text-slate-900">Connect to AxioDB</h1>
253
+ <p className="text-xs text-slate-500 mt-0.5">Connect via HTTP to manage your databases and collections</p>
254
+ </div>
255
+ </div>
256
+ <span className="text-[10px] uppercase font-mono px-2 py-1 rounded bg-white border border-slate-200 text-slate-600 shadow-2xs">
257
+ Default Port: 27018
258
+ </span>
259
+ </div>
260
+
261
+ {/* Form Box */}
262
+ <div className="rounded-2xl bg-white border border-slate-200 p-6 shadow-md relative overflow-hidden">
263
+ {/* Animated Connecting Overlay */}
264
+ <AnimatePresence>
265
+ {isConnecting && (
266
+ <motion.div
267
+ initial={{ opacity: 0 }}
268
+ animate={{ opacity: 1 }}
269
+ exit={{ opacity: 0 }}
270
+ className="absolute inset-0 bg-white/95 z-20 flex flex-col items-center justify-center p-6 backdrop-blur-xs"
271
+ >
272
+ <div className="relative mb-6">
273
+ <div className="h-24 w-24 rounded-full border border-emerald-500/20 animate-ping absolute inset-0" />
274
+ <div className="h-24 w-24 rounded-full border border-emerald-500/40 animate-pulseGlow" />
275
+ <div className="absolute inset-0 flex items-center justify-center">
276
+ <img src={axioLogo} alt="AxioDB" className="h-10 w-10 rounded-xl animate-bounce" />
277
+ </div>
278
+ </div>
279
+ <h3 className="text-base font-bold text-slate-900 tracking-tight">Connecting to AxioDB</h3>
280
+ <p className="text-xs font-mono text-emerald-700 mt-2 text-center animate-pulse font-medium">
281
+ {connectingStep || "Establishing handshake..."}
282
+ </p>
283
+ </motion.div>
284
+ )}
285
+ </AnimatePresence>
286
+
287
+ <form onSubmit={handleConnect} className="space-y-4">
288
+ {/* Nickname & Protocol */}
289
+ <div className="grid grid-cols-3 gap-3">
290
+ <div className="col-span-2">
291
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">Connection Name</label>
292
+ <input
293
+ type="text"
294
+ name="name"
295
+ value={formData.name}
296
+ onChange={handleChange}
297
+ placeholder="e.g. Local Dev Cluster"
298
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 text-xs focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500/25"
299
+ />
300
+ </div>
301
+ <div>
302
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">Protocol</label>
303
+ <select
304
+ name="protocol"
305
+ value={formData.protocol}
306
+ onChange={handleChange}
307
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 text-xs focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500/25 cursor-pointer"
308
+ >
309
+ <option value="http">HTTP</option>
310
+ <option value="https">HTTPS</option>
311
+ </select>
312
+ </div>
313
+ </div>
314
+
315
+ {/* Host & Port */}
316
+ <div className="grid grid-cols-3 gap-3">
317
+ <div className="col-span-2">
318
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">Host / IP / Domain</label>
319
+ <input
320
+ type="text"
321
+ name="host"
322
+ value={formData.host}
323
+ onChange={handleChange}
324
+ placeholder="localhost or 192.168.1.100"
325
+ required
326
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 font-mono text-xs focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500/25"
327
+ />
328
+ </div>
329
+ <div>
330
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">Port</label>
331
+ <input
332
+ type="number"
333
+ name="port"
334
+ value={formData.port}
335
+ onChange={handleChange}
336
+ placeholder="27018"
337
+ required
338
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 font-mono text-xs focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500/25"
339
+ />
340
+ </div>
341
+ </div>
342
+
343
+ {/* Username & Password */}
344
+ <div className="grid grid-cols-2 gap-3">
345
+ <div>
346
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">Username</label>
347
+ <input
348
+ type="text"
349
+ name="username"
350
+ value={formData.username}
351
+ onChange={handleChange}
352
+ placeholder="admin"
353
+ required
354
+ className="w-full px-3 py-2 rounded-lg bg-white border border-slate-300 text-slate-900 text-xs focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500/25"
355
+ />
356
+ </div>
357
+ <div>
358
+ <label className="block text-xs font-semibold text-slate-700 mb-1.5">Password</label>
359
+ <div className="relative">
360
+ <input
361
+ type={showPassword ? "text" : "password"}
362
+ name="password"
363
+ value={formData.password}
364
+ onChange={handleChange}
365
+ placeholder="••••••••"
366
+ required
367
+ className="w-full px-3 py-2 pr-8 rounded-lg bg-white border border-slate-300 text-slate-900 text-xs focus:border-emerald-500 focus:outline-none focus:ring-1 focus:ring-emerald-500/25"
368
+ />
369
+ <button
370
+ type="button"
371
+ onClick={() => setShowPassword(!showPassword)}
372
+ className="absolute right-2 top-2 text-slate-400 hover:text-slate-600"
373
+ >
374
+ {showPassword ? (
375
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
376
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l18 18" />
377
+ </svg>
378
+ ) : (
379
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
380
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
381
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
382
+ </svg>
383
+ )}
384
+ </button>
385
+ </div>
386
+ </div>
387
+ </div>
388
+
389
+ {/* Save to Favorites checkbox */}
390
+ <div className="pt-1">
391
+ <label className="flex items-center gap-2 text-xs text-slate-600 cursor-pointer">
392
+ <input
393
+ type="checkbox"
394
+ name="saveToFavorites"
395
+ checked={formData.saveToFavorites}
396
+ onChange={handleChange}
397
+ className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
398
+ />
399
+ <span>Save this connection for future sessions</span>
400
+ </label>
401
+ </div>
402
+
403
+ {/* Test Result Banner */}
404
+ {testResult && (
405
+ <div
406
+ className={`p-3 rounded-lg text-xs font-mono flex items-center gap-2 border ${
407
+ testResult.success
408
+ ? "bg-emerald-50 border-emerald-200 text-emerald-800"
409
+ : "bg-red-50 border-red-200 text-red-700"
410
+ }`}
411
+ >
412
+ <span className={`h-2 w-2 rounded-full ${testResult.success ? "bg-emerald-500" : "bg-red-500"}`} />
413
+ <span className="truncate">{testResult.message}</span>
414
+ </div>
415
+ )}
416
+
417
+ {/* Error Message */}
418
+ {connectionError && (
419
+ <div className="p-3 rounded-lg bg-red-50 border border-red-200 text-red-700 text-xs leading-relaxed">
420
+ {connectionError}
421
+ </div>
422
+ )}
423
+
424
+ {/* Buttons */}
425
+ <div className="pt-2 flex items-center justify-between gap-3">
426
+ <button
427
+ type="button"
428
+ onClick={handleTestConnection}
429
+ disabled={isTesting || isConnecting}
430
+ className="px-4 py-2 rounded-lg bg-slate-100 hover:bg-slate-200 active:scale-[0.98] border border-slate-200 text-slate-700 text-xs font-semibold transition-all disabled:opacity-50 flex items-center gap-1.5"
431
+ >
432
+ {isTesting ? (
433
+ <>
434
+ <svg className="w-3.5 h-3.5 animate-spin text-emerald-600" fill="none" viewBox="0 0 24 24">
435
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
436
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
437
+ </svg>
438
+ Testing...
439
+ </>
440
+ ) : (
441
+ "Test Connection"
442
+ )}
443
+ </button>
444
+
445
+ <button
446
+ type="submit"
447
+ disabled={isConnecting}
448
+ className="flex-1 py-2.5 px-6 rounded-lg bg-emerald-600 hover:bg-emerald-700 active:scale-[0.98] text-white text-xs font-bold shadow-sm transition-all flex items-center justify-center gap-2"
449
+ >
450
+ <span>Connect</span>
451
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
452
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
453
+ </svg>
454
+ </button>
455
+ </div>
456
+ </form>
457
+ </div>
458
+ </div>
459
+ </main>
460
+ </div>
461
+ );
462
+ };
463
+
464
+ export default ConnectionHub;
@@ -0,0 +1,128 @@
1
+ import { useEffect, useState } from 'react'
2
+ import apiClient from '../api/client'
3
+ import DatabaseTreeView from '../components/dashboard/DatabaseTreeView'
4
+ import CollectionsChart from '../components/dashboard/CollectionsChart'
5
+ import StorageDonut from '../components/dashboard/StorageDonut'
6
+ import MetricCard from '../components/ui/MetricCard'
7
+ import { Alert } from '../components/ui/Feedback'
8
+
9
+ const iconClass = 'h-5 w-5'
10
+
11
+ const ICONS = {
12
+ databases: (
13
+ <svg className={iconClass} fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
14
+ <ellipse cx='12' cy='6' rx='8' ry='3' />
15
+ <path d='M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6' />
16
+ <path d='M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6' />
17
+ </svg>
18
+ ),
19
+ collections: (
20
+ <svg className={iconClass} fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
21
+ <rect x='3' y='3' width='7' height='7' rx='1.5' />
22
+ <rect x='14' y='3' width='7' height='7' rx='1.5' />
23
+ <rect x='3' y='14' width='7' height='7' rx='1.5' />
24
+ <rect x='14' y='14' width='7' height='7' rx='1.5' />
25
+ </svg>
26
+ ),
27
+ documents: (
28
+ <svg className={iconClass} fill='none' viewBox='0 0 24 24' stroke='currentColor' strokeWidth={2}>
29
+ <path strokeLinecap='round' strokeLinejoin='round' d='M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' />
30
+ </svg>
31
+ )
32
+ }
33
+
34
+ const Dashboard = () => {
35
+ const [loading, setLoading] = useState(true)
36
+ const [stats, setStats] = useState(null)
37
+ const [error, setError] = useState(null)
38
+
39
+ useEffect(() => {
40
+ let cancelled = false
41
+
42
+ apiClient
43
+ .get('/api/dashboard-stats')
44
+ .then((response) => {
45
+ if (cancelled) return
46
+ setStats(response.data?.data)
47
+ setLoading(false)
48
+ })
49
+ .catch((err) => {
50
+ if (cancelled) return
51
+ console.error('Error fetching dashboard stats:', err)
52
+ setError(err.response?.data?.message || 'Could not load dashboard statistics.')
53
+ setLoading(false)
54
+ })
55
+
56
+ return () => { cancelled = true }
57
+ }, [])
58
+
59
+ const storage = stats?.storageInfo ?? {}
60
+ const cache = stats?.cacheStorage ?? {}
61
+
62
+ return (
63
+ <div className='flex-1 overflow-y-auto bg-slate-50 p-6 font-sans'>
64
+ <div className='mx-auto max-w-7xl'>
65
+ <header className='mb-6'>
66
+ <h1 className='text-2xl font-bold tracking-tight text-slate-900'>Server Performance & Metrics</h1>
67
+ <p className='mt-1 text-xs text-slate-500'>
68
+ Real-time telemetry for this AxioDB instance: in-memory cache ceiling, disk utilization, and collection tree.
69
+ </p>
70
+ </header>
71
+
72
+ {error && <Alert tone='danger' title='Metrics unavailable' className='mb-6'>{error}</Alert>}
73
+
74
+ <div className='stagger mb-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3'>
75
+ <MetricCard
76
+ label='Total Databases'
77
+ value={stats?.totalDatabases ?? 0}
78
+ icon={ICONS.databases}
79
+ accent='brand'
80
+ loading={loading}
81
+ />
82
+ <MetricCard
83
+ label='Total Collections'
84
+ value={stats?.totalCollections ?? 0}
85
+ icon={ICONS.collections}
86
+ accent='indigo'
87
+ loading={loading}
88
+ />
89
+ <MetricCard
90
+ label='Total Documents'
91
+ value={stats?.totalDocuments ?? 0}
92
+ icon={ICONS.documents}
93
+ accent='cyan'
94
+ loading={loading}
95
+ />
96
+ </div>
97
+
98
+ <div className='stagger mb-6 grid grid-cols-1 gap-4 lg:grid-cols-2'>
99
+ <StorageDonut
100
+ title='Disk Storage'
101
+ subtitle='AxioDB data files vs host filesystem capacity'
102
+ used={storage.total ?? 0}
103
+ total={storage.machine ?? 0}
104
+ unit={storage.matrixUnit ?? 'MB'}
105
+ loading={loading}
106
+ color='var(--color-viz-1)'
107
+ />
108
+ <StorageDonut
109
+ title='In-Memory Cache'
110
+ subtitle='Current active documents cached vs configured RAM ceiling'
111
+ used={cache.Storage ?? 0}
112
+ total={cache.Max ?? 0}
113
+ unit={cache.Unit ?? 'MB'}
114
+ loading={loading}
115
+ color='var(--color-viz-2)'
116
+ />
117
+ </div>
118
+
119
+ <div className='stagger grid grid-cols-1 gap-4 xl:grid-cols-2'>
120
+ <CollectionsChart nodeTree={stats?.nodeTree ?? []} loading={loading} />
121
+ <DatabaseTreeView treeDB={stats?.nodeTree ?? []} loading={loading} />
122
+ </div>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ export default Dashboard