axiodb 22.2.2 → 22.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/electron/electron-builder.json +58 -0
  2. package/electron/main/main.cts +585 -0
  3. package/electron/main/preload.cts +51 -0
  4. package/electron/package-lock.json +8123 -0
  5. package/electron/package.json +52 -0
  6. package/electron/public/AXioDB.png +0 -0
  7. package/electron/resources/after-install.sh +45 -0
  8. package/electron/resources/after-remove.sh +29 -0
  9. package/electron/resources/icon.png +0 -0
  10. package/electron/resources/icons/128x128.png +0 -0
  11. package/electron/resources/icons/16x16.png +0 -0
  12. package/electron/resources/icons/24x24.png +0 -0
  13. package/electron/resources/icons/256x256.png +0 -0
  14. package/electron/resources/icons/32x32.png +0 -0
  15. package/electron/resources/icons/48x48.png +0 -0
  16. package/electron/resources/icons/512x512.png +0 -0
  17. package/electron/resources/icons/64x64.png +0 -0
  18. package/electron/src/App.jsx +128 -0
  19. package/electron/src/api/authApi.js +83 -0
  20. package/electron/src/api/client.js +102 -0
  21. package/electron/src/assets/AXioDB.png +0 -0
  22. package/electron/src/components/auth/CreateRoleModal.jsx +189 -0
  23. package/electron/src/components/auth/CreateUserModal.jsx +131 -0
  24. package/electron/src/components/auth/ForcePasswordChangeModal.jsx +132 -0
  25. package/electron/src/components/auth/ProtectedRoute.jsx +42 -0
  26. package/electron/src/components/auth/ResetPasswordModal.jsx +90 -0
  27. package/electron/src/components/auth/UserAvatarMenu.jsx +103 -0
  28. package/electron/src/components/collection/CreateCollectionModal.jsx +116 -0
  29. package/electron/src/components/collection/DeleteCollectionModal.jsx +85 -0
  30. package/electron/src/components/collection/SchemaViewModal.jsx +369 -0
  31. package/electron/src/components/dashboard/CollectionsChart.jsx +94 -0
  32. package/electron/src/components/dashboard/DatabaseTreeView.jsx +130 -0
  33. package/electron/src/components/dashboard/InMemoryCacheCard.jsx +60 -0
  34. package/electron/src/components/dashboard/StorageDonut.jsx +76 -0
  35. package/electron/src/components/dashboard/StorageUsageCard.jsx +59 -0
  36. package/electron/src/components/dashboard/TotalCollectionsCard.jsx +47 -0
  37. package/electron/src/components/dashboard/TotalDatabasesCard.jsx +43 -0
  38. package/electron/src/components/dashboard/TotalDocumentsCard.jsx +44 -0
  39. package/electron/src/components/database/CreateDatabaseModal.jsx +109 -0
  40. package/electron/src/components/database/DeleteDatabaseModal.jsx +97 -0
  41. package/electron/src/components/query/CodeEditor.jsx +343 -0
  42. package/electron/src/components/query/ObjectEditor.jsx +115 -0
  43. package/electron/src/components/query/ObjectView.jsx +44 -0
  44. package/electron/src/components/query/QueryEditor.jsx +32 -0
  45. package/electron/src/components/query/queryLanguage.js +755 -0
  46. package/electron/src/components/ui/Button.jsx +58 -0
  47. package/electron/src/components/ui/Card.jsx +29 -0
  48. package/electron/src/components/ui/ErrorBoundary.jsx +108 -0
  49. package/electron/src/components/ui/Feedback.jsx +66 -0
  50. package/electron/src/components/ui/Field.jsx +65 -0
  51. package/electron/src/components/ui/MetricCard.jsx +104 -0
  52. package/electron/src/components/ui/Modal.jsx +119 -0
  53. package/electron/src/components/ui/Page.jsx +40 -0
  54. package/electron/src/config/key.js +11 -0
  55. package/electron/src/index.css +181 -0
  56. package/electron/src/index.html +13 -0
  57. package/electron/src/layout/Sidebar.jsx +478 -0
  58. package/electron/src/layout/StatusBar.jsx +70 -0
  59. package/electron/src/layout/Titlebar.jsx +128 -0
  60. package/electron/src/main.jsx +42 -0
  61. package/electron/src/pages/ConnectionHub.jsx +464 -0
  62. package/electron/src/pages/Dashboard.jsx +128 -0
  63. package/electron/src/pages/Documents.jsx +823 -0
  64. package/electron/src/pages/Import.jsx +527 -0
  65. package/electron/src/pages/UserManagement.jsx +294 -0
  66. package/electron/src/pages/Welcome.jsx +157 -0
  67. package/electron/src/store/authStore.js +30 -0
  68. package/electron/src/store/connectionStore.js +103 -0
  69. package/electron/src/store/dbStore.js +165 -0
  70. package/electron/src/store/store.js +8 -0
  71. package/electron/src/utils/format.js +39 -0
  72. package/electron/vite.config.js +28 -0
  73. package/lib/Services/Indexation.operation.js +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,823 @@
