From 61c40178f64979df24311803adf68ac7c9210f01 Mon Sep 17 00:00:00 2001 From: ByteExceptionM Date: Sun, 10 May 2026 19:29:16 +0200 Subject: [PATCH 1/5] fix: strip mongod binary path from dashboard process field Some launchers (mongodb-memory-server, custom installs) spawn mongod with a full path as argv[0], which then leaks back through serverStatus.process and into the dashboard subtitle. Reduce to the basename so we show "mongod" rather than a filesystem path. --- src/main/services/DatabaseService.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/services/DatabaseService.ts b/src/main/services/DatabaseService.ts index 35e26dd..87ba5f6 100644 --- a/src/main/services/DatabaseService.ts +++ b/src/main/services/DatabaseService.ts @@ -211,7 +211,11 @@ export class DatabaseService { host: stringOr(status['host'], 'unknown'), version: stringOr(status['version'], 'unknown'), uptimeSeconds: numberOr(status['uptime'], 0), - process: stringOr(status['process'], 'unknown'), + // Some launchers (mongodb-memory-server, custom installs) spawn + // mongod with a full path as argv[0], which then leaks back through + // `serverStatus.process`. Reduce to the basename so the dashboard + // shows `mongod` / `mongos` rather than a filesystem path. + process: basename(stringOr(status['process'], 'unknown')), ...(storageEngine !== undefined ? { storageEngine } : {}), connections: { current: numberOr(conn['current'], 0), @@ -326,6 +330,11 @@ function stringOr(value: unknown, fallback: string): string { return typeof value === 'string' ? value : fallback } +function basename(path: string): string { + const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + return idx >= 0 ? path.slice(idx + 1) : path +} + function assertCollectionName(name: string): void { if (!COLLECTION_NAME_RE.test(name)) { const e = new Error('Collection name must not contain $ or null bytes') From f2974c9272ecae674055d443fbe8f2f784f611ea Mon Sep 17 00:00:00 2001 From: ByteExceptionM Date: Sun, 10 May 2026 19:29:44 +0200 Subject: [PATCH 2/5] feat: denser connection dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose more of the serverStatus payload and split it across the three tabs so a single glance covers more ground. Overview gains a compact 8-tile KPI strip (ops/s, net/s, connections, cache hit, resident memory, document totals, storage, uptime) plus a Server card listing host, version, process, storage engine, database count, total + largest, and a Workload-mix card with a stacked bar showing the per-op-type share of the most recent sample. Operations / second tile grid grows from 4 to 6 — getmore and command were previously invisible — and each tile now carries a cumulative total beneath the live rate. Health tab adds Database breakdown (counts, total, average size, largest, plus per-database bars) and Operation counters (cumulative since uptime with proportional bars). Asserts gains the rollovers field that mongod also exposes. --- .../dashboard/ConnectionDashboard.tsx | 386 +++++++++++++++++- 1 file changed, 374 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/features/dashboard/ConnectionDashboard.tsx b/src/renderer/src/features/dashboard/ConnectionDashboard.tsx index bb6d38c..19a1652 100644 --- a/src/renderer/src/features/dashboard/ConnectionDashboard.tsx +++ b/src/renderer/src/features/dashboard/ConnectionDashboard.tsx @@ -5,7 +5,9 @@ import { ArrowDownToLine, ArrowUpFromLine, Boxes, + Clock, Cpu, + Database, FileEdit, FileMinus2, FilePlus2, @@ -13,6 +15,7 @@ import { Gauge, HardDrive, Heart, + Info, Layers, Loader2, MousePointer, @@ -37,12 +40,14 @@ import type { ConnectionConfig, ServerStats } from '@shared/types' type Series = { /** Total ops/sec across all opcounters. */ opsPerSec: ChartPoint[] - /** Ops/sec broken out per opcounter family. */ + /** Ops/sec broken out per opcounter family (all six). */ opsPerSecByKind: { query: ChartPoint[] insert: ChartPoint[] update: ChartPoint[] delete: ChartPoint[] + getmore: ChartPoint[] + command: ChartPoint[] } /** Active connections over time. */ connections: ChartPoint[] @@ -208,12 +213,21 @@ function Overview({ stats, series }: { stats: ServerStats; series: Series }) { const lastOpsPerSec = series.opsPerSec.at(-1)?.value ?? 0 return (
- }> + + + }> - }> + }> + + + + }> + }> + + 0 ? (conn.current / connTotal) * 100 : 0 + const hitRate = + stats.cache && stats.cache.pagesRequested > 0 + ? (1 - stats.cache.pagesRead / stats.cache.pagesRequested) * 100 + : null + const cachePct = + stats.cache && stats.cache.maxBytesConfigured > 0 + ? (stats.cache.bytesInCache / stats.cache.maxBytesConfigured) * 100 + : null + const totalDocs = + stats.documents.inserted + + stats.documents.returned + + stats.documents.updated + + stats.documents.deleted + + const tiles: Array<{ + label: string + icon: React.ReactNode + value: string + detail: string + series?: ChartPoint[] + color?: string + }> = [ + { + label: 'ops / sec', + icon: , + value: opsPs < 10 ? opsPs.toFixed(1) : opsPs.toFixed(0), + detail: `${formatNumber(stats.network.numRequests)} cumulative`, + series: series.opsPerSec, + color: 'hsl(var(--primary))' + }, + { + label: 'net / sec', + icon: , + value: formatBytesShort(netPs), + detail: `${formatBytes(stats.network.bytesIn + stats.network.bytesOut)} total`, + series: series.bytesPerSec, + color: 'hsl(var(--success))' + }, + { + label: 'connections', + icon: , + value: formatNumber(conn.current), + detail: `${connPct.toFixed(0)}% of ${formatNumber(connTotal)}`, + series: series.connections, + color: '#fb923c' + }, + { + label: 'cache hit', + icon: , + value: hitRate !== null ? `${hitRate.toFixed(2)}%` : '—', + detail: cachePct !== null ? `fill ${cachePct.toFixed(0)}%` : 'no cache stats', + series: series.cacheFillPct, + color: '#a78bfa' + }, + { + label: 'resident', + icon: , + value: `${formatNumber(stats.mem.residentMb)} MB`, + detail: `${formatNumber(stats.mem.virtualMb)} MB virtual`, + series: series.residentMb, + color: '#f472b6' + }, + { + label: 'documents', + icon: , + value: formatCompact(totalDocs), + detail: `${formatCompact(stats.documents.returned)} read · ${formatCompact( + stats.documents.inserted + stats.documents.updated + stats.documents.deleted + )} written`, + color: '#facc15' + }, + { + label: 'storage', + icon: , + value: formatBytes(stats.totalSizeOnDisk), + detail: `${stats.databases.length} dbs`, + color: '#60a5fa' + }, + { + label: 'uptime', + icon: , + value: formatUptime(stats.uptimeSeconds), + detail: `MongoDB ${stats.version}`, + color: '#34d399' + } + ] + + return ( +
+ {tiles.map((t) => ( +
+
+ + {t.icon} + + {t.label} +
+
+ {t.value} +
+
+ {t.detail} +
+ {t.series && t.series.length > 1 && ( + p.value)} + color={t.color ?? 'hsl(var(--primary))'} + height={18} + fillOpacity={0.18} + /> + )} +
+ ))} +
+ ) +} + +function ServerInfoBlock({ stats }: { stats: ServerStats }) { + const totalSize = stats.totalSizeOnDisk + const emptyDbs = stats.databases.filter((d) => d.empty).length + const biggest = + stats.databases.length > 0 + ? [...stats.databases].sort((a, b) => b.sizeOnDisk - a.sizeOnDisk)[0] + : null + const rows: Array<[string, string]> = [ + ['Host', stats.host], + ['Version', `MongoDB ${stats.version}`], + ['Process', stats.process], + ['Storage engine', stats.storageEngine ?? '—'], + ['Uptime', formatUptime(stats.uptimeSeconds)], + ['Databases', `${stats.databases.length}${emptyDbs > 0 ? ` (${emptyDbs} empty)` : ''}`], + ['Total storage', formatBytes(totalSize)], + ['Largest database', biggest ? `${biggest.name} · ${formatBytes(biggest.sizeOnDisk)}` : '—'] + ] + return ( +
    + {rows.map(([k, v]) => ( +
  • + {k} + {v} +
  • + ))} +
+ ) +} + +function WorkloadMixBlock({ series }: { series: Series }) { + const samples = [ + ['query', series.opsPerSecByKind.query.at(-1)?.value ?? 0, 'hsl(var(--primary))'], + ['insert', series.opsPerSecByKind.insert.at(-1)?.value ?? 0, 'hsl(var(--success))'], + ['update', series.opsPerSecByKind.update.at(-1)?.value ?? 0, '#fb923c'], + ['delete', series.opsPerSecByKind.delete.at(-1)?.value ?? 0, '#f472b6'], + ['getmore', series.opsPerSecByKind.getmore.at(-1)?.value ?? 0, '#60a5fa'], + ['command', series.opsPerSecByKind.command.at(-1)?.value ?? 0, '#a78bfa'] + ] as const + const total = samples.reduce((s, [, v]) => s + v, 0) + return ( +
+
+ Last sample + + {total.toFixed(total < 10 ? 1 : 0)}{' '} + ops/s + +
+
+ {samples.map(([label, v, color]) => { + const pct = total > 0 ? (v / total) * 100 : 0 + if (pct === 0) return null + return ( +
+ ) + })} +
+
    + {samples.map(([label, v, color]) => { + const pct = total > 0 ? (v / total) * 100 : 0 + return ( +
  • + + {label} + {v.toFixed(v < 10 ? 1 : 0)} + + {pct.toFixed(0)}% + +
  • + ) + })} +
+
+ ) +} + function Performance({ stats, series }: { stats: ServerStats; series: Series }) { return (
@@ -287,6 +509,117 @@ function Health({ stats, series }: { stats: ServerStats; series: Series }) { }> + }> + + + }> + + +
+ ) +} + +function DatabaseBreakdownBlock({ stats }: { stats: ServerStats }) { + const sorted = [...stats.databases].sort((a, b) => b.sizeOnDisk - a.sizeOnDisk) + const total = stats.totalSizeOnDisk + const nonEmpty = stats.databases.filter((d) => !d.empty).length + const avgSize = nonEmpty > 0 ? total / nonEmpty : 0 + return ( +
+
+
+
Databases
+
+ {formatNumber(stats.databases.length)} +
+
+ {stats.databases.filter((d) => d.empty).length} empty +
+
+
+
Total size
+
{formatBytes(total)}
+
avg {formatBytes(avgSize)}
+
+
+
Largest
+
+ {sorted[0] ? formatBytes(sorted[0].sizeOnDisk) : '—'} +
+
{sorted[0]?.name ?? '—'}
+
+
+
    + {sorted.map((d) => { + const pct = total > 0 ? (d.sizeOnDisk / total) * 100 : 0 + return ( +
  • + {d.name} +
    +
    +
    + + {formatBytes(d.sizeOnDisk)} + + + {pct.toFixed(1)}% + +
  • + ) + })} +
+
+ ) +} + +function OperationCountersBlock({ stats }: { stats: ServerStats }) { + const rows: Array<[string, number, string]> = [ + ['query', stats.opcounters.query, 'hsl(var(--primary))'], + ['insert', stats.opcounters.insert, 'hsl(var(--success))'], + ['update', stats.opcounters.update, '#fb923c'], + ['delete', stats.opcounters.delete, '#f472b6'], + ['getmore', stats.opcounters.getmore, '#60a5fa'], + ['command', stats.opcounters.command, '#a78bfa'] + ] + const total = rows.reduce((s, [, v]) => s + v, 0) + return ( +
+
+ + Cumulative since uptime + + {formatNumber(total)} +
+
    + {rows.map(([label, v, color]) => { + const pct = total > 0 ? (v / total) * 100 : 0 + return ( +
  • + + {label} +
    +
    +
    + {formatNumber(v)} + + {pct.toFixed(1)}% + +
  • + ) + })} +
) } @@ -390,11 +723,13 @@ function OpsRateBlock({ stats, series }: { stats: ServerStats; series: Series }) stats.opcounters.getmore + stats.opcounters.command - const tiles: Array<[string, ChartPoint[], string]> = [ - ['query', series.opsPerSecByKind.query, 'hsl(var(--primary))'], - ['insert', series.opsPerSecByKind.insert, 'hsl(var(--success))'], - ['update', series.opsPerSecByKind.update, '#fb923c'], - ['delete', series.opsPerSecByKind.delete, '#f472b6'] + const tiles: Array<[string, ChartPoint[], string, number]> = [ + ['query', series.opsPerSecByKind.query, 'hsl(var(--primary))', stats.opcounters.query], + ['insert', series.opsPerSecByKind.insert, 'hsl(var(--success))', stats.opcounters.insert], + ['update', series.opsPerSecByKind.update, '#fb923c', stats.opcounters.update], + ['delete', series.opsPerSecByKind.delete, '#f472b6', stats.opcounters.delete], + ['getmore', series.opsPerSecByKind.getmore, '#60a5fa', stats.opcounters.getmore], + ['command', series.opsPerSecByKind.command, '#a78bfa', stats.opcounters.command] ] return ( @@ -407,8 +742,8 @@ function OpsRateBlock({ stats, series }: { stats: ServerStats; series: Series }) formatY={(v) => v.toFixed(v < 10 ? 1 : 0)} yMin={0} /> -
- {tiles.map(([label, s, color]) => ( +
+ {tiles.map(([label, s, color, cumulative]) => (
/s
p.value)} color={color} height={24} fillOpacity={0.2} /> +
+ {formatCompact(cumulative)} total +
))}
@@ -684,7 +1022,8 @@ function AssertsBlock({ stats, totalAsserts }: { stats: ServerStats; totalAssert ['regular', stats.asserts.regular], ['warning', stats.asserts.warning], ['user', stats.asserts.user], - ['msg', stats.asserts.msg] + ['msg', stats.asserts.msg], + ['rollovers', stats.asserts.rollovers] ].map(([label, value]) => (
  • {label} @@ -926,6 +1265,14 @@ function formatNumber(n: number): string { return n.toLocaleString() } +function formatCompact(n: number): string { + if (!Number.isFinite(n)) return '—' + if (Math.abs(n) < 1000) return n.toFixed(0) + if (Math.abs(n) < 1_000_000) return `${(n / 1000).toFixed(1)}k` + if (Math.abs(n) < 1_000_000_000) return `${(n / 1_000_000).toFixed(2)}M` + return `${(n / 1_000_000_000).toFixed(2)}B` +} + function formatUptime(seconds: number): string { if (!Number.isFinite(seconds) || seconds < 0) return '—' const days = Math.floor(seconds / 86400) @@ -973,7 +1320,14 @@ const EMPTY_HISTORY: StatsSample[] = [] function derive(history: StatsSample[]): Series { const empty: Series = { opsPerSec: [], - opsPerSecByKind: { query: [], insert: [], update: [], delete: [] }, + opsPerSecByKind: { + query: [], + insert: [], + update: [], + delete: [], + getmore: [], + command: [] + }, connections: [], cacheFillPct: [], residentMb: [], @@ -1030,6 +1384,14 @@ function derive(history: StatsSample[]): Series { ts: cur.ts, value: Math.max(0, (cur.data.opcounters.delete - prev.data.opcounters.delete) / dt) }) + empty.opsPerSecByKind.getmore.push({ + ts: cur.ts, + value: Math.max(0, (cur.data.opcounters.getmore - prev.data.opcounters.getmore) / dt) + }) + empty.opsPerSecByKind.command.push({ + ts: cur.ts, + value: Math.max(0, (cur.data.opcounters.command - prev.data.opcounters.command) / dt) + }) const bytesDelta = cur.data.network.bytesIn - From 7301b1df140f003497579bee0861f18145663329 Mon Sep 17 00:00:00 2001 From: ByteExceptionM Date: Sun, 10 May 2026 19:30:02 +0200 Subject: [PATCH 3/5] docs: rewrite README with full feature catalog and screenshots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the placeholder README with a hero shot, fourteen embedded screenshots covering every major feature (welcome screen, connection form, dashboard tabs, document table + context menu, document editor, operator and field-name autocomplete, indexes, users, command palette), and an exhaustive feature list grouped by area. Also drops the old docs/screenshots/*.svg references and the scripts/demo callout — the repo no longer ships demo data. --- README.md | 275 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 252 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5d82a58..7fee147 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,267 @@ +
    + # MongoBench -A modern, dark-mode-first MongoDB GUI. Built as a daily-driver alternative to MongoDB Compass. +A modern, dark-mode-first MongoDB GUI. Built as a daily-driver alternative to MongoDB Compass, focused on speed, density, and a polished editing experience. + +[![Latest release](https://img.shields.io/github/v/release/ByteExceptionM/MongoBench?style=flat-square&color=21DCEF)](https://github.com/ByteExceptionM/MongoBench/releases/latest) +[![License](https://img.shields.io/badge/license-MIT-1F232C?style=flat-square)](LICENSE) +[![CI](https://img.shields.io/github/actions/workflow/status/ByteExceptionM/MongoBench/ci.yml?branch=main&style=flat-square)](https://github.com/ByteExceptionM/MongoBench/actions) + +
    + +![MongoBench main view](https://i.masel.io/ZIXO8/HokIQaKU21.png/raw) + +## Highlights + +- **Multiple connections, side-by-side.** Connect to as many clusters as you want; each runs an independent pool. Drag-reorder them in the sidebar, manage them with right-click context menus. +- **Three query modes per tab.** Switch a single tab between **Simple** (filter / projection / sort), **Aggregation** pipeline, and **Shell** (`db.coll.find().limit()` syntax) without losing state. +- **Mongo shell syntax everywhere.** Type `ObjectId("…")`, `ISODate("…")`, `UUID("…")`, `NumberLong("…")` etc. directly in filters and editors — MongoBench parses it into canonical EJSON before sending. +- **Optimistic concurrency on writes.** Every edit and delete includes a sha-256 hash precondition of the document; concurrent edits surface as conflicts instead of silently overwriting. +- **Built-in observability.** Per-connection dashboard with op rate, read/write/command latency, connection pool state, cache fill, network throughput, and a per-database storage breakdown. +- **Auto-update on Windows and Linux** via `electron-updater`, pulling directly from this repo's GitHub Releases. + +## Screenshots + +### Welcome screen + +Saved connections at a glance with quick-connect buttons and inline tips. + +![Welcome screen](https://i.masel.io/ZIXO8/pIrEHaLi39.png/raw) + +### Connection form + +Full driver-option surface — auth, topology, pool, timeouts, UUID encoding, display timezone. + +![New connection dialog](https://i.masel.io/ZIXO8/huKuWAvE28.png/raw) + +### Connection dashboard + +Live per-connection stats. The hero shot above is the Overview tab; the Health tab focuses on document throughput, cursors, asserts, and a per-database breakdown. + +![Connection dashboard — health](https://i.masel.io/ZIXO8/fogaDuMu69.png/raw) + +### Document table + +Type-aware rendering — `ObjectId`, `Date`, `Decimal128`, `UUID`, `Long`, `Binary`, regex — each with its own kind badge. + +![Document table](https://i.masel.io/ZIXO8/NUlUBopA45.png/raw) + +### Document row context menu + +Right-click any row for **View**, **Edit**, **Duplicate**, **Copy**, **Delete**, plus a **Lookup ObjectId in…** submenu that jumps to the referencing collection. + +![Row context menu with cross-collection ObjectId lookup](https://i.masel.io/ZIXO8/xOMAzUja46.png/raw) + +### Document editor + +Monaco editor with mongo shell helpers (`ObjectId(…)`, `ISODate(…)`, `NumberDecimal(…)`, `UUID(…)`). Hash-protected writes, view / edit / duplicate / insert modes. + +![Edit document dialog](https://i.masel.io/ZIXO8/XEQaXIko38.png/raw) + +### Query autocomplete + +Operator autocomplete (`$eq`, `$in`, `$elemMatch`, `$geoWithin`, …) and field-name autocomplete from the most recent results. + +![Operator autocomplete](https://i.masel.io/ZIXO8/mIhavoWU58.png/raw) +![Field-name autocomplete](https://i.masel.io/ZIXO8/KiTUxUkA81.png/raw) + +### Indexes + +Per-collection index manager — list with key spec, properties, size; create with full option surface (TTL, partial filter, collation, weights, 2dsphere, wildcard). + +![Indexes dialog](https://i.masel.io/ZIXO8/ZuzImILI62.png/raw) + +### Users + +Per-database user management. Common-role shortcuts plus arbitrary custom roles. + +![Manage users](https://i.masel.io/ZIXO8/royEDozi90.png/raw) +![New user dialog](https://i.masel.io/ZIXO8/codOjUZU83.png/raw) + +### Command palette + +`Ctrl+K` / `Cmd+K` — fuzzy subsequence search across every active connection, database, and collection. Picking a result expands the sidebar and scrolls the row into view. -> Status: early development +![Command palette — connections](https://i.masel.io/ZIXO8/HeCAzubE08.png/raw) +![Command palette — collection search](https://i.masel.io/ZIXO8/RUcUvaJu02.png/raw) -## Stack +## Features -- Electron + TypeScript (strict) -- Vite via `electron-vite` (separate main / preload / renderer builds) -- React 18, Zustand, TanStack Query -- Tailwind CSS + shadcn/ui -- Official `mongodb` Node.js driver, `bson` for EJSON (added in M1) +### Connections -## Scripts +- Multiple **active connections** simultaneously, each with its own pool +- **Encrypted password storage** via OS keystore (Windows DPAPI, libsecret on Linux) +- **Test before save** — probes server, reports MongoDB version + ping latency +- **Drag-to-reorder** saved connections in the sidebar +- **Right-click context menu** per connection: connect / disconnect, edit, delete, new database, refresh, copy URI +- Full driver option surface, persisted per connection: + - URI: `mongodb://` and `mongodb+srv://` + - Auth: `SCRAM-SHA-256`, `SCRAM-SHA-1`, `DEFAULT`, custom `authSource` + - Topology: `directConnection`, `replicaSet`, `readPreference` (5 modes) + - Pool: `minPoolSize`, `maxPoolSize` + - Timeouts: `connectTimeoutMS`, `socketTimeoutMS`, `serverSelectionTimeoutMS` + - Behavior: `retryWrites`, `retryReads`, `appName`, `tls` + - Display: `uuidEncoding` (default / Java legacy), IANA `timezone`, `authorizedOnly` filter -| Command | What it does | -| ------------------- | ----------------------------------------------------- | -| `npm run dev` | Launch the app in development mode with HMR | -| `npm run build` | Build all three processes for production into `out/` | -| `npm run typecheck` | Run the TypeScript project-references build (no emit) | -| `npm run lint` | ESLint over `src/` | -| `npm run format` | Apply Prettier | -| `npm test` | Run Vitest | +### Explorer -## Project layout +- Three-level tree: **connection → database → collection** +- Live status dot per connection (connected / disconnected / busy) +- Right-click on a **database**: new collection, manage users, drop, copy name +- Right-click on a **collection**: open, info, copy namespace, insert document, manage indexes, rename, drop +- Distinct icons for **collections vs. views** +- Hover-quick-create buttons on database rows +- Active tab is highlighted in the sidebar; opening a tab from the palette auto-expands and scrolls to the row + +### Query modes + +| Mode | Surface | Highlights | +| --------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| **Simple** | filter · projection · sort · skip · limit | EJSON or shell syntax, field-name autocomplete from last results | +| **Aggregation** | pipeline as `[{ $match: … }, …]` | Monaco, full operator autocomplete | +| **Shell** | `db.coll.find({…}).sort({…}).limit(n)` | Curated subset of mongosh syntax — `find`, `findOne`, `aggregate`, `count`, `countDocuments`, `estimatedDocumentCount` | + +- **Run:** click button or `Ctrl+Enter` / `Cmd+Enter` +- **Format:** `Shift+Alt+F` — custom formatter that preserves shell helpers +- **Operator autocomplete** for every standard query operator (`$eq`, `$in`, `$elemMatch`, `$geoWithin`, `$jsonSchema`, …) + +### Documents + +- Auto-sized table columns, drag-to-resize, per-collection persistence +- Multi-select with `Shift+Click` range select +- Right-click row: **view, edit, duplicate, delete, copy `_id`, jump to referenced ObjectId** (cross-collection lookup submenu) +- **View / edit modes:** + - Canonical EJSON (`{ "$oid": "…" }`, `{ "$date": "…" }`, …) or relaxed display + - Shell-syntax accepted on input, parsed before send + - sha-256 hash precondition on `replaceOne` / `deleteOne` to catch concurrent edits +- **Bulk delete** via multi-select + confirm +- **Export** the current result set as JSON, CSV or TSV +- **Type-aware rendering** in the table: + - `ObjectId` as hex string with kind badge + - `Date` rendered in the connection's display timezone (IANA) + - `UUID` (BSON subType 4 always; subType 3 only if `uuidEncoding: "java"`, with byte-reversal) + - `Decimal128`, `Long` as exact strings + - `Binary`, `Regex`, nested objects / arrays summarized + +### Indexes + +- List with name, key spec, type (1/-1/text/2dsphere/2d/hashed), TTL, uniqueness, sparseness, hidden flag, partial filter, collation, weights +- Create with full option surface: `unique`, `sparse`, `hidden`, `expireAfterSeconds`, `partialFilterExpression`, `collation`, `weights`, `default_language`, `language_override`, `textIndexVersion`, `2dsphereIndexVersion`, `bits`, `min`, `max`, `wildcardProjection` +- Drop with confirmation (`_id` index protected) +- Per-index size from `collStats` + +### Users + +- List per-database users with their roles +- Create with username, password, role assignments per database +- Common-role shortcuts: `read`, `readWrite`, `dbAdmin`, `dbOwner`, `userAdmin` — plus arbitrary custom roles +- Update password and / or roles independently (preserve unchanged fields) +- Drop with confirmation + +### Database & collection ops + +- Create / drop database (with confirmation, closes affected tabs) +- Create / rename / drop collection +- **Collection info dialog**: namespace, document count, average doc size, storage size (compressed + uncompressed), index count, free space, capped status + +### Connection dashboard + +Per-connection live view, sampled every 5 s, sliding 5-min history: + +- **KPIs:** ops/sec, current connections (with cap), cache fill %, resident memory, total storage +- **Charts** with hover crosshair + tooltips: + - Operation latency (read / write / command, µs) + - Operations breakdown (query / insert / update / delete / getmore / command) + - Network throughput (bytes in / out) +- **Storage pie:** top databases + aggregated "other" wedge, total in the center + +### Command palette + +- Trigger: `Ctrl+K` / `Cmd+K` +- Fuzzy subsequence matching with prefix-aware scoring across **all active connections** (databases + collections + saved connections) +- Pre-fetches collection metadata when opened so search is instant +- Result actions: open collection (auto-expand + scroll in sidebar), connect / disconnect + +### Visual & interaction polish + +- **Dark theme** with cool, neutral palette — no warm undertones +- **Geist Variable** for UI, **JetBrains Mono** for code; tabular numerals throughout +- Custom **Monaco theme** matching the app palette +- **Custom scrollbars** (Chromium): 10 px, transparent track, primary-tinted on grab +- **Status indicators** everywhere: spinners on pending operations, success / error toasts, hash-protected badges in the status bar + +## Install + +| Platform | Download | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Windows (x64)** | `MongoBench-Setup--x64.exe` from [Releases](https://github.com/ByteExceptionM/MongoBench/releases/latest) — NSIS installer, per-user, no admin | +| **Linux (x64)** | `MongoBench--x86_64.AppImage` — `chmod +x` and run | +| **Arch / AUR** | PKGBUILD shipped under `packaging/arch/` | + +Builds are **unsigned**. On first launch on Windows you'll see SmartScreen — click "More info → Run anyway". + +### Auto-update + +Installed builds check GitHub Releases at startup via `electron-updater`. New versions are downloaded in the background and applied on quit — no prompts, no clicks. AppImage updates self-replace; the Arch package updates via `pacman`. + +## Develop + +```bash +git clone https://github.com/ByteExceptionM/MongoBench +cd MongoBench +npm install +npm run dev # launches the app with HMR +``` + +Useful scripts: + +| Command | What it does | +| -------------------- | --------------------------------------------------------------- | +| `npm run dev` | Run the app with hot reload (renderer) and main-process restart | +| `npm run build` | Build all three processes into `out/` | +| `npm run dist:win` | Build a Windows NSIS installer into `dist/` | +| `npm run dist:linux` | Build a Linux AppImage into `dist/` | +| `npm run typecheck` | TypeScript project-references build, no emit | +| `npm run lint` | ESLint over `src/` | +| `npm run format` | Apply Prettier | +| `npm test` | Vitest | + +### Project layout ``` src/ -├── main/ # Node.js side: window, services, IPC handlers -├── preload/ # contextBridge — typed API exposed to the renderer -├── renderer/ # React UI (Vite) -└── shared/ # Types and Zod schemas shared across processes +├── main/ # Node side: window, services, IPC handlers, MongoClient pool +├── preload/ # contextBridge — typed API exposed to the renderer +├── renderer/ # React UI (Vite + Tailwind + shadcn/ui + Monaco) +└── shared/ # Types and Zod schemas shared across processes ``` +### Stack + +- **Electron 32** + **TypeScript** strict +- **electron-vite** — separate main / preload / renderer Vite configs +- **React 18** + **Zustand** + **TanStack Query** +- **Tailwind CSS v3** + **shadcn/ui** (Radix primitives) +- **Monaco** editor with custom `mongobench-dark` theme +- **mongodb** Node driver v7 + **bson** v7 (canonical / relaxed EJSON) +- **electron-builder** for packaging, **electron-updater** for self-update +- **Vitest** for unit tests + +## Compatibility + +- MongoDB **4.4+** (older versions may work but aren't tested) +- Auth mechanisms: `SCRAM-SHA-256`, `SCRAM-SHA-1`, `DEFAULT` +- TLS, replica sets, sharded clusters, direct connections +- IPv4 + IPv6, Atlas SRV, self-hosted, localhost + +## Known limitations + +- Builds are **unsigned** — Windows shows SmartScreen on first launch +- No **change-streams / live tail** UI yet +- Shell mode parses a curated subset of `db.coll.(...)` syntax, not the full mongosh grammar +- macOS builds aren't published (the codebase runs on macOS in dev; binaries just aren't in the release matrix yet) + ## License -MIT +[MIT](LICENSE). From d965bcf077435294b275f9df082d87d69d87d91d Mon Sep 17 00:00:00 2001 From: ByteExceptionM Date: Sun, 10 May 2026 19:30:31 +0200 Subject: [PATCH 4/5] chore: bump version to 1.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 311529d..ad8afea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mongobench", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mongobench", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "@fontsource-variable/geist": "^5.2.8", diff --git a/package.json b/package.json index 93ba6de..2bd8541 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mongobench", - "version": "1.1.0", + "version": "1.2.0", "private": true, "description": "A modern, dark-mode-first MongoDB GUI.", "author": "ByteExceptionM", From 1429451121c463eec5720433bc9d79547a0b1ca8 Mon Sep 17 00:00:00 2001 From: ByteExceptionM Date: Sun, 10 May 2026 19:35:27 +0200 Subject: [PATCH 5/5] fix: reorder shields.io badge query params shields.io renders the latest-release badge as "invalid" when ?style=flat-square comes before &color=21DCEF; swapping the order makes it resolve to v1.1.0 again. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fee147..2096f1d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A modern, dark-mode-first MongoDB GUI. Built as a daily-driver alternative to MongoDB Compass, focused on speed, density, and a polished editing experience. -[![Latest release](https://img.shields.io/github/v/release/ByteExceptionM/MongoBench?style=flat-square&color=21DCEF)](https://github.com/ByteExceptionM/MongoBench/releases/latest) +[![Latest release](https://img.shields.io/github/v/release/ByteExceptionM/MongoBench?color=21DCEF&style=flat-square)](https://github.com/ByteExceptionM/MongoBench/releases/latest) [![License](https://img.shields.io/badge/license-MIT-1F232C?style=flat-square)](LICENSE) [![CI](https://img.shields.io/github/actions/workflow/status/ByteExceptionM/MongoBench/ci.yml?branch=main&style=flat-square)](https://github.com/ByteExceptionM/MongoBench/actions)