Spaces:
Paused
Paused
| 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> | |
| ); | |
| } | |