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,294 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import authApi from "../api/authApi";
|
|
3
|
+
import { useAuthStore } from "../store/authStore";
|
|
4
|
+
import CreateUserModal from "../components/auth/CreateUserModal";
|
|
5
|
+
import CreateRoleModal from "../components/auth/CreateRoleModal";
|
|
6
|
+
import ResetPasswordModal from "../components/auth/ResetPasswordModal";
|
|
7
|
+
|
|
8
|
+
const UserManagement = () => {
|
|
9
|
+
const permissions = useAuthStore((state) => state.permissions);
|
|
10
|
+
const currentUsername = useAuthStore((state) => state.username);
|
|
11
|
+
const [users, setUsers] = useState([]);
|
|
12
|
+
const [roles, setRoles] = useState([]);
|
|
13
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
14
|
+
const [error, setError] = useState("");
|
|
15
|
+
const [isCreateUserOpen, setIsCreateUserOpen] = useState(false);
|
|
16
|
+
const [isCreateRoleOpen, setIsCreateRoleOpen] = useState(false);
|
|
17
|
+
const [resetPasswordTarget, setResetPasswordTarget] = useState(null);
|
|
18
|
+
|
|
19
|
+
const canCreateUser = permissions.includes("user:create");
|
|
20
|
+
const canDeleteUser = permissions.includes("user:delete");
|
|
21
|
+
const canUpdateUserRole = permissions.includes("user:update-role");
|
|
22
|
+
const canResetPassword = permissions.includes("user:reset-password");
|
|
23
|
+
const canCreateRole = permissions.includes("role:create");
|
|
24
|
+
|
|
25
|
+
const loadData = async () => {
|
|
26
|
+
setIsLoading(true);
|
|
27
|
+
setError("");
|
|
28
|
+
try {
|
|
29
|
+
const [usersRes, rolesRes] = await Promise.all([authApi.listUsers(), authApi.listRoles()]);
|
|
30
|
+
setUsers(usersRes.data.data || []);
|
|
31
|
+
setRoles(rolesRes.data.data || []);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
setError(err.response?.data?.message || "Failed to load users and roles");
|
|
34
|
+
} finally {
|
|
35
|
+
setIsLoading(false);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
loadData();
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
const handleRoleChange = async (username, newRole) => {
|
|
44
|
+
try {
|
|
45
|
+
await authApi.updateUserRole(username, newRole);
|
|
46
|
+
loadData();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError(err.response?.data?.message || "Failed to update role");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleDelete = async (username) => {
|
|
53
|
+
if (!window.confirm(`Delete user "${username}"? This cannot be undone.`)) return;
|
|
54
|
+
try {
|
|
55
|
+
await authApi.deleteUser(username);
|
|
56
|
+
loadData();
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setError(err.response?.data?.message || "Failed to delete user");
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex-1 overflow-y-auto bg-slate-50 p-6 md:p-8">
|
|
64
|
+
<div className="max-w-6xl mx-auto space-y-6">
|
|
65
|
+
{/* Header */}
|
|
66
|
+
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-xs flex items-center justify-between">
|
|
67
|
+
<div className="flex items-center gap-4">
|
|
68
|
+
<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">
|
|
69
|
+
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
70
|
+
<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" />
|
|
71
|
+
</svg>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<h1 className="text-xl font-bold text-slate-900">User & Access Control</h1>
|
|
75
|
+
<p className="text-xs text-slate-500 mt-0.5">
|
|
76
|
+
Manage database users, RBAC roles, and operational permissions.
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex items-center gap-3">
|
|
81
|
+
{canCreateUser && (
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
onClick={() => setIsCreateUserOpen(true)}
|
|
85
|
+
className="px-3.5 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold rounded-lg shadow-xs transition-all cursor-pointer flex items-center gap-2"
|
|
86
|
+
>
|
|
87
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
88
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
89
|
+
</svg>
|
|
90
|
+
New User
|
|
91
|
+
</button>
|
|
92
|
+
)}
|
|
93
|
+
{canCreateRole && (
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => setIsCreateRoleOpen(true)}
|
|
97
|
+
className="px-3.5 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-semibold rounded-lg transition-all cursor-pointer flex items-center gap-2"
|
|
98
|
+
>
|
|
99
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
100
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
101
|
+
</svg>
|
|
102
|
+
New Role
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{error && (
|
|
109
|
+
<div className="p-4 rounded-xl bg-red-50 border border-red-200 text-red-700 text-xs flex items-center gap-3">
|
|
110
|
+
<svg className="w-5 h-5 shrink-0 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
111
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
112
|
+
</svg>
|
|
113
|
+
<span>{error}</span>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* Users Table */}
|
|
118
|
+
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-xs">
|
|
119
|
+
<div className="px-5 py-4 border-b border-slate-200 flex items-center justify-between">
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<h2 className="text-sm font-bold text-slate-900">Registered Users</h2>
|
|
122
|
+
<span className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 text-xs font-medium">
|
|
123
|
+
{users.length}
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{isLoading ? (
|
|
129
|
+
<div className="p-8 text-center text-slate-400 text-xs">Loading users...</div>
|
|
130
|
+
) : (
|
|
131
|
+
<div className="overflow-x-auto">
|
|
132
|
+
<table className="w-full text-left text-xs">
|
|
133
|
+
<thead>
|
|
134
|
+
<tr className="bg-slate-50 border-b border-slate-200 text-slate-500 uppercase tracking-wider font-semibold">
|
|
135
|
+
<th className="px-5 py-3">Username</th>
|
|
136
|
+
<th className="px-5 py-3">Assigned Role</th>
|
|
137
|
+
<th className="px-5 py-3">Account Status</th>
|
|
138
|
+
<th className="px-5 py-3 text-right">Actions</th>
|
|
139
|
+
</tr>
|
|
140
|
+
</thead>
|
|
141
|
+
<tbody className="divide-y divide-slate-100">
|
|
142
|
+
{users.map((u) => (
|
|
143
|
+
<tr key={u.username} className="hover:bg-slate-50/70 transition-colors">
|
|
144
|
+
<td className="px-5 py-3.5 font-medium text-slate-900 flex items-center gap-2">
|
|
145
|
+
<div className="h-7 w-7 rounded-full bg-emerald-100 text-emerald-800 font-bold flex items-center justify-center text-xs">
|
|
146
|
+
{u.username.slice(0, 1).toUpperCase()}
|
|
147
|
+
</div>
|
|
148
|
+
<span className="font-mono">{u.username}</span>
|
|
149
|
+
{u.username === currentUsername && (
|
|
150
|
+
<span className="px-1.5 py-0.5 rounded bg-slate-100 text-slate-500 text-[10px] font-medium">
|
|
151
|
+
You
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
</td>
|
|
155
|
+
<td className="px-5 py-3.5">
|
|
156
|
+
{canUpdateUserRole ? (
|
|
157
|
+
<select
|
|
158
|
+
value={u.role}
|
|
159
|
+
onChange={(e) => handleRoleChange(u.username, e.target.value)}
|
|
160
|
+
className="border border-slate-300 rounded-md px-2.5 py-1 text-xs bg-white text-slate-800 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500/20"
|
|
161
|
+
>
|
|
162
|
+
{roles.map((r) => (
|
|
163
|
+
<option key={r.roleName} value={r.roleName}>
|
|
164
|
+
{r.roleName}
|
|
165
|
+
</option>
|
|
166
|
+
))}
|
|
167
|
+
</select>
|
|
168
|
+
) : (
|
|
169
|
+
<span className="px-2.5 py-1 rounded-md bg-slate-100 text-slate-700 font-medium">
|
|
170
|
+
{u.role}
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
</td>
|
|
174
|
+
<td className="px-5 py-3.5">
|
|
175
|
+
{u.mustChangePassword ? (
|
|
176
|
+
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-amber-50 text-amber-700 border border-amber-200 text-[11px] font-medium">
|
|
177
|
+
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
|
|
178
|
+
Must Change Password
|
|
179
|
+
</span>
|
|
180
|
+
) : (
|
|
181
|
+
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 text-[11px] font-medium">
|
|
182
|
+
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
|
183
|
+
Active
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
</td>
|
|
187
|
+
<td className="px-5 py-3.5 text-right space-x-2">
|
|
188
|
+
{canResetPassword && (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={() => setResetPasswordTarget(u.username)}
|
|
192
|
+
className="px-2.5 py-1 text-xs font-semibold text-slate-600 hover:text-slate-900 bg-slate-100 hover:bg-slate-200 rounded-md transition-colors cursor-pointer"
|
|
193
|
+
>
|
|
194
|
+
Reset Password
|
|
195
|
+
</button>
|
|
196
|
+
)}
|
|
197
|
+
{canDeleteUser && u.username !== currentUsername && (
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
onClick={() => handleDelete(u.username)}
|
|
201
|
+
className="px-2.5 py-1 text-xs font-semibold text-red-600 hover:text-red-700 bg-red-50 hover:bg-red-100 rounded-md transition-colors cursor-pointer"
|
|
202
|
+
>
|
|
203
|
+
Delete
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
</td>
|
|
207
|
+
</tr>
|
|
208
|
+
))}
|
|
209
|
+
</tbody>
|
|
210
|
+
</table>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Roles Table */}
|
|
216
|
+
<div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-xs">
|
|
217
|
+
<div className="px-5 py-4 border-b border-slate-200 flex items-center justify-between">
|
|
218
|
+
<div className="flex items-center gap-2">
|
|
219
|
+
<h2 className="text-sm font-bold text-slate-900">Security Roles & Permissions</h2>
|
|
220
|
+
<span className="px-2 py-0.5 rounded-full bg-slate-100 text-slate-600 text-xs font-medium">
|
|
221
|
+
{roles.length}
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<div className="overflow-x-auto">
|
|
227
|
+
<table className="w-full text-left text-xs">
|
|
228
|
+
<thead>
|
|
229
|
+
<tr className="bg-slate-50 border-b border-slate-200 text-slate-500 uppercase tracking-wider font-semibold">
|
|
230
|
+
<th className="px-5 py-3">Role Name</th>
|
|
231
|
+
<th className="px-5 py-3">Granted Permissions</th>
|
|
232
|
+
<th className="px-5 py-3">Category</th>
|
|
233
|
+
</tr>
|
|
234
|
+
</thead>
|
|
235
|
+
<tbody className="divide-y divide-slate-100">
|
|
236
|
+
{roles.map((r) => (
|
|
237
|
+
<tr key={r.roleName} className="hover:bg-slate-50/70 transition-colors">
|
|
238
|
+
<td className="px-5 py-3.5 font-bold text-slate-900">{r.roleName}</td>
|
|
239
|
+
<td className="px-5 py-3.5 text-slate-600 font-mono text-[11px]">
|
|
240
|
+
{r.permissions.length} capabilities
|
|
241
|
+
<div className="flex flex-wrap gap-1 mt-1 max-w-xl">
|
|
242
|
+
{r.permissions.slice(0, 6).map((p) => (
|
|
243
|
+
<span key={p} className="px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 text-[10px]">
|
|
244
|
+
{p}
|
|
245
|
+
</span>
|
|
246
|
+
))}
|
|
247
|
+
{r.permissions.length > 6 && (
|
|
248
|
+
<span className="px-1.5 py-0.5 rounded bg-slate-100 text-slate-400 text-[10px]">
|
|
249
|
+
+{r.permissions.length - 6} more
|
|
250
|
+
</span>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</td>
|
|
254
|
+
<td className="px-5 py-3.5">
|
|
255
|
+
{r.isSystemRole ? (
|
|
256
|
+
<span className="px-2 py-0.5 rounded bg-slate-100 text-slate-600 text-[11px] font-medium">
|
|
257
|
+
System Role
|
|
258
|
+
</span>
|
|
259
|
+
) : (
|
|
260
|
+
<span className="px-2 py-0.5 rounded bg-purple-50 text-purple-700 border border-purple-200 text-[11px] font-medium">
|
|
261
|
+
Custom Role
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</td>
|
|
265
|
+
</tr>
|
|
266
|
+
))}
|
|
267
|
+
</tbody>
|
|
268
|
+
</table>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<CreateUserModal
|
|
273
|
+
isOpen={isCreateUserOpen}
|
|
274
|
+
onClose={() => setIsCreateUserOpen(false)}
|
|
275
|
+
onUserCreated={loadData}
|
|
276
|
+
roles={roles}
|
|
277
|
+
/>
|
|
278
|
+
<CreateRoleModal
|
|
279
|
+
isOpen={isCreateRoleOpen}
|
|
280
|
+
onClose={() => setIsCreateRoleOpen(false)}
|
|
281
|
+
onRoleCreated={loadData}
|
|
282
|
+
/>
|
|
283
|
+
<ResetPasswordModal
|
|
284
|
+
isOpen={!!resetPasswordTarget}
|
|
285
|
+
username={resetPasswordTarget}
|
|
286
|
+
onClose={() => setResetPasswordTarget(null)}
|
|
287
|
+
onPasswordReset={loadData}
|
|
288
|
+
/>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export default UserManagement;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { motion } from "framer-motion";
|
|
3
|
+
import { useConnectionStore } from "../store/connectionStore";
|
|
4
|
+
import axioLogo from "../assets/AXioDB.png";
|
|
5
|
+
|
|
6
|
+
const HIGHLIGHTS = [
|
|
7
|
+
{
|
|
8
|
+
icon: (
|
|
9
|
+
<svg className="w-5 h-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
10
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
11
|
+
</svg>
|
|
12
|
+
),
|
|
13
|
+
title: "Pure JavaScript Speed",
|
|
14
|
+
description: "Embedded NoSQL database with zero native bindings, zero node-gyp, and instant cold starts.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
icon: (
|
|
18
|
+
<svg className="w-5 h-5 text-sky-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
19
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
20
|
+
</svg>
|
|
21
|
+
),
|
|
22
|
+
title: "MongoDB-Style Queries",
|
|
23
|
+
description: "Intuitive query and aggregation console with live syntax validation and code completions.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
icon: (
|
|
27
|
+
<svg className="w-5 h-5 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
28
|
+
<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" />
|
|
29
|
+
</svg>
|
|
30
|
+
),
|
|
31
|
+
title: "In-Memory Cache & Metrics",
|
|
32
|
+
description: "Real-time cache ceiling monitoring, disk utilization donuts, and hierarchical database tree views.",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
icon: (
|
|
36
|
+
<svg className="w-5 h-5 text-amber-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
37
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
38
|
+
</svg>
|
|
39
|
+
),
|
|
40
|
+
title: "ACID Consistency & RBAC",
|
|
41
|
+
description: "Write-ahead logging, transactional safety, and robust role-based permission control.",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const Welcome = ({ onGetStarted }) => {
|
|
46
|
+
const { setHasSeenWelcome } = useConnectionStore();
|
|
47
|
+
const [dontShowAgain, setDontShowAgain] = useState(true);
|
|
48
|
+
|
|
49
|
+
const handleStart = () => {
|
|
50
|
+
if (dontShowAgain) {
|
|
51
|
+
setHasSeenWelcome(true);
|
|
52
|
+
}
|
|
53
|
+
onGetStarted();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="relative min-h-screen bg-slate-50 flex items-center justify-center p-6 overflow-hidden select-none font-sans">
|
|
58
|
+
{/* Ambient background glows */}
|
|
59
|
+
<div
|
|
60
|
+
className="pointer-events-none absolute -top-32 -left-32 w-96 h-96 rounded-full bg-emerald-500/10 blur-[100px]"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
/>
|
|
63
|
+
<div
|
|
64
|
+
className="pointer-events-none absolute -bottom-32 -right-32 w-96 h-96 rounded-full bg-sky-500/10 blur-[100px]"
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<div className="relative max-w-3xl w-full flex flex-col items-center text-center">
|
|
69
|
+
{/* Animated Logo Container */}
|
|
70
|
+
<motion.div
|
|
71
|
+
initial={{ scale: 0.85, opacity: 0, y: -16 }}
|
|
72
|
+
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
73
|
+
transition={{ duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
|
|
74
|
+
className="relative mb-6"
|
|
75
|
+
>
|
|
76
|
+
<div className="h-20 w-20 rounded-2xl p-2 bg-white border border-slate-200 shadow-xl flex items-center justify-center">
|
|
77
|
+
<img src={axioLogo} alt="AxioDB Logo" className="h-16 w-16 object-contain rounded-xl" />
|
|
78
|
+
</div>
|
|
79
|
+
</motion.div>
|
|
80
|
+
|
|
81
|
+
{/* Header Titles */}
|
|
82
|
+
<motion.div
|
|
83
|
+
initial={{ opacity: 0, y: 12 }}
|
|
84
|
+
animate={{ opacity: 1, y: 0 }}
|
|
85
|
+
transition={{ delay: 0.1, duration: 0.4 }}
|
|
86
|
+
className="space-y-2 mb-8"
|
|
87
|
+
>
|
|
88
|
+
<div className="inline-flex items-center gap-1.5 px-3 py-0.5 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-700 text-xs font-semibold uppercase tracking-wider">
|
|
89
|
+
<span className="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
|
|
90
|
+
Desktop Management Suite
|
|
91
|
+
</div>
|
|
92
|
+
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-slate-900">
|
|
93
|
+
Welcome to <span className="bg-gradient-to-r from-emerald-600 via-teal-600 to-sky-600 bg-clip-text text-transparent">AxioDB Control</span>
|
|
94
|
+
</h1>
|
|
95
|
+
<p className="text-slate-600 max-w-lg text-sm leading-relaxed mx-auto">
|
|
96
|
+
High-performance desktop client for exploring, querying, and managing AxioDB instances with MongoDB Compass-style productivity.
|
|
97
|
+
</p>
|
|
98
|
+
</motion.div>
|
|
99
|
+
|
|
100
|
+
{/* Feature Grid */}
|
|
101
|
+
<motion.div
|
|
102
|
+
initial={{ opacity: 0, y: 16 }}
|
|
103
|
+
animate={{ opacity: 1, y: 0 }}
|
|
104
|
+
transition={{ delay: 0.2, duration: 0.5 }}
|
|
105
|
+
className="grid grid-cols-1 md:grid-cols-2 gap-3 w-full mb-8"
|
|
106
|
+
>
|
|
107
|
+
{HIGHLIGHTS.map((item, i) => (
|
|
108
|
+
<div
|
|
109
|
+
key={i}
|
|
110
|
+
className="p-4 rounded-xl bg-white border border-slate-200 text-left shadow-2xs hover:border-emerald-300 hover:shadow-xs transition-all"
|
|
111
|
+
>
|
|
112
|
+
<div className="flex items-start gap-3">
|
|
113
|
+
<div className="p-2 rounded-lg bg-slate-50 border border-slate-100 shrink-0">
|
|
114
|
+
{item.icon}
|
|
115
|
+
</div>
|
|
116
|
+
<div>
|
|
117
|
+
<h3 className="text-xs font-bold text-slate-900 mb-0.5">{item.title}</h3>
|
|
118
|
+
<p className="text-xs text-slate-500 leading-relaxed">{item.description}</p>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</motion.div>
|
|
124
|
+
|
|
125
|
+
{/* Action Controls */}
|
|
126
|
+
<motion.div
|
|
127
|
+
initial={{ opacity: 0, y: 12 }}
|
|
128
|
+
animate={{ opacity: 1, y: 0 }}
|
|
129
|
+
transition={{ delay: 0.3, duration: 0.4 }}
|
|
130
|
+
className="flex flex-col items-center gap-3 w-full max-w-xs"
|
|
131
|
+
>
|
|
132
|
+
<button
|
|
133
|
+
onClick={handleStart}
|
|
134
|
+
className="w-full py-2.5 px-6 rounded-xl bg-emerald-600 hover:bg-emerald-700 active:scale-[0.98] text-white font-semibold text-sm shadow-sm transition-all flex items-center justify-center gap-2"
|
|
135
|
+
>
|
|
136
|
+
<span>Connect to Instance</span>
|
|
137
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
138
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
|
139
|
+
</svg>
|
|
140
|
+
</button>
|
|
141
|
+
|
|
142
|
+
<label className="flex items-center gap-2 text-xs text-slate-500 cursor-pointer hover:text-slate-700">
|
|
143
|
+
<input
|
|
144
|
+
type="checkbox"
|
|
145
|
+
checked={dontShowAgain}
|
|
146
|
+
onChange={(e) => setDontShowAgain(e.target.checked)}
|
|
147
|
+
className="rounded border-slate-300 text-emerald-600 focus:ring-emerald-500"
|
|
148
|
+
/>
|
|
149
|
+
<span>Don't show welcome screen on launch</span>
|
|
150
|
+
</label>
|
|
151
|
+
</motion.div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export default Welcome;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
export const useAuthStore = create((set) => ({
|
|
4
|
+
username: null,
|
|
5
|
+
role: null,
|
|
6
|
+
permissions: [],
|
|
7
|
+
mustChangePassword: false,
|
|
8
|
+
isAuthenticated: false,
|
|
9
|
+
isLoading: true,
|
|
10
|
+
|
|
11
|
+
setSession: ({ username, role, permissions, mustChangePassword }) =>
|
|
12
|
+
set({
|
|
13
|
+
username,
|
|
14
|
+
role,
|
|
15
|
+
permissions: permissions || [],
|
|
16
|
+
mustChangePassword: !!mustChangePassword,
|
|
17
|
+
isAuthenticated: true,
|
|
18
|
+
isLoading: false,
|
|
19
|
+
}),
|
|
20
|
+
|
|
21
|
+
clearSession: () =>
|
|
22
|
+
set({
|
|
23
|
+
username: null,
|
|
24
|
+
role: null,
|
|
25
|
+
permissions: [],
|
|
26
|
+
mustChangePassword: false,
|
|
27
|
+
isAuthenticated: false,
|
|
28
|
+
isLoading: false,
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY_SAVED = "axiodb_saved_connections";
|
|
4
|
+
const STORAGE_KEY_WELCOME = "axiodb_welcome_seen";
|
|
5
|
+
|
|
6
|
+
const loadSavedConnections = () => {
|
|
7
|
+
try {
|
|
8
|
+
const raw = localStorage.getItem(STORAGE_KEY_SAVED);
|
|
9
|
+
return raw ? JSON.parse(raw) : [];
|
|
10
|
+
} catch {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const loadWelcomeStatus = () => {
|
|
16
|
+
try {
|
|
17
|
+
return localStorage.getItem(STORAGE_KEY_WELCOME) === "true";
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const useConnectionStore = create((set, get) => ({
|
|
24
|
+
protocol: "http",
|
|
25
|
+
host: "localhost",
|
|
26
|
+
port: 27018,
|
|
27
|
+
username: "admin",
|
|
28
|
+
password: "",
|
|
29
|
+
name: "Local Instance",
|
|
30
|
+
|
|
31
|
+
isConnected: false,
|
|
32
|
+
isConnecting: false,
|
|
33
|
+
connectionError: null,
|
|
34
|
+
pingLatency: null,
|
|
35
|
+
serverVersion: null,
|
|
36
|
+
|
|
37
|
+
savedConnections: loadSavedConnections(),
|
|
38
|
+
hasSeenWelcome: loadWelcomeStatus(),
|
|
39
|
+
|
|
40
|
+
setConnectionParams: (params) => set((state) => ({ ...state, ...params })),
|
|
41
|
+
|
|
42
|
+
setIsConnected: (isConnected) => set({ isConnected }),
|
|
43
|
+
setIsConnecting: (isConnecting) => set({ isConnecting }),
|
|
44
|
+
setConnectionError: (connectionError) => set({ connectionError }),
|
|
45
|
+
setPingLatency: (pingLatency) => set({ pingLatency }),
|
|
46
|
+
setServerVersion: (serverVersion) => set({ serverVersion }),
|
|
47
|
+
|
|
48
|
+
setHasSeenWelcome: (hasSeenWelcome) => {
|
|
49
|
+
try {
|
|
50
|
+
localStorage.setItem(STORAGE_KEY_WELCOME, hasSeenWelcome ? "true" : "false");
|
|
51
|
+
} catch {
|
|
52
|
+
// ignore
|
|
53
|
+
}
|
|
54
|
+
set({ hasSeenWelcome });
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
saveConnection: (connection) => {
|
|
58
|
+
const current = get().savedConnections;
|
|
59
|
+
const existingIndex = current.findIndex(
|
|
60
|
+
(c) => c.host === connection.host && c.port === connection.port && c.username === connection.username
|
|
61
|
+
);
|
|
62
|
+
let updated;
|
|
63
|
+
if (existingIndex >= 0) {
|
|
64
|
+
updated = [...current];
|
|
65
|
+
updated[existingIndex] = { ...updated[existingIndex], ...connection };
|
|
66
|
+
} else {
|
|
67
|
+
updated = [connection, ...current];
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
localStorage.setItem(STORAGE_KEY_SAVED, JSON.stringify(updated));
|
|
71
|
+
} catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
set({ savedConnections: updated });
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
deleteSavedConnection: (id) => {
|
|
78
|
+
const updated = get().savedConnections.filter((c) => c.id !== id);
|
|
79
|
+
try {
|
|
80
|
+
localStorage.setItem(STORAGE_KEY_SAVED, JSON.stringify(updated));
|
|
81
|
+
} catch {
|
|
82
|
+
// ignore
|
|
83
|
+
}
|
|
84
|
+
set({ savedConnections: updated });
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
getBaseUrl: () => {
|
|
88
|
+
const { protocol, host, port } = get();
|
|
89
|
+
return `${protocol}://${host}:${port}`;
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
disconnect: () => {
|
|
93
|
+
if (window.electronAPI?.clearCookies) {
|
|
94
|
+
window.electronAPI.clearCookies();
|
|
95
|
+
}
|
|
96
|
+
set({
|
|
97
|
+
isConnected: false,
|
|
98
|
+
isConnecting: false,
|
|
99
|
+
connectionError: null,
|
|
100
|
+
pingLatency: null,
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
}));
|