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,181 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-brand-50: #ecfdf5;
|
|
5
|
+
--color-brand-100: #d1fae5;
|
|
6
|
+
--color-brand-200: #a7f3d0;
|
|
7
|
+
--color-brand-300: #6ee7b7;
|
|
8
|
+
--color-brand-400: #34d399;
|
|
9
|
+
--color-brand-500: #10b981;
|
|
10
|
+
--color-brand-600: #059669;
|
|
11
|
+
--color-brand-700: #047857;
|
|
12
|
+
--color-brand-800: #065f46;
|
|
13
|
+
--color-brand-900: #064e3b;
|
|
14
|
+
|
|
15
|
+
--color-ink-50: #ffffff;
|
|
16
|
+
--color-ink-100: #f8fafc;
|
|
17
|
+
--color-ink-200: #f1f5f9;
|
|
18
|
+
--color-ink-300: #e2e8f0;
|
|
19
|
+
--color-ink-400: #cbd5e1;
|
|
20
|
+
--color-ink-500: #94a3b8;
|
|
21
|
+
--color-ink-600: #64748b;
|
|
22
|
+
--color-ink-700: #475569;
|
|
23
|
+
--color-ink-800: #334155;
|
|
24
|
+
--color-ink-900: #1e293b;
|
|
25
|
+
--color-ink-950: #0f172a;
|
|
26
|
+
|
|
27
|
+
--color-viz-1: #059669;
|
|
28
|
+
--color-viz-2: #4f46e5;
|
|
29
|
+
--color-viz-3: #d97706;
|
|
30
|
+
--color-viz-4: #db2777;
|
|
31
|
+
--color-viz-5: #0284c7;
|
|
32
|
+
--color-viz-6: #7c3aed;
|
|
33
|
+
|
|
34
|
+
--color-danger-50: #fef2f2;
|
|
35
|
+
--color-danger-200: #fecaca;
|
|
36
|
+
--color-danger-500: #ef4444;
|
|
37
|
+
--color-danger-600: #dc2626;
|
|
38
|
+
--color-danger-700: #b91c1c;
|
|
39
|
+
|
|
40
|
+
--color-warn-50: #fffbeb;
|
|
41
|
+
--color-warn-200: #fde68a;
|
|
42
|
+
--color-warn-500: #f59e0b;
|
|
43
|
+
--color-warn-700: #b45309;
|
|
44
|
+
|
|
45
|
+
--color-info-50: #f0f9ff;
|
|
46
|
+
--color-info-200: #bae6fd;
|
|
47
|
+
--color-info-500: #0284c7;
|
|
48
|
+
--color-info-700: #0369a1;
|
|
49
|
+
|
|
50
|
+
--shadow-card: 0 1px 3px 0 rgba(0, 0, 0, 0.06), 0 1px 2px -1px rgba(0, 0, 0, 0.04);
|
|
51
|
+
--shadow-raised: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
|
52
|
+
--shadow-float: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
|
|
53
|
+
--shadow-modal: 0 20px 25px -5px rgba(0, 0, 0, 0.12), 0 8px 10px -6px rgba(0, 0, 0, 0.08);
|
|
54
|
+
--shadow-glow: 0 0 20px -3px rgba(16, 185, 129, 0.25);
|
|
55
|
+
--shadow-glow-cyan: 0 0 20px -3px rgba(6, 182, 212, 0.25);
|
|
56
|
+
--shadow-glow-indigo: 0 0 20px -3px rgba(99, 102, 241, 0.25);
|
|
57
|
+
|
|
58
|
+
--radius-card: 0.625rem;
|
|
59
|
+
|
|
60
|
+
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
|
|
61
|
+
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@layer base {
|
|
65
|
+
body {
|
|
66
|
+
background-color: #f8fafc;
|
|
67
|
+
color: #0f172a;
|
|
68
|
+
-webkit-font-smoothing: antialiased;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
:focus-visible {
|
|
72
|
+
outline: 2px solid var(--color-brand-600);
|
|
73
|
+
outline-offset: 2px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.tabular-nums {
|
|
77
|
+
font-variant-numeric: tabular-nums;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.drag-region {
|
|
82
|
+
-webkit-app-region: drag;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.no-drag {
|
|
86
|
+
-webkit-app-region: no-drag;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Animations */
|
|
90
|
+
@keyframes fadeIn {
|
|
91
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
92
|
+
to { opacity: 1; transform: translateY(0); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes slideIn {
|
|
96
|
+
from { opacity: 0; transform: translateX(-12px); }
|
|
97
|
+
to { opacity: 1; transform: translateX(0); }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@keyframes riseIn {
|
|
101
|
+
from { opacity: 0; transform: translateY(10px) scale(0.99); }
|
|
102
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@keyframes popIn {
|
|
106
|
+
from { opacity: 0; transform: translateY(4px) scale(0.97); }
|
|
107
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@keyframes shimmer {
|
|
111
|
+
from { background-position: 200% 0; }
|
|
112
|
+
to { background-position: -200% 0; }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@keyframes pulseGlow {
|
|
116
|
+
0%, 100% { opacity: 0.3; transform: scale(1); }
|
|
117
|
+
50% { opacity: 0.6; transform: scale(1.04); }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@keyframes radarSweep {
|
|
121
|
+
0% { transform: rotate(0deg); }
|
|
122
|
+
100% { transform: rotate(360deg); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@keyframes floatOrb {
|
|
126
|
+
0%, 100% { transform: translateY(0px) translateX(0px); }
|
|
127
|
+
50% { transform: translateY(-8px) translateX(6px); }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@keyframes latencyUpdate {
|
|
131
|
+
0% { color: #10b981; transform: scale(1.05); }
|
|
132
|
+
60% { color: #059669; transform: scale(1.12); }
|
|
133
|
+
100% { color: #10b981; transform: scale(1); }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@keyframes pulseDot {
|
|
137
|
+
0%, 100% { opacity: 0.6; transform: scale(1); }
|
|
138
|
+
50% { opacity: 1; transform: scale(1.15); }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.animate-fadeIn { animation: fadeIn 0.25s var(--ease-out-soft) both; }
|
|
142
|
+
.animate-slideIn { animation: slideIn 0.25s var(--ease-out-soft) both; }
|
|
143
|
+
.animate-riseIn { animation: riseIn 0.35s var(--ease-out-soft) both; }
|
|
144
|
+
.animate-popIn { animation: popIn 0.2s var(--ease-spring) both; }
|
|
145
|
+
.animate-pulseGlow { animation: pulseGlow 3s ease-in-out infinite; }
|
|
146
|
+
.animate-radar { animation: radarSweep 2s linear infinite; }
|
|
147
|
+
.animate-float { animation: floatOrb 6s ease-in-out infinite; }
|
|
148
|
+
.animate-latencyUpdate { animation: latencyUpdate 0.35s var(--ease-spring) both; }
|
|
149
|
+
.animate-pulseDot { animation: pulseDot 1.5s ease-in-out infinite both; }
|
|
150
|
+
|
|
151
|
+
.stagger > * { animation: riseIn 0.35s var(--ease-out-soft) both; }
|
|
152
|
+
.stagger > *:nth-child(1) { animation-delay: 0ms; }
|
|
153
|
+
.stagger > *:nth-child(2) { animation-delay: 35ms; }
|
|
154
|
+
.stagger > *:nth-child(3) { animation-delay: 70ms; }
|
|
155
|
+
.stagger > *:nth-child(4) { animation-delay: 105ms; }
|
|
156
|
+
.stagger > *:nth-child(5) { animation-delay: 140ms; }
|
|
157
|
+
.stagger > *:nth-child(6) { animation-delay: 175ms; }
|
|
158
|
+
.stagger > *:nth-child(7) { animation-delay: 210ms; }
|
|
159
|
+
.stagger > *:nth-child(8) { animation-delay: 245ms; }
|
|
160
|
+
|
|
161
|
+
.skeleton {
|
|
162
|
+
background-image: linear-gradient(
|
|
163
|
+
90deg,
|
|
164
|
+
#f1f5f9 0%,
|
|
165
|
+
#e2e8f0 40%,
|
|
166
|
+
#f1f5f9 80%
|
|
167
|
+
);
|
|
168
|
+
background-size: 200% 100%;
|
|
169
|
+
animation: shimmer 1.5s linear infinite;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@supports selector(::-webkit-scrollbar) {
|
|
173
|
+
::-webkit-scrollbar { width: 7px; height: 7px; }
|
|
174
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
175
|
+
::-webkit-scrollbar-thumb {
|
|
176
|
+
background: #cbd5e1;
|
|
177
|
+
border: 2px solid #f8fafc;
|
|
178
|
+
border-radius: 9999px;
|
|
179
|
+
}
|
|
180
|
+
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
|
181
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full bg-slate-50">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/png" href="./assets/AXioDB.png" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>AxioDB Control</title>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="h-full overflow-hidden select-none bg-slate-50 text-slate-900 font-sans">
|
|
10
|
+
<div id="root" class="h-full flex flex-col"></div>
|
|
11
|
+
<script type="module" src="/main.jsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { useNavigate, useLocation } from "react-router-dom";
|
|
3
|
+
import { useAuthStore } from "../store/authStore";
|
|
4
|
+
import { useDbStore } from "../store/dbStore";
|
|
5
|
+
import { useConnectionStore } from "../store/connectionStore";
|
|
6
|
+
import authApi from "../api/authApi";
|
|
7
|
+
import CreateDatabaseModal from "../components/database/CreateDatabaseModal";
|
|
8
|
+
import DeleteDatabaseModal from "../components/database/DeleteDatabaseModal";
|
|
9
|
+
import CreateCollectionModal from "../components/collection/CreateCollectionModal";
|
|
10
|
+
import DeleteCollectionModal from "../components/collection/DeleteCollectionModal";
|
|
11
|
+
|
|
12
|
+
const Sidebar = () => {
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
const location = useLocation();
|
|
15
|
+
|
|
16
|
+
const { username, role, permissions } = useAuthStore();
|
|
17
|
+
const { disconnect } = useConnectionStore();
|
|
18
|
+
const {
|
|
19
|
+
databases,
|
|
20
|
+
collectionsMap,
|
|
21
|
+
expandedDbs,
|
|
22
|
+
selectedDatabase,
|
|
23
|
+
selectedCollection,
|
|
24
|
+
loadingTree,
|
|
25
|
+
fetchDatabases,
|
|
26
|
+
fetchCollections,
|
|
27
|
+
toggleDbExpanded,
|
|
28
|
+
setSelectedDatabase,
|
|
29
|
+
setSelectedCollection,
|
|
30
|
+
addDatabaseLocally,
|
|
31
|
+
removeDatabaseLocally,
|
|
32
|
+
addCollectionLocally,
|
|
33
|
+
removeCollectionLocally,
|
|
34
|
+
} = useDbStore();
|
|
35
|
+
|
|
36
|
+
const [filterText, setFilterText] = useState("");
|
|
37
|
+
|
|
38
|
+
// Modals state
|
|
39
|
+
const [showCreateDb, setShowCreateDb] = useState(false);
|
|
40
|
+
const [dbToDelete, setDbToDelete] = useState(null);
|
|
41
|
+
const [showCreateColl, setShowCreateColl] = useState(false);
|
|
42
|
+
const [createCollDb, setCreateCollDb] = useState("");
|
|
43
|
+
const [collToDelete, setCollToDelete] = useState(null); // { dbName, collName }
|
|
44
|
+
|
|
45
|
+
// Export notification
|
|
46
|
+
const [exportMsg, setExportMsg] = useState(null); // { type: 'success'|'error', text: string }
|
|
47
|
+
const [exportingDb, setExportingDb] = useState(null);
|
|
48
|
+
|
|
49
|
+
const clearExportMsg = () => {
|
|
50
|
+
setTimeout(() => setExportMsg(null), 4000);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleExportDatabase = async (dbName) => {
|
|
54
|
+
setExportingDb(dbName);
|
|
55
|
+
try {
|
|
56
|
+
const result = await authApi.exportDatabase(dbName);
|
|
57
|
+
if (result.canceled) return;
|
|
58
|
+
setExportMsg({ type: "success", text: `${dbName}.tar.gz exported` });
|
|
59
|
+
clearExportMsg();
|
|
60
|
+
} catch (err) {
|
|
61
|
+
setExportMsg({ type: "error", text: err.message || "Export failed" });
|
|
62
|
+
clearExportMsg();
|
|
63
|
+
} finally {
|
|
64
|
+
setExportingDb(null);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
fetchDatabases();
|
|
70
|
+
}, [fetchDatabases]);
|
|
71
|
+
|
|
72
|
+
const activePath = location.pathname;
|
|
73
|
+
|
|
74
|
+
const handleSelectDb = (dbName) => {
|
|
75
|
+
setSelectedDatabase(dbName);
|
|
76
|
+
if (activePath !== "/") {
|
|
77
|
+
navigate("/");
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleSelectColl = (dbName, collName) => {
|
|
82
|
+
setSelectedCollection(dbName, collName);
|
|
83
|
+
if (activePath !== "/") {
|
|
84
|
+
navigate("/");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const filteredDatabases = databases.filter((db) => {
|
|
89
|
+
if (!filterText.trim()) return true;
|
|
90
|
+
const matchDb = db.toLowerCase().includes(filterText.toLowerCase());
|
|
91
|
+
const colls = collectionsMap[db] || [];
|
|
92
|
+
const matchColl = colls.some((c) => c.name.toLowerCase().includes(filterText.toLowerCase()));
|
|
93
|
+
return matchDb || matchColl;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<aside className="w-72 h-full bg-slate-50 border-r border-slate-200/90 flex flex-col justify-between select-none shrink-0 font-sans">
|
|
98
|
+
{/* Upper Navigation & Tree Section */}
|
|
99
|
+
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
|
100
|
+
{/* Workspace Utility Switcher */}
|
|
101
|
+
<div className="p-2 border-b border-slate-200/80 bg-white">
|
|
102
|
+
<div className="grid grid-cols-4 gap-1 p-1 bg-slate-100 rounded-lg text-xs font-medium text-slate-600">
|
|
103
|
+
<button
|
|
104
|
+
onClick={() => navigate("/")}
|
|
105
|
+
className={`flex flex-col items-center justify-center py-1.5 px-1 rounded-md transition-all ${
|
|
106
|
+
activePath === "/"
|
|
107
|
+
? "bg-white text-emerald-700 shadow-xs font-semibold"
|
|
108
|
+
: "hover:text-slate-900 hover:bg-slate-200/60"
|
|
109
|
+
}`}
|
|
110
|
+
title="Database & Collection Explorer"
|
|
111
|
+
>
|
|
112
|
+
<svg className="w-4 h-4 mb-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
113
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2 3.58 3 8 3s8-1 8-3V7M4 7c0 2 3.58 3 8 3s8-1 8-3M4 7c0-2 3.58-3 8-3s8 1 8 3m0 5c0 2-3.58 3-8 3s-8-1-8-3" />
|
|
114
|
+
</svg>
|
|
115
|
+
<span>Explorer</span>
|
|
116
|
+
</button>
|
|
117
|
+
|
|
118
|
+
<button
|
|
119
|
+
onClick={() => navigate("/metrics")}
|
|
120
|
+
className={`flex flex-col items-center justify-center py-1.5 px-1 rounded-md transition-all ${
|
|
121
|
+
activePath === "/metrics"
|
|
122
|
+
? "bg-white text-emerald-700 shadow-xs font-semibold"
|
|
123
|
+
: "hover:text-slate-900 hover:bg-slate-200/60"
|
|
124
|
+
}`}
|
|
125
|
+
title="Real-time Server Metrics"
|
|
126
|
+
>
|
|
127
|
+
<svg className="w-4 h-4 mb-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
128
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
129
|
+
</svg>
|
|
130
|
+
<span>Metrics</span>
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
<button
|
|
134
|
+
onClick={() => navigate("/import")}
|
|
135
|
+
className={`flex flex-col items-center justify-center py-1.5 px-1 rounded-md transition-all ${
|
|
136
|
+
activePath === "/import"
|
|
137
|
+
? "bg-white text-emerald-700 shadow-xs font-semibold"
|
|
138
|
+
: "hover:text-slate-900 hover:bg-slate-200/60"
|
|
139
|
+
}`}
|
|
140
|
+
title="Import & Export"
|
|
141
|
+
>
|
|
142
|
+
<svg className="w-4 h-4 mb-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
143
|
+
<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" />
|
|
144
|
+
</svg>
|
|
145
|
+
<span>Backup</span>
|
|
146
|
+
</button>
|
|
147
|
+
|
|
148
|
+
{(!permissions || permissions.includes("user:view")) && (
|
|
149
|
+
<button
|
|
150
|
+
onClick={() => navigate("/users")}
|
|
151
|
+
className={`flex flex-col items-center justify-center py-1.5 px-1 rounded-md transition-all ${
|
|
152
|
+
activePath === "/users"
|
|
153
|
+
? "bg-white text-emerald-700 shadow-xs font-semibold"
|
|
154
|
+
: "hover:text-slate-900 hover:bg-slate-200/60"
|
|
155
|
+
}`}
|
|
156
|
+
title="Users & Roles RBAC"
|
|
157
|
+
>
|
|
158
|
+
<svg className="w-4 h-4 mb-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
159
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
160
|
+
</svg>
|
|
161
|
+
<span>Users</span>
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Explorer Header & Controls */}
|
|
168
|
+
<div className="px-3 pt-3 pb-2">
|
|
169
|
+
<div className="flex items-center justify-between mb-2">
|
|
170
|
+
<span className="text-[11px] font-bold text-slate-500 uppercase tracking-wider">
|
|
171
|
+
Databases
|
|
172
|
+
</span>
|
|
173
|
+
<div className="flex items-center gap-1">
|
|
174
|
+
<button
|
|
175
|
+
onClick={() => setShowCreateDb(true)}
|
|
176
|
+
className="p-1 rounded-md text-slate-500 hover:text-emerald-700 hover:bg-emerald-50 transition-colors"
|
|
177
|
+
title="Create Database"
|
|
178
|
+
>
|
|
179
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
180
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 4v16m8-8H4" />
|
|
181
|
+
</svg>
|
|
182
|
+
</button>
|
|
183
|
+
<button
|
|
184
|
+
onClick={() => fetchDatabases()}
|
|
185
|
+
className="p-1 rounded-md text-slate-500 hover:text-slate-800 hover:bg-slate-200/70 transition-colors"
|
|
186
|
+
title="Refresh Databases"
|
|
187
|
+
>
|
|
188
|
+
<svg className={`w-3.5 h-3.5 ${loadingTree ? "animate-spin text-emerald-600" : ""}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
189
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
190
|
+
</svg>
|
|
191
|
+
</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{/* Search Filter */}
|
|
196
|
+
<div className="relative">
|
|
197
|
+
<input
|
|
198
|
+
type="text"
|
|
199
|
+
value={filterText}
|
|
200
|
+
onChange={(e) => setFilterText(e.target.value)}
|
|
201
|
+
placeholder="Search databases..."
|
|
202
|
+
className="w-full pl-7 pr-6 py-1 bg-white border border-slate-200 rounded-md text-xs text-slate-800 placeholder-slate-400 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500/20"
|
|
203
|
+
/>
|
|
204
|
+
<svg className="w-3.5 h-3.5 text-slate-400 absolute left-2 top-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
205
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
206
|
+
</svg>
|
|
207
|
+
{filterText && (
|
|
208
|
+
<button
|
|
209
|
+
onClick={() => setFilterText("")}
|
|
210
|
+
className="absolute right-2 top-1.5 text-slate-400 hover:text-slate-600"
|
|
211
|
+
>
|
|
212
|
+
×
|
|
213
|
+
</button>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Database & Collection Tree List */}
|
|
219
|
+
<div className="flex-1 overflow-y-auto px-2 pb-2 space-y-0.5">
|
|
220
|
+
{filteredDatabases.length === 0 ? (
|
|
221
|
+
<div className="text-center py-8 text-xs text-slate-400">
|
|
222
|
+
{loadingTree ? "Loading databases..." : "No databases found"}
|
|
223
|
+
</div>
|
|
224
|
+
) : (
|
|
225
|
+
filteredDatabases.map((dbName) => {
|
|
226
|
+
const isExpanded = !!expandedDbs[dbName];
|
|
227
|
+
const isDbSelected = selectedDatabase === dbName && !selectedCollection;
|
|
228
|
+
const colls = collectionsMap[dbName] || [];
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div key={dbName} className="group/db">
|
|
232
|
+
{/* Database Node */}
|
|
233
|
+
<div
|
|
234
|
+
onClick={() => handleSelectDb(dbName)}
|
|
235
|
+
className={`flex items-center justify-between px-2 py-1.5 rounded-lg text-xs cursor-pointer transition-all ${
|
|
236
|
+
isDbSelected
|
|
237
|
+
? "bg-emerald-50 text-emerald-800 font-semibold border border-emerald-200/80 shadow-2xs"
|
|
238
|
+
: "text-slate-700 hover:bg-slate-200/60"
|
|
239
|
+
}`}
|
|
240
|
+
>
|
|
241
|
+
<div className="flex items-center gap-1.5 min-w-0 flex-1">
|
|
242
|
+
<button
|
|
243
|
+
onClick={(e) => {
|
|
244
|
+
e.stopPropagation();
|
|
245
|
+
toggleDbExpanded(dbName);
|
|
246
|
+
}}
|
|
247
|
+
className="p-0.5 text-slate-400 hover:text-slate-700 rounded transition-transform"
|
|
248
|
+
>
|
|
249
|
+
<svg
|
|
250
|
+
className={`w-3.5 h-3.5 transition-transform ${isExpanded ? "rotate-90" : ""}`}
|
|
251
|
+
fill="none"
|
|
252
|
+
viewBox="0 0 24 24"
|
|
253
|
+
stroke="currentColor"
|
|
254
|
+
>
|
|
255
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
256
|
+
</svg>
|
|
257
|
+
</button>
|
|
258
|
+
|
|
259
|
+
<svg className={`w-3.5 h-3.5 shrink-0 ${isDbSelected ? "text-emerald-600" : "text-slate-400"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
260
|
+
<ellipse cx="12" cy="6" rx="8" ry="3" strokeWidth={1.8} />
|
|
261
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6" />
|
|
262
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6" />
|
|
263
|
+
</svg>
|
|
264
|
+
|
|
265
|
+
<span className="truncate font-medium">{dbName}</span>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<div className="flex items-center gap-1">
|
|
269
|
+
<span className="text-[10px] text-slate-400 bg-slate-200/60 px-1 rounded font-mono">
|
|
270
|
+
{colls.length}
|
|
271
|
+
</span>
|
|
272
|
+
|
|
273
|
+
{/* Quick Actions Hover */}
|
|
274
|
+
<button
|
|
275
|
+
onClick={(e) => {
|
|
276
|
+
e.stopPropagation();
|
|
277
|
+
setCreateCollDb(dbName);
|
|
278
|
+
setShowCreateColl(true);
|
|
279
|
+
}}
|
|
280
|
+
className="opacity-0 group-hover/db:opacity-100 p-0.5 text-slate-400 hover:text-emerald-700 hover:bg-emerald-50 rounded transition-opacity"
|
|
281
|
+
title="Add Collection"
|
|
282
|
+
>
|
|
283
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
284
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M12 4v16m8-8H4" />
|
|
285
|
+
</svg>
|
|
286
|
+
</button>
|
|
287
|
+
|
|
288
|
+
<button
|
|
289
|
+
onClick={(e) => {
|
|
290
|
+
e.stopPropagation();
|
|
291
|
+
handleExportDatabase(dbName);
|
|
292
|
+
}}
|
|
293
|
+
disabled={exportingDb === dbName}
|
|
294
|
+
className="opacity-0 group-hover/db:opacity-100 p-0.5 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded transition-opacity disabled:opacity-50"
|
|
295
|
+
title="Export Database"
|
|
296
|
+
>
|
|
297
|
+
{exportingDb === dbName ? (
|
|
298
|
+
<svg className="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24">
|
|
299
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
300
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
|
|
301
|
+
</svg>
|
|
302
|
+
) : (
|
|
303
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
304
|
+
<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" />
|
|
305
|
+
</svg>
|
|
306
|
+
)}
|
|
307
|
+
</button>
|
|
308
|
+
|
|
309
|
+
<button
|
|
310
|
+
onClick={(e) => {
|
|
311
|
+
e.stopPropagation();
|
|
312
|
+
setDbToDelete(dbName);
|
|
313
|
+
}}
|
|
314
|
+
className="opacity-0 group-hover/db:opacity-100 p-0.5 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded transition-opacity"
|
|
315
|
+
title="Delete Database"
|
|
316
|
+
>
|
|
317
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
318
|
+
<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" />
|
|
319
|
+
</svg>
|
|
320
|
+
</button>
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
|
|
324
|
+
{/* Collections List under Database */}
|
|
325
|
+
{isExpanded && (
|
|
326
|
+
<div className="ml-5 pl-2 border-l border-slate-200/70 space-y-0.5 my-0.5">
|
|
327
|
+
{colls.length === 0 ? (
|
|
328
|
+
<div className="py-1 px-2 text-[11px] text-slate-400 italic">
|
|
329
|
+
No collections
|
|
330
|
+
</div>
|
|
331
|
+
) : (
|
|
332
|
+
colls.map((coll) => {
|
|
333
|
+
const isCollSelected = selectedDatabase === dbName && selectedCollection === coll.name;
|
|
334
|
+
|
|
335
|
+
return (
|
|
336
|
+
<div
|
|
337
|
+
key={coll.name}
|
|
338
|
+
onClick={() => handleSelectColl(dbName, coll.name)}
|
|
339
|
+
className={`group/coll flex items-center justify-between px-2 py-1 rounded-md text-xs cursor-pointer transition-all ${
|
|
340
|
+
isCollSelected
|
|
341
|
+
? "bg-emerald-600 text-white font-medium shadow-xs"
|
|
342
|
+
: "text-slate-600 hover:text-slate-900 hover:bg-slate-200/60"
|
|
343
|
+
}`}
|
|
344
|
+
>
|
|
345
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
346
|
+
<svg className={`w-3.5 h-3.5 shrink-0 ${isCollSelected ? "text-white" : "text-slate-400"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
347
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} 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" />
|
|
348
|
+
</svg>
|
|
349
|
+
<span className="truncate">{coll.name}</span>
|
|
350
|
+
</div>
|
|
351
|
+
|
|
352
|
+
<div className="flex items-center gap-1">
|
|
353
|
+
<span className={`text-[10px] font-mono px-1 rounded ${isCollSelected ? "bg-emerald-700 text-emerald-100" : "text-slate-400 bg-slate-200/60"}`}>
|
|
354
|
+
{coll.count ?? 0}
|
|
355
|
+
</span>
|
|
356
|
+
|
|
357
|
+
<button
|
|
358
|
+
onClick={(e) => {
|
|
359
|
+
e.stopPropagation();
|
|
360
|
+
setCollToDelete({ dbName, collName: coll.name });
|
|
361
|
+
}}
|
|
362
|
+
className={`opacity-0 group-hover/coll:opacity-100 p-0.5 rounded ${
|
|
363
|
+
isCollSelected ? "text-emerald-200 hover:text-white" : "text-slate-400 hover:text-red-600"
|
|
364
|
+
}`}
|
|
365
|
+
title="Drop Collection"
|
|
366
|
+
>
|
|
367
|
+
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
368
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
369
|
+
</svg>
|
|
370
|
+
</button>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
})
|
|
375
|
+
)}
|
|
376
|
+
</div>
|
|
377
|
+
)}
|
|
378
|
+
</div>
|
|
379
|
+
);
|
|
380
|
+
})
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
{/* Export Notification */}
|
|
386
|
+
{exportMsg && (
|
|
387
|
+
<div className="px-3 pb-2">
|
|
388
|
+
<div
|
|
389
|
+
className={`p-2.5 rounded-lg text-xs font-medium flex items-center gap-2 transition-all ${
|
|
390
|
+
exportMsg.type === "success"
|
|
391
|
+
? "bg-emerald-50 border border-emerald-200 text-emerald-800"
|
|
392
|
+
: "bg-red-50 border border-red-200 text-red-800"
|
|
393
|
+
}`}
|
|
394
|
+
>
|
|
395
|
+
<svg
|
|
396
|
+
className={`w-3.5 h-3.5 shrink-0 ${exportMsg.type === "success" ? "text-emerald-600" : "text-red-600"}`}
|
|
397
|
+
fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
|
398
|
+
>
|
|
399
|
+
{exportMsg.type === "success" ? (
|
|
400
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
401
|
+
) : (
|
|
402
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
403
|
+
)}
|
|
404
|
+
</svg>
|
|
405
|
+
<span className="truncate">{exportMsg.text}</span>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
|
|
410
|
+
{/* Bottom User Profile Section */}
|
|
411
|
+
<div className="p-2 border-t border-slate-200 bg-white">
|
|
412
|
+
<div className="flex items-center justify-between p-2 rounded-lg bg-slate-50 border border-slate-200/80">
|
|
413
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
414
|
+
<div className="h-7 w-7 rounded-lg bg-emerald-100 text-emerald-700 border border-emerald-200 flex items-center justify-center font-bold text-xs uppercase shrink-0">
|
|
415
|
+
{username ? username.charAt(0) : "A"}
|
|
416
|
+
</div>
|
|
417
|
+
<div className="min-w-0">
|
|
418
|
+
<p className="text-xs font-semibold text-slate-800 truncate">{username || "admin"}</p>
|
|
419
|
+
<p className="text-[10px] text-slate-500 font-mono capitalize">{role || "Super Admin"}</p>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
<button
|
|
423
|
+
onClick={disconnect}
|
|
424
|
+
className="p-1 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
|
425
|
+
title="Disconnect"
|
|
426
|
+
>
|
|
427
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
428
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
429
|
+
</svg>
|
|
430
|
+
</button>
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
|
|
434
|
+
{/* Database & Collection Modals */}
|
|
435
|
+
<CreateDatabaseModal
|
|
436
|
+
isOpen={showCreateDb}
|
|
437
|
+
onClose={() => setShowCreateDb(false)}
|
|
438
|
+
onDatabaseCreated={(newDb) => {
|
|
439
|
+
addDatabaseLocally(newDb);
|
|
440
|
+
setShowCreateDb(false);
|
|
441
|
+
}}
|
|
442
|
+
/>
|
|
443
|
+
|
|
444
|
+
<DeleteDatabaseModal
|
|
445
|
+
isOpen={!!dbToDelete}
|
|
446
|
+
dbName={dbToDelete || ""}
|
|
447
|
+
onClose={() => setDbToDelete(null)}
|
|
448
|
+
onConfirmDelete={() => {
|
|
449
|
+
if (dbToDelete) removeDatabaseLocally(dbToDelete);
|
|
450
|
+
setDbToDelete(null);
|
|
451
|
+
}}
|
|
452
|
+
/>
|
|
453
|
+
|
|
454
|
+
<CreateCollectionModal
|
|
455
|
+
isOpen={showCreateColl}
|
|
456
|
+
databaseName={createCollDb}
|
|
457
|
+
onClose={() => setShowCreateColl(false)}
|
|
458
|
+
onCollectionCreated={(collName) => {
|
|
459
|
+
if (createCollDb) addCollectionLocally(createCollDb, collName);
|
|
460
|
+
setShowCreateColl(false);
|
|
461
|
+
}}
|
|
462
|
+
/>
|
|
463
|
+
|
|
464
|
+
<DeleteCollectionModal
|
|
465
|
+
isOpen={!!collToDelete}
|
|
466
|
+
databaseName={collToDelete?.dbName || ""}
|
|
467
|
+
collectionName={collToDelete?.collName || ""}
|
|
468
|
+
onClose={() => setCollToDelete(null)}
|
|
469
|
+
onCollectionDeleted={() => {
|
|
470
|
+
if (collToDelete) removeCollectionLocally(collToDelete.dbName, collToDelete.collName);
|
|
471
|
+
setCollToDelete(null);
|
|
472
|
+
}}
|
|
473
|
+
/>
|
|
474
|
+
</aside>
|
|
475
|
+
);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export default Sidebar;
|