diff --git a/.env.example b/.env.example
index 58e7d87..4387aba 100644
--- a/.env.example
+++ b/.env.example
@@ -1 +1,10 @@
-MONGODB_URI=mongodb://admin:password@localhost:27017
\ No newline at end of file
+MONGODB_URI=mongodb://admin:password@localhost:27017
+
+# Authentication (optional - set both to enable auth, leave empty to disable)
+MONGOMAN_USERNAME=
+MONGOMAN_PASSWORD=
+# Optional read-only user
+MONGOMAN_READONLY_USERNAME=
+MONGOMAN_READONLY_PASSWORD=
+# Secret for session signing (auto-derived from MONGODB_URI if not set)
+AUTH_SECRET=
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..daaa5ee
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,7 @@
+{
+ "recommendations": [
+ "arcanis.vscode-zipfs",
+ "dbaeumer.vscode-eslint",
+ "esbenp.prettier-vscode"
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..b7e02b5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,10 @@
+{
+ "search.exclude": {
+ "**/.yarn": true,
+ "**/.pnp.*": true
+ },
+ "eslint.nodePath": ".yarn/sdks",
+ "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
+ "typescript.tsdk": ".yarn/sdks/typescript/lib",
+ "typescript.enablePromptUseWorkspaceTsdk": true
+}
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 0000000..09b1012
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1 @@
+nodeLinker: pnp
diff --git a/package.json b/package.json
index 5f604ac..49604bb 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "0.1.2",
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev --webpack",
"build": "next build",
"docker-build": "docker build -t mongoman .",
"start": "next start",
@@ -11,15 +11,17 @@
"format": "npx prettier . '!./mongodb_data' --write"
},
"dependencies": {
- "@codemirror/autocomplete": "^6.20.0",
+ "@codemirror/autocomplete": "^6.18.4",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.2",
+ "@codemirror/view": "^6.36.1",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-toast": "^1.2.15",
@@ -34,6 +36,7 @@
"mongodb": "^7.0.0",
"mongodb-query-parser": "^4.7.4",
"next": "16.1.0",
+ "next-themes": "^0.4.6",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hook-form": "^7.69.0",
@@ -41,6 +44,10 @@
"tailwindcss-animate": "^1.0.7",
"zod": "^4.2.1"
},
+ "resolutions": {
+ "@codemirror/view": "6.36.1",
+ "@codemirror/state": "6.5.0"
+ },
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4.1.18",
@@ -52,6 +59,6 @@
"postcss": "^8",
"prettier": "3.7.4",
"tailwindcss": "^4.1.18",
- "typescript": "^5"
+ "typescript": "5.9.3"
}
-}
\ No newline at end of file
+}
diff --git a/src/app/(default)/databases/[databaseName]/[collectionName]/aggregate/page.tsx b/src/app/(default)/databases/[databaseName]/[collectionName]/aggregate/page.tsx
new file mode 100644
index 0000000..3301a86
--- /dev/null
+++ b/src/app/(default)/databases/[databaseName]/[collectionName]/aggregate/page.tsx
@@ -0,0 +1,46 @@
+import { PageParamsWithCollection } from '@/lib/types';
+import { runAggregation } from '@/lib/mongodb';
+import { AggregationPipeline } from '@/components/aggregation-pipeline';
+import { EJSON } from 'bson';
+import { Layers } from 'lucide-react';
+import { Separator } from '@/components/ui/separator';
+
+export default async function Page({ params }: PageParamsWithCollection) {
+ const { collectionName, databaseName } = await params;
+
+ const runSerializedAggregation = async (
+ dbName: string,
+ collName: string,
+ pipeline: object[],
+ ) => {
+ 'use server';
+ const results = await runAggregation(dbName, collName, pipeline);
+ return results.map((doc) => EJSON.serialize(doc));
+ };
+
+ return (
+
+
+
+
+
+
+
Aggregation Pipeline
+
+ Build and run pipelines on {collectionName} in {databaseName}
+
+
+
+
+
+
+
+
+ );
+}
+
+export const dynamic = 'force-dynamic';
diff --git a/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-actions.tsx b/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-actions.tsx
index 21981a7..16dc66c 100644
--- a/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-actions.tsx
+++ b/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-actions.tsx
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
+import { PenLine, RefreshCw, Minimize2, Trash2 } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
@@ -132,7 +133,8 @@ export function CollectionActions({
-
+
+
{isLoading === 'reindex' ? 'Reindexing...' : 'Reindex'}
-
+
+
{isLoading === 'compact' ? 'Compacting...' : 'Compact'}
-
+
+
Clear Data
diff --git a/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-stats.tsx b/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-stats.tsx
index d468434..4367144 100644
--- a/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-stats.tsx
+++ b/src/app/(default)/databases/[databaseName]/[collectionName]/manage/collection-stats.tsx
@@ -3,12 +3,23 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CollectionStats as ICollectionStats } from '@/lib/types';
import { formatBytes } from '@/lib/utils';
+import { FileText, HardDrive, Hash, Layers, Scale, Archive } from 'lucide-react';
+import { type LucideIcon } from 'lucide-react';
interface StatsProps {
stats: ICollectionStats;
}
export function CollectionStats({ stats }: StatsProps) {
+ const statItems: { label: string; value: string | number; icon: LucideIcon; color: string }[] = [
+ { label: 'Documents', value: stats?.count ?? 0, icon: FileText, color: 'text-blue-500 bg-blue-500/10' },
+ { label: 'Total Size', value: formatBytes(stats?.size ?? 0), icon: HardDrive, color: 'text-green-500 bg-green-500/10' },
+ { label: 'Avg Doc Size', value: formatBytes(stats?.avgObjSize ?? 0), icon: Scale, color: 'text-purple-500 bg-purple-500/10' },
+ { label: 'Storage', value: formatBytes(stats?.storageSize ?? 0), icon: Archive, color: 'text-orange-500 bg-orange-500/10' },
+ { label: 'Indexes', value: stats?.nindexes ?? 0, icon: Layers, color: 'text-cyan-500 bg-cyan-500/10' },
+ { label: 'Index Size', value: formatBytes(stats?.totalIndexSize ?? 0), icon: Hash, color: 'text-pink-500 bg-pink-500/10' },
+ ];
+
return (
@@ -16,21 +27,20 @@ export function CollectionStats({ stats }: StatsProps) {
- {[
- { label: 'Documents Count', value: stats?.count ?? 0 },
- { label: 'Total Size', value: formatBytes(stats?.size ?? 0) },
- { label: 'Average Doc Size', value: formatBytes(stats?.avgObjSize ?? 0) },
- { label: 'Pre-allocated Size', value: formatBytes(stats?.storageSize ?? 0) },
- { label: 'Number of Indexes', value: stats?.nindexes ?? 0 },
- { label: 'Total Index Size', value: formatBytes(stats?.totalIndexSize ?? 0) },
- { label: 'Padding Factor', value: stats?.paddingFactor?.toFixed(2) ?? '1.00' },
- { label: 'Extents', value: stats?.nExtents ?? 0 },
- ].map((stat) => (
-
-
{stat.label}
-
{stat.value}
-
- ))}
+ {statItems.map((stat) => {
+ const Icon = stat.icon;
+ return (
+
+
+
+
+
+
{stat.label}
+
{stat.value}
+
+
+ );
+ })}
diff --git a/src/app/(default)/databases/[databaseName]/[collectionName]/manage/document-management.tsx b/src/app/(default)/databases/[databaseName]/[collectionName]/manage/document-management.tsx
index 30ac75c..fe385fe 100644
--- a/src/app/(default)/databases/[databaseName]/[collectionName]/manage/document-management.tsx
+++ b/src/app/(default)/databases/[databaseName]/[collectionName]/manage/document-management.tsx
@@ -2,18 +2,11 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Input } from '@/components/ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { DataTable } from '@/components/data-table';
import { CellContext, ColumnDef } from '@tanstack/react-table';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import { Eye, MoreHorizontal, Pencil, Trash } from 'lucide-react';
+import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Eye, Pencil, Trash } from 'lucide-react';
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
AlertDialog,
AlertDialogAction,
@@ -25,9 +18,13 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useCallback, useEffect, useState } from 'react';
import { getUniqueKeys } from '@/lib/utils';
import Editor from '@/components/editor';
+import { Skeleton } from '@/components/ui/skeleton';
+import { QueryBuilder } from '@/components/query-builder';
+import { useToast } from '@/hooks/use-toast';
import { Document } from 'mongodb';
// Sub-components
@@ -36,34 +33,47 @@ interface DocumentActionsProps {
onEdit: (document: Document) => void;
onView: (document: Document) => void;
onDelete: (id: string) => void;
+ isDeleting?: boolean;
}
-function DocumentActions({ document, onEdit, onView, onDelete }: DocumentActionsProps) {
+function DocumentActions({ document, onEdit, onView, onDelete, isDeleting }: DocumentActionsProps) {
return (
-
-
-
- Open menu
-
-
-
-
- Actions
- onEdit(document)}>
-
- Edit
-
- onView(document)}>
-
- View
-
+
+
+
+
+ onView(document)}>
+
+
+
+ View
+
+
+
+
+ onEdit(document)}>
+
+
+
+ Edit
+
+
-
- e.preventDefault()}>
-
- Delete
-
-
+
+
+
+
+
+
+
+
+ Delete
+
Delete Document
@@ -72,15 +82,22 @@ function DocumentActions({ document, onEdit, onView, onDelete }: DocumentActions
- Cancel
- onDelete(document._id)} className='bg-red-600 hover:bg-red-700'>
- Delete
+ Cancel
+ {
+ e.preventDefault();
+ onDelete(document._id);
+ }}
+ className='bg-red-600 hover:bg-red-700'
+ >
+ {isDeleting ? 'Deleting...' : 'Delete'}
-
-
+
+
);
}
@@ -92,6 +109,8 @@ interface DocumentDialogProps {
onValueChange: (value: object) => void;
onSubmit: () => Promise;
submitLabel: string;
+ submittingLabel?: string;
+ isSubmitting?: boolean;
}
function DocumentDialog({
@@ -102,10 +121,12 @@ function DocumentDialog({
onValueChange,
onSubmit,
submitLabel,
+ submittingLabel,
+ isSubmitting,
}: DocumentDialogProps) {
return (
);
}
+// Pagination component
+interface PaginationProps {
+ page: number;
+ pageSize: number;
+ totalCount: number;
+ onPageChange: (page: number) => void;
+ onPageSizeChange: (size: number) => void;
+ isLoading?: boolean;
+}
+
+function Pagination({ page, pageSize, totalCount, onPageChange, onPageSizeChange, isLoading }: PaginationProps) {
+ const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
+ const startItem = totalCount === 0 ? 0 : page * pageSize + 1;
+ const endItem = Math.min((page + 1) * pageSize, totalCount);
+
+ return (
+
+
+ {totalCount > 0 ? (
+ <>Showing {startItem}-{endItem} of {totalCount.toLocaleString()}>
+ ) : (
+ 'No documents'
+ )}
+
+
+
+
+ Per page
+
+
+
+
+
+ Page {page + 1} of {totalPages}
+
+
+ onPageChange(0)}
+ disabled={page === 0 || isLoading}
+ >
+
+
+ onPageChange(page - 1)}
+ disabled={page === 0 || isLoading}
+ >
+
+
+ onPageChange(page + 1)}
+ disabled={page >= totalPages - 1 || isLoading}
+ >
+
+
+ onPageChange(totalPages - 1)}
+ disabled={page >= totalPages - 1 || isLoading}
+ >
+
+
+
+
+
+
+ );
+}
+
// Main component
interface DocumentManagementProps {
databaseName: string;
collectionName: string;
- getDocuments: (dbName: string, collectionName: string, filter?: object) => Promise;
+ getDocuments: (
+ dbName: string,
+ collectionName: string,
+ filter?: object,
+ skip?: number,
+ limit?: number,
+ ) => Promise<{ documents: Document[]; totalCount: number }>;
createDocument: (dbName: string, collectionName: string, document: Omit) => Promise;
updateDocument: (
dbName: string,
@@ -144,65 +264,117 @@ export function DocumentManagement({
deleteDocument,
getDocuments,
}: DocumentManagementProps) {
+ const { toast } = useToast();
const [searchQuery, setSearchQuery] = useState('');
const [createDialogOpen, setCreateDialogOpen] = useState(false);
const [editDialogOpen, setEditDialogOpen] = useState(false);
+ const [viewDialogOpen, setViewDialogOpen] = useState(false);
const [selectedDocument, setSelectedDocument] = useState(null);
const [documentContent, setDocumentContent] = useState