Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
import React, { useState } from 'react';
import { Camera, History, Trash2, RotateCcw, GitCompare } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
import { useDashboardSnapshots, DashboardSnapshot } from '@/hooks/useDashboardSnapshots';
import { DashboardWidget } from '@/pages/Dashboard';
import { format } from 'date-fns';
interface SnapshotManagerProps {
currentWidgets: DashboardWidget[];
onRestoreSnapshot: (widgets: DashboardWidget[]) => void;
}
export function SnapshotManager({ currentWidgets, onRestoreSnapshot }: SnapshotManagerProps) {
const { snapshots, createSnapshot, removeSnapshot, restoreSnapshot, compareSnapshots } = useDashboardSnapshots();
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
const [snapshotName, setSnapshotName] = useState('');
const [compareMode, setCompareMode] = useState(false);
const [selectedForCompare, setSelectedForCompare] = useState<string[]>([]);
const handleSave = async () => {
if (!snapshotName.trim()) return;
await createSnapshot(snapshotName, currentWidgets);
setSnapshotName('');
setSaveDialogOpen(false);
};
const handleRestore = (snapshotId: string) => {
const widgets = restoreSnapshot(snapshotId);
if (widgets) {
onRestoreSnapshot(widgets);
}
};
const handleCompareSelect = (snapshotId: string) => {
setSelectedForCompare(prev => {
if (prev.includes(snapshotId)) {
return prev.filter(id => id !== snapshotId);
}
if (prev.length >= 2) {
return [prev[1], snapshotId];
}
return [...prev, snapshotId];
});
};
const comparison = selectedForCompare.length === 2
? compareSnapshots(selectedForCompare[0], selectedForCompare[1])
: null;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="gap-2">
<Camera className="h-4 w-4" />
Snapshots
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-80">
<Dialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
<DialogTrigger asChild>
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
<Camera className="h-4 w-4 mr-2" />
Save Current State
</DropdownMenuItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Save Snapshot</DialogTitle>
<DialogDescription>
Save the current dashboard state for later restoration.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 pt-4">
<Input
placeholder="Snapshot name..."
value={snapshotName}
onChange={(e) => setSnapshotName(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setSaveDialogOpen(false)}>
Cancel
</Button>
<Button onClick={handleSave} disabled={!snapshotName.trim()}>
Save Snapshot
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<DropdownMenuItem onSelect={(e) => {
e.preventDefault();
setCompareMode(!compareMode);
setSelectedForCompare([]);
}}>
<GitCompare className="h-4 w-4 mr-2" />
{compareMode ? 'Exit Compare Mode' : 'Compare Snapshots'}
</DropdownMenuItem>
<DropdownMenuSeparator />
{snapshots.length === 0 ? (
<div className="p-4 text-center text-muted-foreground text-sm">
No snapshots saved yet
</div>
) : (
<ScrollArea className="h-64">
{compareMode && comparison && (
<div className="p-3 m-2 rounded-md bg-muted/50 text-xs space-y-1">
<div className="font-medium">Comparison Result:</div>
<div className="text-green-500">+ {comparison.added.length} added</div>
<div className="text-red-500">- {comparison.removed.length} removed</div>
<div className="text-muted-foreground">{comparison.unchanged.length} unchanged</div>
</div>
)}
{snapshots.map((snapshot) => (
<SnapshotItem
key={snapshot.id}
snapshot={snapshot}
compareMode={compareMode}
isSelected={selectedForCompare.includes(snapshot.id)}
onRestore={() => handleRestore(snapshot.id)}
onDelete={() => removeSnapshot(snapshot.id)}
onCompareSelect={() => handleCompareSelect(snapshot.id)}
/>
))}
</ScrollArea>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
interface SnapshotItemProps {
snapshot: DashboardSnapshot;
compareMode: boolean;
isSelected: boolean;
onRestore: () => void;
onDelete: () => void;
onCompareSelect: () => void;
}
function SnapshotItem({ snapshot, compareMode, isSelected, onRestore, onDelete, onCompareSelect }: SnapshotItemProps) {
return (
<div
className={`flex items-center justify-between p-2 hover:bg-accent/50 cursor-pointer ${isSelected ? 'bg-accent' : ''}`}
onClick={compareMode ? onCompareSelect : undefined}
>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate">{snapshot.name}</div>
<div className="text-xs text-muted-foreground">
{format(snapshot.createdAt, 'MMM d, HH:mm')} · {snapshot.data.widgets.length} widgets
</div>
</div>
{!compareMode && (
<div className="flex gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onRestore}>
<RotateCcw className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={onDelete}>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
)}
</div>
);
}