1
+ import React, { useState, useEffect, useCallback, useMemo } from "react";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { useDbStore } from "../store/dbStore";
4
+ import apiClient from "../api/client";
5
+ import ObjectView from "../components/query/ObjectView";
6
+ import QueryEditor from "../components/query/QueryEditor";
7
+ import { parseExpression, parseLiteral, validate } from "../components/query/queryLanguage";
8
+ import InsertDocumentModal from "../components/document/InsertDocumentModal";
9
+ import UpdateDocumentModal from "../components/document/UpdateDocumentModal";
10
+ import DeleteDocumentModal from "../components/document/DeleteDocumentModal";
11
+ import CreateCollectionModal from "../components/collection/CreateCollectionModal";
12
+ import DeleteCollectionModal from "../components/collection/DeleteCollectionModal";
13
+ import DocumentCard from "../components/document/DocumentCard";
14
+
15
+ const Documents = () => {
16
+ const {
17
+ databases,
18
+ collectionsMap,
19
+ selectedDatabase,
20
+ selectedCollection,
21
+ activeTab,
22
+ setSelectedDatabase,
23
+ setSelectedCollection,
24
+ setActiveTab,
25
+ fetchCollections,
26
+ fetchDatabases,
27
+ addCollectionLocally,
28
+ removeCollectionLocally,
29
+ } = useDbStore();
30
+
31
+ // Documents & Query state
32
+ const [documents, setDocuments] = useState([]);
33
+ const [loading, setLoading] = useState(false);
34
+ const [error, setError] = useState(null);
35
+ const [page, setPage] = useState(1);
36
+ const [limit, setLimit] = useState(20);
37
+ const [totalDocs, setTotalDocs] = useState(0);
38
+ const [queryLatency, setQueryLatency] = useState(null);
39
+
40
+ // Compass-style Filter Bar inputs (accepts relaxed JS object literals)
41
+ const [filterStr, setFilterStr] = useState("");
42
+ const [sortStr, setSortStr] = useState("");
43
+ const [projectStr, setProjectStr] = useState("");
44
+ const [skipStr, setSkipStr] = useState("0");
45
+
46
+ // Query Console state
47
+ const [consoleQuery, setConsoleQuery] = useState("");
48
+ const [consoleResults, setConsoleResults] = useState(null);
49
+ const [consoleRunning, setConsoleRunning] = useState(false);
50
+ const [consoleError, setConsoleError] = useState(null);
51
+
52
+ // Modals state
53
+ const [showInsertModal, setShowInsertModal] = useState(false);
54
+ const [showUpdateModal, setShowUpdateModal] = useState(false);
55
+ const [showDeleteModal, setShowDeleteModal] = useState(false);
56
+ const [selectedDoc, setSelectedDoc] = useState(null);
57
+ const [inspectorDoc, setInspectorDoc] = useState(null); // Side Drawer
58
+ const [showCreateColl, setShowCreateColl] = useState(false);
59
+ const [collToDelete, setCollToDelete] = useState(null);
60
+
61
+ // Sync console query when selected collection changes
62
+ useEffect(() => {
63
+ if (selectedCollection) {
64
+ setConsoleQuery(`${selectedCollection}.query({}).exec()`);
65
+ setConsoleResults(null);
66
+ setConsoleError(null);
67
+ }
68
+ }, [selectedCollection]);
69
+
70
+ // Extract known field keys from loaded documents to power autocomplete suggestions
71
+ const collectionFields = useMemo(() => {
72
+ if (!documents || documents.length === 0) {
73
+ return ["name", "title", "status", "role", "type", "description", "age", "email"];
74
+ }
75
+ const keySet = new Set();
76
+ for (const doc of documents) {
77
+ if (doc && typeof doc === "object") {
78
+ Object.keys(doc).forEach((k) => {
79
+ if (k !== "_id" && k !== "documentId" && k !== "updatedAt") {
80
+ keySet.add(k);
81
+ }
82
+ });
83
+ }
84
+ }
85
+ return Array.from(keySet);
86
+ }, [documents]);
87
+
88
+ // Query Console diagnostics
89
+ const consoleDiagnostics = useMemo(() => {
90
+ if (!consoleQuery.trim() || !selectedCollection) return [];
91
+ return validate(consoleQuery, selectedCollection);
92
+ }, [consoleQuery, selectedCollection]);
93
+
94
+ // Quick query examples for the Query Console
95
+ const queryExamples = useMemo(() => {
96
+ const col = selectedCollection || "collection";
97
+ return [
98
+ { label: "All documents", code: `${col}.query({}).exec()` },
99
+ { label: "Exact match", code: `${col}.query({ status: 'active' }).exec()` },
100
+ { label: "Comparison", code: `${col}.query({ age: { $gte: 18 } }).exec()` },
101
+ { label: "Any of", code: `${col}.query({ status: { $in: ['active', 'pending'] } }).exec()` },
102
+ { label: "Pattern", code: `${col}.query({ name: { $regex: '^a', $options: 'i' } }).exec()` },
103
+ { label: "Aggregate", code: `${col}.aggregate([{ $match: {} }, { $group: { _id: '$status', total: { $sum: 1 } } }]).exec()` },
104
+ ];
105
+ }, [selectedCollection]);
106
+
107
+ // Load documents when collection or page changes
108
+ const loadDocuments = useCallback(async () => {
109
+ if (!selectedDatabase || !selectedCollection) return;
110
+ setLoading(true);
111
+ setError(null);
112
+ const start = performance.now();
113
+
114
+ try {
115
+ let url = `/api/operation/all/?dbName=${encodeURIComponent(selectedDatabase)}&collectionName=${encodeURIComponent(selectedCollection)}&page=${page}&limit=${limit}`;
116
+ const res = await apiClient.get(url);
117
+ const latency = Math.round(performance.now() - start);
118
+ setQueryLatency(latency);
119
+
120
+ const payload = res.data?.data?.data || res.data?.data || {};
121
+ const docs = Array.isArray(payload.documents) ? payload.documents : (Array.isArray(payload) ? payload : []);
122
+ setDocuments(docs);
123
+ setTotalDocs(payload.totalDocuments ?? docs.length);
124
+ } catch (err) {
125
+ console.error("Failed to load documents:", err);
126
+ setError(err.response?.data?.message || err.message || "Failed to load documents");
127
+ setDocuments([]);
128
+ } finally {
129
+ setLoading(false);
130
+ }
131
+ }, [selectedDatabase, selectedCollection, page, limit]);
132
+
133
+ useEffect(() => {
134
+ if (selectedDatabase && selectedCollection) {
135
+ loadDocuments();
136
+ }
137
+ }, [selectedDatabase, selectedCollection, page, limit, loadDocuments]);
138
+
139
+ // Apply Compass-style filter: parses relaxed JavaScript object syntax (no JSON required)
140
+ const handleApplyFilter = async () => {
141
+ if (!selectedDatabase || !selectedCollection) return;
142
+ setLoading(true);
143
+ setError(null);
144
+ const start = performance.now();
145
+
146
+ try {
147
+ let parsedFilter = {};
148
+ if (filterStr.trim()) {
149
+ let clean = filterStr.trim();
150
+ if (!clean.startsWith("{") && !clean.startsWith("[")) {
151
+ clean = `{ ${clean} }`;
152
+ }
153
+ try {
154
+ parsedFilter = parseLiteral(clean);
155
+ } catch {
156
+ try {
157
+ parsedFilter = JSON.parse(clean);
158
+ } catch {
159
+ // eslint-disable-next-line no-new-func
160
+ parsedFilter = new Function(`return (${clean})`)();
161
+ }
162
+ }
163
+ }
164
+
165
+ const res = await apiClient.post(
166
+ `/api/operation/all/by-query/?dbName=${encodeURIComponent(selectedDatabase)}&collectionName=${encodeURIComponent(selectedCollection)}&page=${page}&limit=${limit}`,
167
+ { query: parsedFilter }
168
+ );
169
+
170
+ const latency = Math.round(performance.now() - start);
171
+ setQueryLatency(latency);
172
+
173
+ const payload = res.data?.data?.data || res.data?.data || {};
174
+ const docs = Array.isArray(payload.documents) ? payload.documents : (Array.isArray(payload) ? payload : []);
175
+ setDocuments(docs);
176
+ setTotalDocs(payload.totalDocuments ?? docs.length);
177
+ } catch (err) {
178
+ console.error("Filter error:", err);
179
+ setError(err.response?.data?.message || "Invalid query filter syntax. You can use JavaScript object syntax.");
180
+ } finally {
181
+ setLoading(false);
182
+ }
183
+ };
184
+
185
+ const handleResetFilter = () => {
186
+ setFilterStr("");
187
+ setSortStr("");
188
+ setProjectStr("");
189
+ setSkipStr("0");
190
+ setPage(1);
191
+ loadDocuments();
192
+ };
193
+
194
+ // Run Query in Console tab (supports expressions like <col>.query({...}).exec() AND raw objects)
195
+ const handleRunConsoleQuery = async () => {
196
+ if (!consoleQuery.trim() || !selectedDatabase || !selectedCollection) return;
197
+ setConsoleRunning(true);
198
+ setConsoleError(null);
199
+ const start = performance.now();
200
+
201
+ try {
202
+ const trimmed = consoleQuery.trim();
203
+ const parsed = parseExpression(trimmed);
204
+
205
+ if (parsed) {
206
+ const payload = parseLiteral(parsed.args || "{}");
207
+ if (parsed.method === "aggregate") {
208
+ const res = await apiClient.post(
209
+ `/api/operation/aggregate/?dbName=${encodeURIComponent(selectedDatabase)}&collectionName=${encodeURIComponent(selectedCollection)}`,
210
+ { aggregation: payload }
211
+ );
212
+ const docs = res.data?.data?.documents || res.data?.data || [];
213
+ const latency = Math.round(performance.now() - start);
214
+ setConsoleResults({
215
+ executionTime: `${latency}ms`,
216
+ count: docs.length,
217
+ data: docs,
218
+ });
219
+ return;
220
+ } else {
221
+ // Method is query
222
+ const res = await apiClient.post(
223
+ `/api/operation/all/by-query/?dbName=${encodeURIComponent(selectedDatabase)}&collectionName=${encodeURIComponent(selectedCollection)}&page=1&limit=50`,
224
+ { query: payload }
225
+ );
226
+ const payloadData = res.data?.data?.data || res.data?.data || {};
227
+ const docs = Array.isArray(payloadData.documents) ? payloadData.documents : (Array.isArray(payloadData) ? payloadData : []);
228
+ const latency = Math.round(performance.now() - start);
229
+ setConsoleResults({
230
+ executionTime: `${latency}ms`,
231
+ count: docs.length,
232
+ data: docs,
233
+ });
234
+ return;
235
+ }
236
+ }
237
+
238
+ // If not a method expression, treat as direct object literal: { status: 'active' }
239
+ let cleanObj = trimmed;
240
+ if (!cleanObj.startsWith("{") && !cleanObj.startsWith("[")) {
241
+ cleanObj = `{ ${cleanObj} }`;
242
+ }
243
+ const rawFilter = parseLiteral(cleanObj);
244
+ const res = await apiClient.post(
245
+ `/api/operation/all/by-query/?dbName=${encodeURIComponent(selectedDatabase)}&collectionName=${encodeURIComponent(selectedCollection)}&page=1&limit=50`,
246
+ { query: rawFilter }
247
+ );
248
+ const payloadData = res.data?.data?.data || res.data?.data || {};
249
+ const docs = Array.isArray(payloadData.documents) ? payloadData.documents : (Array.isArray(payloadData) ? payloadData : []);
250
+ const latency = Math.round(performance.now() - start);
251
+ setConsoleResults({
252
+ executionTime: `${latency}ms`,
253
+ count: docs.length,
254
+ data: docs,
255
+ });
256
+ } catch (err) {
257
+ console.error("Query console error:", err);
258
+ setConsoleError(err.response?.data?.message || err.message || "Failed to execute query");
259
+ setConsoleResults(null);
260
+ } finally {
261
+ setConsoleRunning(false);
262
+ }
263
+ };
264
+
265
+ const copyToClipboard = (val) => {
266
+ const text = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val);
267
+ navigator.clipboard.writeText(text);
268
+ };
269
+
270
+ // ==========================================
271
+ // VIEW 1: No Database Selected Placeholder
272
+ // ==========================================
273
+ if (!selectedDatabase) {
274
+ return (
275
+ <div className="flex-1 flex flex-col items-center justify-center p-8 text-center bg-slate-50">
276
+ <div className="h-16 w-16 rounded-2xl bg-emerald-100 text-emerald-700 flex items-center justify-center mb-4 border border-emerald-200 shadow-xs">
277
+ <svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
278
+ <ellipse cx="12" cy="6" rx="8" ry="3" strokeWidth={1.8} />
279
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6" />
280
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6" />
281
+ </svg>
282
+ </div>
283
+ <h2 className="text-xl font-bold text-slate-800 mb-1">Select a Database to Explore</h2>
284
+ <p className="text-sm text-slate-500 max-w-md mb-6">
285
+ Choose an existing database from the left explorer tree or create a new database to start managing collections and documents.
286
+ </p>
287
+ </div>
288
+ );
289
+ }
290
+
291
+ // ==========================================
292
+ // VIEW 2: Database Overview (No collection selected)
293
+ // ==========================================
294
+ if (!selectedCollection) {
295
+ const colls = collectionsMap[selectedDatabase] || [];
296
+
297
+ return (
298
+ <div className="flex-1 flex flex-col overflow-hidden bg-slate-50">
299
+ {/* Database Header */}
300
+ <div className="h-14 border-b border-slate-200 bg-white px-6 flex items-center justify-between shrink-0">
301
+ <div className="flex items-center gap-3">
302
+ <div className="h-9 w-9 rounded-lg bg-emerald-100 text-emerald-700 flex items-center justify-center border border-emerald-200 font-bold">
303
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
304
+ <ellipse cx="12" cy="6" rx="8" ry="3" strokeWidth={1.8} />
305
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 6v6c0 1.66 3.58 3 8 3s8-1.34 8-3V6" />
306
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M4 12v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6" />
307
+ </svg>
308
+ </div>
309
+ <div>
310
+ <h1 className="text-base font-bold text-slate-900">{selectedDatabase}</h1>
311
+ <p className="text-xs text-slate-500 font-mono">
312
+ {colls.length} {colls.length === 1 ? "Collection" : "Collections"}
313
+ </p>
314
+ </div>
315
+ </div>
316
+
317
+ <div className="flex items-center gap-2">
318
+ <button
319
+ onClick={() => setShowCreateColl(true)}
320
+ className="px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold flex items-center gap-1.5 shadow-xs transition-colors"
321
+ >
322
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
323
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
324
+ </svg>
325
+ New Collection
326
+ </button>
327
+ </div>
328
+ </div>
329
+
330
+ {/* Collections Grid */}
331
+ <div className="flex-1 overflow-y-auto p-6">
332
+ <div className="max-w-6xl mx-auto space-y-6">
333
+ <div className="flex items-center justify-between">
334
+ <h2 className="text-sm font-bold text-slate-800 uppercase tracking-wider">
335
+ Collections in {selectedDatabase}
336
+ </h2>
337
+ </div>
338
+
339
+ {colls.length === 0 ? (
340
+ <div className="bg-white border border-slate-200 rounded-xl p-12 text-center shadow-xs">
341
+ <div className="h-12 w-12 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center mx-auto mb-3">
342
+ <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
343
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
344
+ </svg>
345
+ </div>
346
+ <h3 className="text-sm font-bold text-slate-800 mb-1">No Collections Yet</h3>
347
+ <p className="text-xs text-slate-500 mb-4 max-w-sm mx-auto">
348
+ Create your first collection in this database to begin storing documents.
349
+ </p>
350
+ <button
351
+ onClick={() => setShowCreateColl(true)}
352
+ className="px-4 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold shadow-xs"
353
+ >
354
+ Create Collection
355
+ </button>
356
+ </div>
357
+ ) : (
358
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
359
+ {colls.map((c) => {
360
+ const name = c.name || c;
361
+ const count = c.count !== undefined ? c.count : (c.documentCount || 0);
362
+
363
+ return (
364
+ <div
365
+ key={name}
366
+ onClick={() => setSelectedCollection(selectedDatabase, name)}
367
+ className="bg-white border border-slate-200 hover:border-emerald-500 rounded-xl p-4 shadow-xs transition-all cursor-pointer group hover:shadow-sm"
368
+ >
369
+ <div className="flex items-center justify-between mb-3">
370
+ <div className="h-8 w-8 rounded-lg bg-slate-100 group-hover:bg-emerald-50 text-slate-600 group-hover:text-emerald-700 flex items-center justify-center transition-colors">
371
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
372
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
373
+ </svg>
374
+ </div>
375
+ <span className="text-[11px] font-mono text-slate-500 bg-slate-100 px-2 py-0.5 rounded-full">
376
+ {count} docs
377
+ </span>
378
+ </div>
379
+ <h3 className="text-sm font-bold text-slate-900 group-hover:text-emerald-700 transition-colors truncate">
380
+ {name}
381
+ </h3>
382
+ <p className="text-[11px] text-slate-400 mt-1 font-mono">
383
+ Click to open documents and query console
384
+ </p>
385
+ </div>
386
+ );
387
+ })}
388
+ </div>
389
+ )}
390
+ </div>
391
+ </div>
392
+
393
+ <CreateCollectionModal
394
+ isOpen={showCreateColl}
395
+ databaseName={selectedDatabase}
396
+ onClose={() => setShowCreateColl(false)}
397
+ onCollectionCreated={(newCollName) => {
398
+ setShowCreateColl(false);
399
+ addCollectionLocally(selectedDatabase, newCollName);
400
+ setSelectedCollection(selectedDatabase, newCollName);
401
+ }}
402
+ />
403
+ </div>
404
+ );
405
+ }
406
+
407
+ // ==========================================
408
+ // VIEW 3: Collection Workspace
409
+ // ==========================================
410
+ return (
411
+ <div className="flex-1 flex flex-col overflow-hidden bg-slate-50 font-sans select-none">
412
+ {/* Top Collection Breadcrumb Bar */}
413
+ <div className="h-12 border-b border-slate-200 bg-white px-4 flex items-center justify-between shrink-0">
414
+ <div className="flex items-center gap-2 text-xs font-mono">
415
+ <span
416
+ onClick={() => setSelectedCollection(selectedDatabase, "")}
417
+ className="text-slate-500 hover:text-emerald-700 cursor-pointer font-bold"
418
+ >
419
+ {selectedDatabase}
420
+ </span>
421
+ <span className="text-slate-300">/</span>
422
+ <span className="font-bold text-slate-900 bg-emerald-50 text-emerald-800 px-2 py-0.5 rounded">
423
+ {selectedCollection}
424
+ </span>
425
+ <span className="text-slate-400 text-[11px] ml-1">
426
+ ({totalDocs} {totalDocs === 1 ? "document" : "documents"})
427
+ </span>
428
+ </div>
429
+
430
+ {/* Tab Switcher & Action Buttons (Documents & Query Console only) */}
431
+ <div className="flex items-center gap-2">
432
+ <div className="flex p-0.5 bg-slate-100 rounded-lg border border-slate-200 text-xs font-medium text-slate-600">
433
+ <button
434
+ onClick={() => setActiveTab("documents")}
435
+ className={`px-3 py-1 rounded-md transition-all ${
436
+ activeTab === "documents"
437
+ ? "bg-white text-emerald-700 shadow-xs font-semibold"
438
+ : "hover:text-slate-900"
439
+ }`}
440
+ >
441
+ Documents
442
+ </button>
443
+ <button
444
+ onClick={() => setActiveTab("query")}
445
+ className={`px-3 py-1 rounded-md transition-all ${
446
+ activeTab === "query"
447
+ ? "bg-white text-emerald-700 shadow-xs font-semibold"
448
+ : "hover:text-slate-900"
449
+ }`}
450
+ >
451
+ Query Console
452
+ </button>
453
+ </div>
454
+
455
+ {activeTab === "documents" && (
456
+ <button
457
+ onClick={() => setShowInsertModal(true)}
458
+ className="px-3 py-1 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold flex items-center gap-1.5 shadow-xs transition-colors"
459
+ >
460
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
461
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
462
+ </svg>
463
+ Insert Document
464
+ </button>
465
+ )}
466
+
467
+ <button
468
+ onClick={() => setCollToDelete(selectedCollection)}
469
+ title="Drop Collection"
470
+ className="p-1 text-slate-400 hover:text-red-600 rounded-lg hover:bg-slate-100 transition-colors"
471
+ >
472
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
473
+ <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" />
474
+ </svg>
475
+ </button>
476
+ </div>
477
+ </div>
478
+
479
+ {/* ==========================================
480
+ TAB 1: DOCUMENTS (Compass-style Filter & Table)
481
+ ========================================== */}
482
+ {activeTab === "documents" && (
483
+ <div className="flex-1 flex flex-col overflow-hidden">
484
+ {/* Compass-Style Query & Filter Bar (JavaScript Object Literal Syntax) */}
485
+ <div className="p-3 bg-white border-b border-slate-200">
486
+ <div className="flex items-center gap-2">
487
+ {/* Filter */}
488
+ <div className="flex-1 flex items-center bg-slate-50 border border-slate-200 rounded-md px-2.5 py-1 focus-within:border-emerald-500 focus-within:bg-white focus-within:ring-1 focus-within:ring-emerald-500/20 transition-all">
489
+ <span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mr-2 font-mono">
490
+ Filter
491
+ </span>
492
+ <input
493
+ type="text"
494
+ value={filterStr}
495
+ onChange={(e) => setFilterStr(e.target.value)}
496
+ onKeyDown={(e) => e.key === "Enter" && handleApplyFilter()}
497
+ placeholder="{ status: 'active' }"
498
+ className="w-full bg-transparent text-xs font-mono text-slate-800 placeholder-slate-400 focus:outline-none"
499
+ />
500
+ </div>
501
+
502
+ {/* Sort */}
503
+ <div className="w-48 flex items-center bg-slate-50 border border-slate-200 rounded-md px-2.5 py-1 focus-within:border-emerald-500 focus-within:bg-white focus-within:ring-1 focus-within:ring-emerald-500/20 transition-all">
504
+ <span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mr-2 font-mono">
505
+ Sort
506
+ </span>
507
+ <input
508
+ type="text"
509
+ value={sortStr}
510
+ onChange={(e) => setSortStr(e.target.value)}
511
+ placeholder="{ _id: -1 }"
512
+ className="w-full bg-transparent text-xs font-mono text-slate-800 placeholder-slate-400 focus:outline-none"
513
+ />
514
+ </div>
515
+
516
+ {/* Buttons */}
517
+ <button
518
+ onClick={handleApplyFilter}
519
+ className="px-4 py-1.5 rounded-md bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-semibold shadow-xs transition-colors cursor-pointer"
520
+ >
521
+ Find
522
+ </button>
523
+
524
+ <button
525
+ onClick={handleResetFilter}
526
+ className="px-3 py-1.5 rounded-md bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-medium transition-colors cursor-pointer"
527
+ >
528
+ Reset
529
+ </button>
530
+
531
+ {/* Cards Indicator */}
532
+ <div className="flex items-center gap-2 text-xs text-slate-500 font-mono">
533
+ <span className="text-emerald-700 font-semibold">
534
+ {documents.length} document{documents.length !== 1 ? "s" : ""}
535
+ </span>
536
+ {queryLatency !== null && (
537
+ <span>⚡ {queryLatency}ms</span>
538
+ )}
539
+ </div>
540
+ </div>
541
+
542
+ {/* Sub-bar: Query Timing & Active Filter */}
543
+ {queryLatency !== null && (
544
+ <div className="flex items-center gap-3 mt-2 text-[11px] text-slate-500 font-mono">
545
+ <span className="text-emerald-700 font-semibold">⚡ {queryLatency}ms</span>
546
+ <span>•</span>
547
+ <span>{documents.length} documents retrieved</span>
548
+ </div>
549
+ )}
550
+ </div>
551
+
552
+ {/* Error Banner */}
553
+ {error && (
554
+ <div className="px-4 py-2 bg-red-50 border-b border-red-200 text-red-700 text-xs flex items-center justify-between">
555
+ <span>{error}</span>
556
+ <button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">✕</button>
557
+ </div>
558
+ )}
559
+
560
+ {/* Content Area: Table / JSON List with Drawer */}
561
+ <div className="flex-1 flex overflow-hidden">
562
+ <div className="flex-1 overflow-auto bg-white">
563
+ {loading ? (
564
+ <div className="p-8 text-center text-slate-400 text-xs font-mono">Loading documents...</div>
565
+ ) : documents.length === 0 ? (
566
+ <div className="p-12 text-center text-slate-400 text-xs">
567
+ <p className="font-semibold text-slate-600 mb-1">No matching documents</p>
568
+ <p>Try clearing your filter or inserting a new document.</p>
569
+ </div>
570
+ ) : (
571
+ /* Card-based Document View */
572
+ <div className="p-4">
573
+ <AnimatePresence>
574
+ {documents.map((doc, idx) => (
575
+ <DocumentCard
576
+ key={doc._id || idx}
577
+ doc={doc}
578
+ idx={idx}
579
+ isSelected={inspectorDoc?._id === doc._id}
580
+ onInspect={(d) => setInspectorDoc(d)}
581
+ onEdit={(d) => {
582
+ setSelectedDoc(d);
583
+ setShowUpdateModal(true);
584
+ }}
585
+ onDelete={(d) => {
586
+ setSelectedDoc(d);
587
+ setShowDeleteModal(true);
588
+ }}
589
+ copyToClipboard={copyToClipboard}
590
+ />
591
+ ))}
592
+ </AnimatePresence>
593
+ </div>
594
+ )}
595
+ </div>
596
+
597
+ {/* Side Document Inspector Drawer */}
598
+ {inspectorDoc && (
599
+ <div className="w-96 border-l border-slate-200 bg-white flex flex-col justify-between shrink-0 shadow-lg animate-slideIn">
600
+ <div className="p-3.5 border-b border-slate-200 flex items-center justify-between bg-slate-50">
601
+ <div className="min-w-0">
602
+ <h3 className="text-xs font-bold text-slate-800">Document Inspector</h3>
603
+ <p className="text-[10px] font-mono text-slate-400 truncate">ID: {inspectorDoc._id || inspectorDoc.documentId}</p>
604
+ </div>
605
+ <button
606
+ onClick={() => setInspectorDoc(null)}
607
+ className="p-1 rounded-md text-slate-400 hover:text-slate-700 hover:bg-slate-200 cursor-pointer"
608
+ >
609
+
610
+ </button>
611
+ </div>
612
+
613
+ <div className="flex-1 overflow-auto p-3 bg-slate-50">
614
+ <ObjectView value={(() => {
615
+ if (!inspectorDoc || typeof inspectorDoc !== "object") return inspectorDoc;
616
+ const { _id, documentId, updatedAt, ...rest } = inspectorDoc;
617
+ return rest;
618
+ })()} maxHeight={600} />
619
+ </div>
620
+
621
+ <div className="p-3 border-t border-slate-200 bg-white flex items-center justify-between">
622
+ <button
623
+ onClick={() => copyToClipboard(inspectorDoc)}
624
+ className="px-3 py-1.5 rounded-md text-xs font-medium text-slate-700 bg-slate-100 hover:bg-slate-200 transition-colors cursor-pointer"
625
+ >
626
+ Copy JSON
627
+ </button>
628
+ <button
629
+ onClick={() => {
630
+ setSelectedDoc(inspectorDoc);
631
+ setShowUpdateModal(true);
632
+ }}
633
+ className="px-3 py-1.5 rounded-md text-xs font-semibold text-white bg-emerald-600 hover:bg-emerald-700 transition-colors cursor-pointer"
634
+ >
635
+ Edit Document
636
+ </button>
637
+ </div>
638
+ </div>
639
+ )}
640
+ </div>
641
+
642
+ {/* Desktop Pagination Bar */}
643
+ <div className="h-9 border-t border-slate-200 bg-white px-4 flex items-center justify-between shrink-0 text-xs text-slate-600 font-mono">
644
+ <div>
645
+ Showing {documents.length > 0 ? (page - 1) * limit + 1 : 0} - {Math.min(page * limit, totalDocs)} of {totalDocs}
646
+ </div>
647
+
648
+ <div className="flex items-center gap-2">
649
+ <button
650
+ disabled={page <= 1}
651
+ onClick={() => setPage((p) => Math.max(1, p - 1))}
652
+ className="px-2 py-0.5 rounded border border-slate-200 disabled:opacity-40 hover:bg-slate-100 font-sans cursor-pointer"
653
+ >
654
+ Previous
655
+ </button>
656
+ <span>Page {page}</span>
657
+ <button
658
+ disabled={page * limit >= totalDocs}
659
+ onClick={() => setPage((p) => p + 1)}
660
+ className="px-2 py-0.5 rounded border border-slate-200 disabled:opacity-40 hover:bg-slate-100 font-sans cursor-pointer"
661
+ >
662
+ Next
663
+ </button>
664
+ </div>
665
+ </div>
666
+ </div>
667
+ )}
668
+
669
+ {/* ==========================================
670
+ TAB 2: QUERY CONSOLE
671
+ ========================================== */}
672
+ {activeTab === "query" && (
673
+ <div className="flex-1 flex flex-col overflow-hidden p-4 bg-slate-50">
674
+ <div className="mb-3 flex items-center justify-between">
675
+ <div>
676
+ <h2 className="text-sm font-bold text-slate-800">Interactive Query Console</h2>
677
+ <p className="text-xs text-slate-500">
678
+ Run JavaScript queries or aggregations against <span className="font-mono font-semibold text-emerald-700">{selectedCollection}</span>.
679
+ </p>
680
+ </div>
681
+ <button
682
+ onClick={handleRunConsoleQuery}
683
+ disabled={consoleRunning}
684
+ className="px-4 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-700 disabled:bg-slate-300 text-white text-xs font-semibold flex items-center gap-1.5 shadow-xs transition-colors cursor-pointer"
685
+ >
686
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
687
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
688
+ </svg>
689
+ {consoleRunning ? "Running..." : "Run Query (Ctrl+Enter)"}
690
+ </button>
691
+ </div>
692
+
693
+ {/* Quick Example Query Pills */}
694
+ <div className="flex items-center gap-1.5 flex-wrap mb-3">
695
+ <span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider mr-1">Examples:</span>
696
+ {queryExamples.map((ex) => (
697
+ <button
698
+ key={ex.label}
699
+ type="button"
700
+ onClick={() => setConsoleQuery(ex.code)}
701
+ title={ex.code}
702
+ className="px-2.5 py-0.5 rounded-full border border-slate-200 bg-white hover:border-emerald-300 hover:bg-emerald-50 hover:text-emerald-700 text-slate-600 text-[11px] font-medium transition-colors cursor-pointer"
703
+ >
704
+ {ex.label}
705
+ </button>
706
+ ))}
707
+ </div>
708
+
709
+ {/* QueryEditor with full autocomplete & diagnostics */}
710
+ <div className="mb-2">
711
+ <QueryEditor
712
+ value={consoleQuery}
713
+ onChange={setConsoleQuery}
714
+ collectionName={selectedCollection}
715
+ fields={collectionFields}
716
+ diagnostics={consoleDiagnostics}
717
+ onSubmit={handleRunConsoleQuery}
718
+ minHeight={150}
719
+ maxHeight={220}
720
+ />
721
+ </div>
722
+
723
+ {/* Keyboard hints */}
724
+ <div className="flex items-center gap-2 text-xs text-slate-500 mb-3 font-sans">
725
+ <kbd className="px-1.5 py-0.5 rounded border border-slate-200 bg-white font-mono text-[11px]">Ctrl</kbd>
726
+ <span>+</span>
727
+ <kbd className="px-1.5 py-0.5 rounded border border-slate-200 bg-white font-mono text-[11px]">Space</kbd>
728
+ <span>suggestions</span>
729
+ <span className="mx-1">•</span>
730
+ <kbd className="px-1.5 py-0.5 rounded border border-slate-200 bg-white font-mono text-[11px]">Ctrl</kbd>
731
+ <span>+</span>
732
+ <kbd className="px-1.5 py-0.5 rounded border border-slate-200 bg-white font-mono text-[11px]">Enter</kbd>
733
+ <span>run query</span>
734
+ </div>
735
+
736
+ {/* Query Results */}
737
+ <div className="flex-1 flex flex-col min-h-0 bg-white rounded-xl border border-slate-200 shadow-2xs overflow-hidden">
738
+ <div className="px-4 py-2 bg-slate-100 border-b border-slate-200 flex items-center justify-between text-xs text-slate-700 font-medium">
739
+ <span>Query Results</span>
740
+ {consoleResults && (
741
+ <span className="font-mono text-emerald-700 font-semibold">
742
+ ⚡ {consoleResults.executionTime} • {consoleResults.count} matches
743
+ </span>
744
+ )}
745
+ </div>
746
+
747
+ <div className="flex-1 overflow-auto p-3 bg-slate-50">
748
+ {consoleError ? (
749
+ <div className="p-3 bg-red-50 text-red-700 rounded-lg text-xs font-mono">{consoleError}</div>
750
+ ) : consoleResults ? (
751
+ <ObjectView value={consoleResults.data} maxHeight={500} />
752
+ ) : (
753
+ <div className="text-center py-12 text-slate-400 text-xs font-mono">
754
+ Write a query above and press "Run Query" (Ctrl+Enter)
755
+ </div>
756
+ )}
757
+ </div>
758
+ </div>
759
+ </div>
760
+ )}
761
+
762
+ {/* Modals with autocomplete field suggestions */}
763
+ <InsertDocumentModal
764
+ isOpen={showInsertModal}
765
+ databaseName={selectedDatabase}
766
+ collectionName={selectedCollection}
767
+ fields={collectionFields}
768
+ onClose={() => setShowInsertModal(false)}
769
+ onDocumentInserted={() => {
770
+ setShowInsertModal(false);
771
+ loadDocuments();
772
+ }}
773
+ />
774
+
775
+ <UpdateDocumentModal
776
+ isOpen={showUpdateModal}
777
+ databaseName={selectedDatabase}
778
+ collectionName={selectedCollection}
779
+ document={selectedDoc}
780
+ fields={collectionFields}
781
+ onClose={() => {
782
+ setShowUpdateModal(false);
783
+ setSelectedDoc(null);
784
+ }}
785
+ onDocumentUpdated={() => {
786
+ setShowUpdateModal(false);
787
+ setSelectedDoc(null);
788
+ loadDocuments();
789
+ }}
790
+ />
791
+
792
+ <DeleteDocumentModal
793
+ isOpen={showDeleteModal}
794
+ databaseName={selectedDatabase}
795
+ collectionName={selectedCollection}
796
+ documentId={selectedDoc?._id || selectedDoc?.documentId}
797
+ onClose={() => {
798
+ setShowDeleteModal(false);
799
+ setSelectedDoc(null);
800
+ }}
801
+ onDocumentDeleted={() => {
802
+ setShowDeleteModal(false);
803
+ setSelectedDoc(null);
804
+ loadDocuments();
805
+ }}
806
+ />
807
+
808
+ <DeleteCollectionModal
809
+ isOpen={!!collToDelete}
810
+ databaseName={selectedDatabase}
811
+ collectionName={collToDelete}
812
+ onClose={() => setCollToDelete(null)}
813
+ onCollectionDeleted={(deletedName) => {
814
+ setCollToDelete(null);
815
+ removeCollectionLocally(selectedDatabase, deletedName);
816
+ setSelectedCollection(selectedDatabase, "");
817
+ }}
818
+ />
819
+ </div>
820
+ );
821
+ };
822
+
823
+ export default Documents;