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,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;
|