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.
- package/electron/electron-builder.json +58 -0
- package/electron/main/main.cts +585 -0
- package/electron/main/preload.cts +51 -0
- package/electron/package-lock.json +8123 -0
- package/electron/package.json +52 -0
- package/electron/public/AXioDB.png +0 -0
- package/electron/resources/after-install.sh +45 -0
- package/electron/resources/after-remove.sh +29 -0
- package/electron/resources/icon.png +0 -0
- package/electron/resources/icons/128x128.png +0 -0
- package/electron/resources/icons/16x16.png +0 -0
- package/electron/resources/icons/24x24.png +0 -0
- package/electron/resources/icons/256x256.png +0 -0
- package/electron/resources/icons/32x32.png +0 -0
- package/electron/resources/icons/48x48.png +0 -0
- package/electron/resources/icons/512x512.png +0 -0
- package/electron/resources/icons/64x64.png +0 -0
- package/electron/src/App.jsx +128 -0
- package/electron/src/api/authApi.js +83 -0
- package/electron/src/api/client.js +102 -0
- package/electron/src/assets/AXioDB.png +0 -0
- package/electron/src/components/auth/CreateRoleModal.jsx +189 -0
- package/electron/src/components/auth/CreateUserModal.jsx +131 -0
- package/electron/src/components/auth/ForcePasswordChangeModal.jsx +132 -0
- package/electron/src/components/auth/ProtectedRoute.jsx +42 -0
- package/electron/src/components/auth/ResetPasswordModal.jsx +90 -0
- package/electron/src/components/auth/UserAvatarMenu.jsx +103 -0
- package/electron/src/components/collection/CreateCollectionModal.jsx +116 -0
- package/electron/src/components/collection/DeleteCollectionModal.jsx +85 -0
- package/electron/src/components/collection/SchemaViewModal.jsx +369 -0
- package/electron/src/components/dashboard/CollectionsChart.jsx +94 -0
- package/electron/src/components/dashboard/DatabaseTreeView.jsx +130 -0
- package/electron/src/components/dashboard/InMemoryCacheCard.jsx +60 -0
- package/electron/src/components/dashboard/StorageDonut.jsx +76 -0
- package/electron/src/components/dashboard/StorageUsageCard.jsx +59 -0
- package/electron/src/components/dashboard/TotalCollectionsCard.jsx +47 -0
- package/electron/src/components/dashboard/TotalDatabasesCard.jsx +43 -0
- package/electron/src/components/dashboard/TotalDocumentsCard.jsx +44 -0
- package/electron/src/components/database/CreateDatabaseModal.jsx +109 -0
- package/electron/src/components/database/DeleteDatabaseModal.jsx +97 -0
- package/electron/src/components/query/CodeEditor.jsx +343 -0
- package/electron/src/components/query/ObjectEditor.jsx +115 -0
- package/electron/src/components/query/ObjectView.jsx +44 -0
- package/electron/src/components/query/QueryEditor.jsx +32 -0
- package/electron/src/components/query/queryLanguage.js +755 -0
- package/electron/src/components/ui/Button.jsx +58 -0
- package/electron/src/components/ui/Card.jsx +29 -0
- package/electron/src/components/ui/ErrorBoundary.jsx +108 -0
- package/electron/src/components/ui/Feedback.jsx +66 -0
- package/electron/src/components/ui/Field.jsx +65 -0
- package/electron/src/components/ui/MetricCard.jsx +104 -0
- package/electron/src/components/ui/Modal.jsx +119 -0
- package/electron/src/components/ui/Page.jsx +40 -0
- package/electron/src/config/key.js +11 -0
- package/electron/src/index.css +181 -0
- package/electron/src/index.html +13 -0
- package/electron/src/layout/Sidebar.jsx +478 -0
- package/electron/src/layout/StatusBar.jsx +70 -0
- package/electron/src/layout/Titlebar.jsx +128 -0
- package/electron/src/main.jsx +42 -0
- package/electron/src/pages/ConnectionHub.jsx +464 -0
- package/electron/src/pages/Dashboard.jsx +128 -0
- package/electron/src/pages/Documents.jsx +823 -0
- package/electron/src/pages/Import.jsx +527 -0
- package/electron/src/pages/UserManagement.jsx +294 -0
- package/electron/src/pages/Welcome.jsx +157 -0
- package/electron/src/store/authStore.js +30 -0
- package/electron/src/store/connectionStore.js +103 -0
- package/electron/src/store/dbStore.js +165 -0
- package/electron/src/store/store.js +8 -0
- package/electron/src/utils/format.js +39 -0
- package/electron/vite.config.js +28 -0
- package/lib/Services/Indexation.operation.js +1 -1
- 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;
|