Select a collection to view documents
546 |No documents selected
570 |Select a collection from the sidebar to view its documents
571 |├── .gitignore ├── README.md ├── screenshots └── v1.jpg ├── package.json ├── main.js ├── renderer.js └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DB Visualiser 2 | 3 | ## v1 4 |  -------------------------------------------------------------------------------- /screenshots/v1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xditya/database-visualiser/main/screenshots/v1.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-test", 3 | "version": "1.0.0", 4 | "description": "MongoDB Visualizer", 5 | "main": "main.js", 6 | "scripts": { 7 | "start": "electron .", 8 | "dev": "electron . --debug" 9 | }, 10 | "author": "xditya", 11 | "license": "ISC", 12 | "dependencies": { 13 | "electron": "^35.1.4", 14 | "mongodb": "^6.3.0", 15 | "chart.js": "^4.4.1", 16 | "bootstrap": "^5.3.2", 17 | "jquery": "^3.7.1", 18 | "popper.js": "^1.16.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, ipcMain } = require("electron"); 2 | const path = require("path"); 3 | const { MongoClient } = require("mongodb"); 4 | 5 | let mainWindow; 6 | let mongoClient = null; 7 | 8 | const createWindow = () => { 9 | mainWindow = new BrowserWindow({ 10 | width: 1200, 11 | height: 800, 12 | webPreferences: { 13 | nodeIntegration: true, 14 | contextIsolation: false, 15 | }, 16 | backgroundColor: "#1a1a1a", 17 | }); 18 | 19 | mainWindow.loadFile("index.html"); 20 | // mainWindow.webContents.openDevTools(); // Uncomment for debugging 21 | 22 | // Handle window controls 23 | ipcMain.on("minimize", () => { 24 | mainWindow.minimize(); 25 | }); 26 | 27 | ipcMain.on("maximize", () => { 28 | if (mainWindow.isMaximized()) { 29 | mainWindow.unmaximize(); 30 | } else { 31 | mainWindow.maximize(); 32 | } 33 | }); 34 | 35 | ipcMain.on("close", () => { 36 | mainWindow.close(); 37 | }); 38 | 39 | ipcMain.on("startDrag", () => { 40 | mainWindow.webContents.send("maximizeChange", mainWindow.isMaximized()); 41 | }); 42 | 43 | // Send maximize state changes to renderer 44 | mainWindow.on("maximize", () => { 45 | mainWindow.webContents.send("maximizeChange", true); 46 | }); 47 | 48 | mainWindow.on("unmaximize", () => { 49 | mainWindow.webContents.send("maximizeChange", false); 50 | }); 51 | }; 52 | 53 | app.whenReady().then(() => { 54 | createWindow(); 55 | 56 | app.on("activate", () => { 57 | if (BrowserWindow.getAllWindows().length === 0) { 58 | createWindow(); 59 | } 60 | }); 61 | }); 62 | 63 | app.on("window-all-closed", () => { 64 | if (process.platform !== "darwin") { 65 | app.quit(); 66 | } 67 | }); 68 | 69 | // MongoDB Connection Handlers 70 | ipcMain.handle("connect-to-mongodb", async (event, url) => { 71 | try { 72 | if (mongoClient) { 73 | await mongoClient.close(); 74 | } 75 | 76 | mongoClient = new MongoClient(url); 77 | await mongoClient.connect(); 78 | 79 | const databases = await mongoClient.db().admin().listDatabases(); 80 | return { success: true, databases: databases.databases }; 81 | } catch (error) { 82 | return { success: false, error: error.message }; 83 | } 84 | }); 85 | 86 | ipcMain.handle("get-collections", async (event, dbName) => { 87 | try { 88 | const db = mongoClient.db(dbName); 89 | const collections = await db.listCollections().toArray(); 90 | return { success: true, collections }; 91 | } catch (error) { 92 | return { success: false, error: error.message }; 93 | } 94 | }); 95 | 96 | ipcMain.handle( 97 | "get-documents", 98 | async (event, { dbName, collectionName, limit = 100 }) => { 99 | try { 100 | const db = mongoClient.db(dbName); 101 | const collection = db.collection(collectionName); 102 | const documents = await collection.find({}).limit(limit).toArray(); 103 | return { success: true, documents }; 104 | } catch (error) { 105 | return { success: false, error: error.message }; 106 | } 107 | } 108 | ); 109 | 110 | ipcMain.handle("disconnect-mongodb", async () => { 111 | try { 112 | if (mongoClient) { 113 | await mongoClient.close(); 114 | mongoClient = null; 115 | } 116 | return { success: true }; 117 | } catch (error) { 118 | return { success: false, error: error.message }; 119 | } 120 | }); 121 | -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | const { ipcRenderer } = require("electron"); 2 | 3 | // DOM Elements 4 | const connectBtn = document.getElementById("connectBtn"); 5 | const connectionUrl = document.getElementById("connectionUrl"); 6 | const databaseList = document.getElementById("databaseList"); 7 | const documentViewer = document.getElementById("documentViewer"); 8 | const loadingSpinner = document.getElementById("loadingSpinner"); 9 | const connectionStatus = document.getElementById("connectionStatus"); 10 | const documentCount = document.getElementById("documentCount"); 11 | const sidebar = document.getElementById("sidebar"); 12 | const sidebarToggle = document.getElementById("sidebarToggle"); 13 | const mainContent = document.getElementById("mainContent"); 14 | const sidebarOverlay = document.getElementById("sidebarOverlay"); 15 | 16 | // State 17 | let currentDb = null; 18 | let currentCollection = null; 19 | let isConnected = false; 20 | let isSidebarCollapsed = false; 21 | 22 | // Document Viewer State 23 | let currentView = "table"; 24 | let currentPage = 1; 25 | let documentsPerPage = 10; 26 | let filteredDocuments = []; 27 | 28 | // DOM Elements for Document Viewer 29 | const searchInput = document.getElementById("searchInput"); 30 | const documentContent = document.getElementById("documentContent"); 31 | const viewOptionButtons = document.querySelectorAll(".view-option-btn"); 32 | 33 | // Title bar controls 34 | const minimizeBtn = document.querySelector(".titlebar-button.minimize"); 35 | const maximizeBtn = document.querySelector(".titlebar-button.maximize"); 36 | const closeBtn = document.querySelector(".titlebar-button.close"); 37 | 38 | // Event Listeners 39 | connectBtn.addEventListener("click", handleConnect); 40 | connectionUrl.addEventListener("keypress", (e) => { 41 | if (e.key === "Enter") { 42 | handleConnect(); 43 | } 44 | }); 45 | 46 | // Sidebar Toggle 47 | sidebarToggle.addEventListener("click", toggleSidebar); 48 | 49 | // Click outside handler 50 | document.addEventListener("click", (e) => { 51 | const isClickInsideSidebar = sidebar.contains(e.target); 52 | const isClickOnToggle = sidebarToggle.contains(e.target); 53 | 54 | if (!isClickInsideSidebar && !isClickOnToggle && !isSidebarCollapsed) { 55 | toggleSidebar(); 56 | } 57 | }); 58 | 59 | // Handle window resize 60 | window.addEventListener("resize", () => { 61 | if (window.innerWidth >= 992) { 62 | sidebarOverlay.classList.remove("visible"); 63 | } 64 | }); 65 | 66 | // Event Listeners for Document Viewer 67 | searchInput.addEventListener("input", handleSearch); 68 | viewOptionButtons.forEach((btn) => { 69 | btn.addEventListener("click", () => { 70 | const view = btn.dataset.view; 71 | setViewMode(view); 72 | }); 73 | }); 74 | 75 | // Add title bar functionality 76 | minimizeBtn.addEventListener("click", () => { 77 | window.electron.minimize(); 78 | }); 79 | 80 | maximizeBtn.addEventListener("click", () => { 81 | window.electron.maximize(); 82 | }); 83 | 84 | closeBtn.addEventListener("click", () => { 85 | window.electron.close(); 86 | }); 87 | 88 | // Update maximize button icon based on window state 89 | window.electron.onMaximizeChange((isMaximized) => { 90 | maximizeBtn.innerHTML = isMaximized 91 | ? '' 92 | : ''; 93 | }); 94 | 95 | // Make title bar draggable 96 | const titlebar = document.querySelector(".titlebar"); 97 | titlebar.addEventListener("mousedown", (e) => { 98 | if (e.target.closest(".titlebar-button")) return; 99 | window.electron.startDrag(); 100 | }); 101 | 102 | function toggleSidebar() { 103 | isSidebarCollapsed = !isSidebarCollapsed; 104 | sidebar.classList.toggle("collapsed", isSidebarCollapsed); 105 | mainContent.classList.toggle("expanded", isSidebarCollapsed); 106 | 107 | // Update toggle button icon 108 | const icon = sidebarToggle.querySelector("i"); 109 | icon.classList.toggle("bi-chevron-left", !isSidebarCollapsed); 110 | icon.classList.toggle("bi-chevron-right", isSidebarCollapsed); 111 | 112 | // Show/hide overlay on mobile 113 | if (window.innerWidth < 992) { 114 | sidebarOverlay.classList.toggle("visible", !isSidebarCollapsed); 115 | } 116 | } 117 | 118 | // Functions 119 | async function handleConnect() { 120 | const url = connectionUrl.value.trim(); 121 | if (!url) { 122 | showError("Please enter a MongoDB URL"); 123 | return; 124 | } 125 | 126 | showLoading(); 127 | try { 128 | const result = await ipcRenderer.invoke("connect-to-mongodb", url); 129 | if (result.success) { 130 | updateDatabaseList(result.databases); 131 | updateConnectionStatus(true); 132 | connectBtn.innerHTML = 'Connected'; 133 | connectBtn.classList.add("disabled"); 134 | connectionUrl.disabled = true; 135 | } else { 136 | showError(result.error); 137 | updateConnectionStatus(false); 138 | } 139 | } catch (error) { 140 | showError(error.message); 141 | updateConnectionStatus(false); 142 | } finally { 143 | hideLoading(); 144 | } 145 | } 146 | 147 | function updateConnectionStatus(connected) { 148 | isConnected = connected; 149 | connectionStatus.classList.toggle("disconnected", !connected); 150 | const statusText = connectionStatus.nextElementSibling; 151 | statusText.textContent = connected ? "Connected" : "Disconnected"; 152 | } 153 | 154 | function updateDatabaseList(databases) { 155 | databaseList.innerHTML = ""; 156 | databases.forEach((db) => { 157 | if (db.name !== "admin" && db.name !== "local") { 158 | const dbElement = createDatabaseElement(db.name); 159 | databaseList.appendChild(dbElement); 160 | } 161 | }); 162 | } 163 | 164 | function createDatabaseElement(dbName) { 165 | const dbDiv = document.createElement("div"); 166 | dbDiv.className = "mb-3"; 167 | 168 | const dbHeader = document.createElement("div"); 169 | dbHeader.className = "database-header d-flex align-items-center"; 170 | dbHeader.innerHTML = ` 171 | 172 | ${dbName} 173 | 174 | `; 175 | 176 | const collectionsDiv = document.createElement("div"); 177 | collectionsDiv.className = "ms-4 collections-container"; 178 | collectionsDiv.style.display = "none"; 179 | 180 | dbHeader.addEventListener("click", async () => { 181 | const isExpanded = collectionsDiv.style.display !== "none"; 182 | collectionsDiv.style.display = isExpanded ? "none" : "block"; 183 | const chevron = dbHeader.querySelector(".bi-chevron-down"); 184 | chevron.style.transform = isExpanded ? "rotate(0deg)" : "rotate(180deg)"; 185 | 186 | if (!isExpanded && !collectionsDiv.hasChildNodes()) { 187 | showLoading(); 188 | try { 189 | const result = await ipcRenderer.invoke("get-collections", dbName); 190 | if (result.success) { 191 | result.collections.forEach((collection) => { 192 | const collectionElement = createCollectionElement( 193 | dbName, 194 | collection.name 195 | ); 196 | collectionsDiv.appendChild(collectionElement); 197 | }); 198 | } else { 199 | showError(result.error); 200 | } 201 | } catch (error) { 202 | showError(error.message); 203 | } finally { 204 | hideLoading(); 205 | } 206 | } 207 | }); 208 | 209 | dbDiv.appendChild(dbHeader); 210 | dbDiv.appendChild(collectionsDiv); 211 | return dbDiv; 212 | } 213 | 214 | function createCollectionElement(dbName, collectionName) { 215 | const collectionDiv = document.createElement("div"); 216 | collectionDiv.className = "collection-item"; 217 | collectionDiv.innerHTML = `${collectionName}`; 218 | 219 | collectionDiv.addEventListener("click", async () => { 220 | currentDb = dbName; 221 | currentCollection = collectionName; 222 | currentPage = 1; 223 | showLoading(); 224 | try { 225 | const result = await ipcRenderer.invoke("get-documents", { 226 | dbName, 227 | collectionName, 228 | limit: 100, 229 | }); 230 | 231 | if (result.success) { 232 | filteredDocuments = result.documents; 233 | displayDocuments(filteredDocuments); 234 | updateDocumentCount(result.documents.length); 235 | searchInput.value = ""; 236 | } else { 237 | showError(result.error); 238 | updateDocumentCount(0); 239 | } 240 | } catch (error) { 241 | showError(error.message); 242 | updateDocumentCount(0); 243 | } finally { 244 | hideLoading(); 245 | } 246 | }); 247 | 248 | return collectionDiv; 249 | } 250 | 251 | function displayDocuments(documents) { 252 | if (!documents || documents.length === 0) { 253 | documentContent.innerHTML = ` 254 |
No documents match your search criteria
258 || ${header} | `).join("")} 283 |
|---|
| ${formatValue(doc[header])} | 294 | ` 295 | ) 296 | .join("")} 297 |
${JSON.stringify(doc, null, 2)}
326 | ${JSON.stringify(value, null, 2)}`;
396 | }
397 | return value;
398 | }
399 |
400 | function updateDocumentCount(count) {
401 | documentCount.textContent =
402 | count > 0
403 | ? `${count} document${count === 1 ? "" : "s"} selected`
404 | : "No documents selected";
405 | }
406 |
407 | function showLoading() {
408 | loadingSpinner.style.display = "block";
409 | documentContent.style.display = "none";
410 | }
411 |
412 | function hideLoading() {
413 | loadingSpinner.style.display = "none";
414 | documentContent.style.display = "block";
415 | }
416 |
417 | function showError(message) {
418 | const alertDiv = document.createElement("div");
419 | alertDiv.className = "alert alert-danger";
420 | alertDiv.innerHTML = `
421 | Select a collection from the sidebar to view its documents
571 |