Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
MONGODB_URI=mongodb://admin:password@localhost:27017
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=
7 changes: 7 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"recommendations": [
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
nodeLinker: pnp
15 changes: 11 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@
"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",
"lint": "next lint",
"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",
Expand All @@ -34,13 +36,18 @@
"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",
"tailwind-merge": "^3.4.0",
"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",
Expand All @@ -52,6 +59,6 @@
"postcss": "^8",
"prettier": "3.7.4",
"tailwindcss": "^4.1.18",
"typescript": "^5"
"typescript": "5.9.3"
}
}
}
Original file line number Diff line number Diff line change
@@ -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 (
<div className='p-6 space-y-6'>
<div className='flex items-center gap-3'>
<div className='flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/10'>
<Layers className='h-5 w-5 text-purple-500' />
</div>
<div>
<h1 className='text-2xl font-bold tracking-tight'>Aggregation Pipeline</h1>
<p className='text-sm text-muted-foreground'>
Build and run pipelines on {collectionName} in {databaseName}
</p>
</div>
</div>

<Separator />

<AggregationPipeline
databaseName={databaseName}
collectionName={collectionName}
runAggregation={runSerializedAggregation}
/>
</div>
);
}

export const dynamic = 'force-dynamic';
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -132,7 +133,8 @@ export function CollectionActions({
<CardContent className='grid grid-cols-2 gap-4'>
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
<DialogTrigger asChild>
<Button variant='outline' className='w-full'>
<Button variant='outline' className='w-full justify-start gap-2'>
<PenLine className='h-4 w-4' />
Rename Collection
</Button>
</DialogTrigger>
Expand Down Expand Up @@ -165,17 +167,20 @@ export function CollectionActions({
</DialogContent>
</Dialog>

<Button variant='outline' className='w-full' onClick={handleReindex} disabled={isLoading === 'reindex'}>
<Button variant='outline' className='w-full justify-start gap-2' onClick={handleReindex} disabled={isLoading === 'reindex'}>
<RefreshCw className={`h-4 w-4 ${isLoading === 'reindex' ? 'animate-spin' : ''}`} />
{isLoading === 'reindex' ? 'Reindexing...' : 'Reindex'}
</Button>

<Button variant='outline' className='w-full' onClick={handleCompact} disabled={isLoading === 'compact'}>
<Button variant='outline' className='w-full justify-start gap-2' onClick={handleCompact} disabled={isLoading === 'compact'}>
<Minimize2 className='h-4 w-4' />
{isLoading === 'compact' ? 'Compacting...' : 'Compact'}
</Button>

<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant='outline' className='w-full text-red-600 hover:text-red-600'>
<Button variant='outline' className='w-full justify-start gap-2 text-red-600 hover:text-red-600'>
<Trash2 className='h-4 w-4' />
Clear Data
</Button>
</AlertDialogTrigger>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,44 @@
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 (
<Card>
<CardHeader>
<CardTitle>Collection Statistics</CardTitle>
</CardHeader>
<CardContent>
<div className='grid grid-cols-2 gap-4'>
{[
{ 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) => (
<div key={stat.label}>
<div className='text-sm text-muted-foreground'>{stat.label}</div>
<div className='text-lg font-mono'>{stat.value}</div>
</div>
))}
{statItems.map((stat) => {
const Icon = stat.icon;
return (
<div key={stat.label} className='flex items-center gap-3'>
<div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-md ${stat.color}`}>
<Icon className='h-4 w-4' />
</div>
<div className='min-w-0'>
<p className='text-sm text-muted-foreground'>{stat.label}</p>
<p className='text-lg font-bold font-mono truncate'>{stat.value}</p>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
Expand Down
Loading
Loading