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,527 @@
1
+ import React, { useState, useEffect, useCallback } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import apiClient from "../api/client";
4
+ import authApi from "../api/authApi";
5
+ import { useConnectionStore } from "../store/connectionStore";
6
+ import { useDbStore } from "../store/dbStore";
7
+ import { formatBytes } from "../utils/format";
8
+
9
+ const Import = () => {
10
+ const [selectedFile, setSelectedFile] = useState(null); // { name, size, filePath?, rawFile? }
11
+ const [dragActive, setDragActive] = useState(false);
12
+ const [uploading, setUploading] = useState(false);
13
+ const [uploadStatus, setUploadStatus] = useState(null); // 'success' | 'error' | null
14
+ const [errorMessage, setErrorMessage] = useState("");
15
+ const [successMessage, setSuccessMessage] = useState("");
16
+
17
+ // Export state
18
+ const [exportingDb, setExportingDb] = useState(null);
19
+ const [exportStatus, setExportStatus] = useState(null); // 'success' | 'error' | null
20
+ const [exportError, setExportError] = useState("");
21
+ const [exportSuccess, setExportSuccess] = useState("");
22
+
23
+ const getBaseUrl = useConnectionStore((state) => state.getBaseUrl);
24
+ const fetchDatabases = useDbStore((state) => state.fetchDatabases);
25
+ const databases = useDbStore((state) => state.databases);
26
+ const loadingDatabases = useDbStore((state) => state.loadingTree);
27
+
28
+ const validateFile = (name) => {
29
+ const allowed = [".zip", ".tar", ".tar.gz", ".tgz"];
30
+ const lower = name.toLowerCase();
31
+ const isValid = allowed.some((ext) => lower.endsWith(ext));
32
+ if (!isValid) {
33
+ setErrorMessage("Please select a valid database backup archive (.zip, .tar, .tar.gz, .tgz)");
34
+ setUploadStatus("error");
35
+ return false;
36
+ }
37
+ return true;
38
+ };
39
+
40
+ const handleNativeSelect = async () => {
41
+ try {
42
+ if (window.electronAPI?.selectImportFile) {
43
+ const result = await window.electronAPI.selectImportFile();
44
+ if (result) {
45
+ setSelectedFile({
46
+ name: result.name,
47
+ size: result.size,
48
+ filePath: result.filePath,
49
+ });
50
+ setUploadStatus(null);
51
+ setErrorMessage("");
52
+ }
53
+ }
54
+ } catch (err) {
55
+ setErrorMessage(err.message || "Failed to open file picker");
56
+ }
57
+ };
58
+
59
+ const handleDrag = useCallback((e) => {
60
+ e.preventDefault();
61
+ e.stopPropagation();
62
+ if (e.type === "dragenter" || e.type === "dragover") {
63
+ setDragActive(true);
64
+ } else if (e.type === "dragleave") {
65
+ setDragActive(false);
66
+ }
67
+ }, []);
68
+
69
+ const handleDrop = useCallback((e) => {
70
+ e.preventDefault();
71
+ e.stopPropagation();
72
+ setDragActive(false);
73
+
74
+ if (e.dataTransfer.files && e.dataTransfer.files[0]) {
75
+ const dropped = e.dataTransfer.files[0];
76
+ if (validateFile(dropped.name)) {
77
+ // In electron, dropped.path might be populated or webUtils
78
+ const filePath = dropped.path || null;
79
+ setSelectedFile({
80
+ name: dropped.name,
81
+ size: dropped.size,
82
+ filePath,
83
+ rawFile: dropped,
84
+ });
85
+ setUploadStatus(null);
86
+ setErrorMessage("");
87
+ }
88
+ }
89
+ }, []);
90
+
91
+ const handleInputSelect = (e) => {
92
+ const file = e.target.files?.[0];
93
+ if (file && validateFile(file.name)) {
94
+ setSelectedFile({
95
+ name: file.name,
96
+ size: file.size,
97
+ filePath: file.path || null,
98
+ rawFile: file,
99
+ });
100
+ setUploadStatus(null);
101
+ setErrorMessage("");
102
+ }
103
+ };
104
+
105
+ const handleUpload = async () => {
106
+ if (!selectedFile) {
107
+ setErrorMessage("Please select a database backup archive to import");
108
+ setUploadStatus("error");
109
+ return;
110
+ }
111
+
112
+ setUploading(true);
113
+ setUploadStatus(null);
114
+ setErrorMessage("");
115
+ setSuccessMessage("");
116
+
117
+ try {
118
+ const baseUrl = getBaseUrl();
119
+ const targetUrl = `${baseUrl.replace(/\/+$/, "")}/api/db/import-database/`;
120
+
121
+ if (selectedFile.filePath && window.electronAPI?.uploadDatabase) {
122
+ // Native streaming upload through Electron main process
123
+ const res = await window.electronAPI.uploadDatabase(selectedFile.filePath, targetUrl);
124
+ const msg = res.data?.message || "Database imported and initialized successfully!";
125
+ setSuccessMessage(msg);
126
+ } else if (selectedFile.rawFile) {
127
+ // Browser / Web fallback with multipart FormData
128
+ const formData = new FormData();
129
+ formData.append("file", selectedFile.rawFile);
130
+ const res = await apiClient.post("/api/db/import-database/", formData, {
131
+ headers: { "Content-Type": "multipart/form-data" },
132
+ });
133
+ const msg = res.data?.message || "Database imported and initialized successfully!";
134
+ setSuccessMessage(msg);
135
+ } else {
136
+ throw new Error("No file path or file payload available for upload");
137
+ }
138
+
139
+ setUploadStatus("success");
140
+ setSelectedFile(null);
141
+ // Immediately refresh the database tree in the explorer sidebar
142
+ fetchDatabases();
143
+ } catch (err) {
144
+ setUploadStatus("error");
145
+ setErrorMessage(
146
+ err.response?.data?.message || err.message || "Failed to import database archive."
147
+ );
148
+ } finally {
149
+ setUploading(false);
150
+ }
151
+ };
152
+
153
+ const removeFile = () => {
154
+ setSelectedFile(null);
155
+ setUploadStatus(null);
156
+ setErrorMessage("");
157
+ setSuccessMessage("");
158
+ };
159
+
160
+ const handleExportDatabase = async (dbName) => {
161
+ setExportingDb(dbName);
162
+ setExportStatus(null);
163
+ setExportError("");
164
+ setExportSuccess("");
165
+
166
+ try {
167
+ const result = await authApi.exportDatabase(dbName);
168
+
169
+ if (result.canceled) {
170
+ return;
171
+ }
172
+
173
+ setExportStatus("success");
174
+ setExportSuccess(`${dbName}.tar.gz exported successfully`);
175
+ } catch (err) {
176
+ setExportStatus("error");
177
+ setExportError(
178
+ err.message || "Failed to export database. Please try again."
179
+ );
180
+ } finally {
181
+ setExportingDb(null);
182
+ }
183
+ };
184
+
185
+ useEffect(() => {
186
+ if (!databases.length && !loadingDatabases) {
187
+ fetchDatabases();
188
+ }
189
+ }, [databases, loadingDatabases, fetchDatabases]);
190
+
191
+ return (
192
+ <div className="flex-1 overflow-y-auto bg-slate-50 p-6 md:p-8">
193
+ <motion.div
194
+ initial={{ opacity: 0, y: 15 }}
195
+ animate={{ opacity: 1, y: 0 }}
196
+ transition={{ duration: 0.3 }}
197
+ className="max-w-3xl mx-auto space-y-6"
198
+ >
199
+ {/* Header */}
200
+ <div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
201
+ <div className="flex items-center gap-4">
202
+ <div className="h-12 w-12 rounded-xl bg-emerald-50 text-emerald-600 border border-emerald-200 flex items-center justify-center shrink-0">
203
+ <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
204
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
205
+ </svg>
206
+ </div>
207
+ <div>
208
+ <h1 className="text-xl font-bold text-slate-900">Import Database Archive</h1>
209
+ <p className="text-xs text-slate-500 mt-1">
210
+ Restore or clone an AxioDB database from a previously exported <span className="font-mono text-slate-700">.tar.gz</span> or <span className="font-mono text-slate-700">.zip</span> backup archive.
211
+ </p>
212
+ </div>
213
+ </div>
214
+ </div>
215
+
216
+ {/* Drop Zone */}
217
+ <div
218
+ onDragEnter={handleDrag}
219
+ onDragLeave={handleDrag}
220
+ onDragOver={handleDrag}
221
+ onDrop={handleDrop}
222
+ className={`relative border-2 border-dashed rounded-2xl p-8 text-center transition-all bg-white shadow-sm ${
223
+ dragActive
224
+ ? "border-emerald-500 bg-emerald-50/40 ring-4 ring-emerald-500/10"
225
+ : selectedFile
226
+ ? "border-emerald-400 bg-emerald-50/20"
227
+ : "border-slate-300 hover:border-slate-400"
228
+ }`}
229
+ >
230
+ <input
231
+ type="file"
232
+ id="archive-input"
233
+ className="hidden"
234
+ accept=".zip,.tar,.tar.gz,.tgz"
235
+ onChange={handleInputSelect}
236
+ disabled={uploading}
237
+ />
238
+
239
+ <AnimatePresence mode="wait">
240
+ {!selectedFile ? (
241
+ <motion.div
242
+ key="prompt"
243
+ initial={{ opacity: 0 }}
244
+ animate={{ opacity: 1 }}
245
+ exit={{ opacity: 0 }}
246
+ className="py-4 space-y-4"
247
+ >
248
+ <div className="h-16 w-16 mx-auto rounded-2xl bg-slate-100 text-slate-400 flex items-center justify-center">
249
+ <svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
250
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
251
+ </svg>
252
+ </div>
253
+
254
+ <div>
255
+ <p className="text-sm font-semibold text-slate-700">
256
+ Drag and drop your database archive here
257
+ </p>
258
+ <p className="text-xs text-slate-400 mt-1">
259
+ Supports <span className="font-mono">.tar.gz</span>, <span className="font-mono">.tgz</span>, <span className="font-mono">.tar</span>, <span className="font-mono">.zip</span>
260
+ </p>
261
+ </div>
262
+
263
+ <div className="flex items-center justify-center gap-3 pt-2">
264
+ <button
265
+ type="button"
266
+ onClick={handleNativeSelect}
267
+ className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold rounded-lg shadow-sm transition-all cursor-pointer flex items-center gap-2"
268
+ >
269
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
270
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
271
+ </svg>
272
+ Browse Local File
273
+ </button>
274
+ <label
275
+ htmlFor="archive-input"
276
+ className="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg transition-all cursor-pointer"
277
+ >
278
+ Standard Picker
279
+ </label>
280
+ </div>
281
+ </motion.div>
282
+ ) : (
283
+ <motion.div
284
+ key="selected"
285
+ initial={{ opacity: 0, scale: 0.98 }}
286
+ animate={{ opacity: 1, scale: 1 }}
287
+ exit={{ opacity: 0 }}
288
+ className="flex items-center justify-between p-4 bg-white border border-slate-200 rounded-xl shadow-xs"
289
+ >
290
+ <div className="flex items-center gap-3 text-left">
291
+ <div className="h-10 w-10 rounded-lg bg-emerald-100 text-emerald-700 flex items-center justify-center shrink-0">
292
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
293
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} 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" />
294
+ </svg>
295
+ </div>
296
+ <div>
297
+ <p className="text-sm font-semibold text-slate-900">{selectedFile.name}</p>
298
+ <p className="text-xs text-slate-500 font-mono">
299
+ {formatBytes(selectedFile.size)}
300
+ {selectedFile.filePath && (
301
+ <span className="text-slate-400 ml-2">({selectedFile.filePath})</span>
302
+ )}
303
+ </p>
304
+ </div>
305
+ </div>
306
+
307
+ {!uploading && (
308
+ <button
309
+ type="button"
310
+ onClick={removeFile}
311
+ className="p-1.5 text-slate-400 hover:text-red-600 rounded-lg hover:bg-slate-100 transition-colors cursor-pointer"
312
+ title="Remove file"
313
+ >
314
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
315
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
316
+ </svg>
317
+ </button>
318
+ )}
319
+ </motion.div>
320
+ )}
321
+ </AnimatePresence>
322
+ </div>
323
+
324
+ {/* Status Messages */}
325
+ <AnimatePresence>
326
+ {uploadStatus === "success" && (
327
+ <motion.div
328
+ initial={{ opacity: 0, y: 5 }}
329
+ animate={{ opacity: 1, y: 0 }}
330
+ exit={{ opacity: 0 }}
331
+ className="p-4 rounded-xl bg-emerald-50 border border-emerald-200 text-emerald-800 text-xs flex items-center gap-3"
332
+ >
333
+ <svg className="w-5 h-5 shrink-0 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
334
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
335
+ </svg>
336
+ <div>
337
+ <p className="font-semibold text-emerald-900">Import Successful</p>
338
+ <p className="text-emerald-700 mt-0.5">{successMessage}</p>
339
+ </div>
340
+ </motion.div>
341
+ )}
342
+
343
+ {uploadStatus === "error" && (
344
+ <motion.div
345
+ initial={{ opacity: 0, y: 5 }}
346
+ animate={{ opacity: 1, y: 0 }}
347
+ exit={{ opacity: 0 }}
348
+ className="p-4 rounded-xl bg-red-50 border border-red-200 text-red-800 text-xs flex items-center gap-3"
349
+ >
350
+ <svg className="w-5 h-5 shrink-0 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
351
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
352
+ </svg>
353
+ <div>
354
+ <p className="font-semibold text-red-900">Import Failed</p>
355
+ <p className="text-red-700 mt-0.5">{errorMessage}</p>
356
+ </div>
357
+ </motion.div>
358
+ )}
359
+ </AnimatePresence>
360
+
361
+ {/* Action Button */}
362
+ <div>
363
+ <button
364
+ type="button"
365
+ onClick={handleUpload}
366
+ disabled={!selectedFile || uploading}
367
+ className="w-full py-3 px-4 rounded-xl bg-emerald-600 hover:bg-emerald-700 text-white font-semibold text-xs shadow-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer flex items-center justify-center gap-2"
368
+ >
369
+ {uploading ? (
370
+ <>
371
+ <svg className="animate-spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
372
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
373
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
374
+ </svg>
375
+ Streaming Archive to AxioDB Server...
376
+ </>
377
+ ) : (
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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
381
+ </svg>
382
+ Import Archive
383
+ </>
384
+ )}
385
+ </button>
386
+ </div>
387
+
388
+ {/* Export Status */}
389
+ <AnimatePresence>
390
+ {exportStatus === "success" && exportSuccess && (
391
+ <motion.div
392
+ initial={{ opacity: 0, y: 5 }}
393
+ animate={{ opacity: 1, y: 0 }}
394
+ exit={{ opacity: 0 }}
395
+ className="p-4 rounded-xl bg-emerald-50 border border-emerald-200 text-emerald-800 text-xs flex items-center gap-3"
396
+ >
397
+ <svg className="w-5 h-5 shrink-0 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
398
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
399
+ </svg>
400
+ <span className="font-semibold text-emerald-900">{exportSuccess}</span>
401
+ </motion.div>
402
+ )}
403
+
404
+ {exportStatus === "error" && exportError && (
405
+ <motion.div
406
+ initial={{ opacity: 0, y: 5 }}
407
+ animate={{ opacity: 1, y: 0 }}
408
+ exit={{ opacity: 0 }}
409
+ className="p-4 rounded-xl bg-red-50 border border-red-200 text-red-800 text-xs flex items-center gap-3"
410
+ >
411
+ <svg className="w-5 h-5 shrink-0 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
412
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
413
+ </svg>
414
+ <span className="font-semibold text-red-900">{exportError}</span>
415
+ </motion.div>
416
+ )}
417
+ </AnimatePresence>
418
+
419
+ {/* Informational Card */}
420
+ <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-xs">
421
+ <h2 className="text-xs font-bold text-slate-800 uppercase tracking-wider mb-3">Guidelines & Behavior</h2>
422
+ <ul className="space-y-2 text-xs text-slate-600">
423
+ <li className="flex items-start gap-2">
424
+ <span className="text-emerald-500 font-bold">•</span>
425
+ <span>Archives are extracted securely on the AxioDB server directly into the configured data directory.</span>
426
+ </li>
427
+ <li className="flex items-start gap-2">
428
+ <span className="text-emerald-500 font-bold">•</span>
429
+ <span>Databases with existing names are preserved or handled per server safety policies.</span>
430
+ </li>
431
+ <li className="flex items-start gap-2">
432
+ <span className="text-emerald-500 font-bold">•</span>
433
+ <span>Once import completes, the database tree in the Explorer sidebar automatically updates.</span>
434
+ </li>
435
+ </ul>
436
+ </div>
437
+ </motion.div>
438
+
439
+ {/* Export Databases Section */}
440
+ <motion.div
441
+ initial={{ opacity: 0, y: 15 }}
442
+ animate={{ opacity: 1, y: 0 }}
443
+ transition={{ duration: 0.3, delay: 0.2 }}
444
+ className="max-w-3xl mx-auto mt-8 space-y-4"
445
+ >
446
+ <div className="bg-white border border-slate-200 rounded-xl p-5 shadow-sm">
447
+ <div className="flex items-center gap-3 mb-4">
448
+ <div className="h-10 w-10 rounded-xl bg-amber-50 text-amber-600 border border-amber-200 flex items-center justify-center shrink-0">
449
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
450
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 12V2m0 0l-4 4m4-4l4 4M5 12h14" />
451
+ </svg>
452
+ </div>
453
+ <div>
454
+ <h2 className="text-lg font-bold text-slate-900">Export Databases</h2>
455
+ <p className="text-xs text-slate-500 mt-0.5">
456
+ Download a compressed backup of any database as a <span className="font-mono text-slate-700">.tar.gz</span> archive.
457
+ </p>
458
+ </div>
459
+ </div>
460
+
461
+ {loadingDatabases ? (
462
+ <div className="space-y-3">
463
+ {[0, 1, 2].map((i) => (
464
+ <div key={i} className="h-12 bg-slate-100 rounded-lg animate-pulse" />
465
+ ))}
466
+ </div>
467
+ ) : databases.length === 0 ? (
468
+ <div className="text-center py-8">
469
+ <div className="h-10 w-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-3">
470
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
471
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
472
+ </svg>
473
+ </div>
474
+ <p className="text-xs text-slate-500">No databases available for export.</p>
475
+ </div>
476
+ ) : (
477
+ <div className="space-y-2">
478
+ {databases.map((dbName) => (
479
+ <motion.div
480
+ key={dbName}
481
+ initial={{ opacity: 0, x: -10 }}
482
+ animate={{ opacity: 1, x: 0 }}
483
+ transition={{ duration: 0.2, delay: 0.05 * databases.indexOf(dbName) }}
484
+ className="flex items-center justify-between p-3 rounded-xl border border-slate-200 hover:bg-slate-50 hover:border-emerald-200 transition-all group"
485
+ >
486
+ <div className="flex items-center gap-3 min-w-0">
487
+ <div className="h-8 w-8 rounded-lg bg-amber-100 text-amber-700 flex items-center justify-center shrink-0">
488
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
489
+ <ellipse cx="12" cy="6" rx="8" ry="3" />
490
+ <path d="M4 6v12c0 1.66 3.58 3 8 3s8-1.34 8-3V6" />
491
+ </svg>
492
+ </div>
493
+ <span className="font-mono text-sm text-slate-900 truncate">{dbName}</span>
494
+ </div>
495
+ <button
496
+ onClick={() => handleExportDatabase(dbName)}
497
+ disabled={exportingDb === dbName}
498
+ className="px-3 py-1.5 rounded-lg bg-amber-600 hover:bg-amber-700 text-white text-xs font-semibold transition-all disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer flex items-center gap-1.5 shadow-sm"
499
+ >
500
+ {exportingDb === dbName ? (
501
+ <>
502
+ <svg className="animate-spin h-3.5 w-3.5" fill="none" viewBox="0 0 24 24">
503
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
504
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
505
+ </svg>
506
+ Exporting...
507
+ </>
508
+ ) : (
509
+ <>
510
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
511
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1M12 12V2m0 0l-4 4m4-4l4 4M5 12h14" />
512
+ </svg>
513
+ Export
514
+ </>
515
+ )}
516
+ </button>
517
+ </motion.div>
518
+ ))}
519
+ </div>
520
+ )}
521
+ </div>
522
+ </motion.div>
523
+ </div>
524
+ );
525
+ };
526
+
527
+ export default Import;