diff options
| author | diogo464 <[email protected]> | 2025-08-14 15:07:28 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-14 15:07:28 +0100 |
| commit | 912bef7608aab286a5cc82c8ac9e2e19b19b5f1c (patch) | |
| tree | 8896efe5081b7cf11606ca5a6270f388fa324be3 /frontend/components | |
| parent | e49771f9c97110b4e0d66c796716c43dd92166c4 (diff) | |
feat: add paginated history page with activity log
- Create /history page showing drive activity with server-side rendering
- Display timestamp, action, user, path, and file size in table format
- Add pagination (50 entries per page) using URL query parameters
- Sort entries by timestamp descending (most recent first)
- Add History button to drive header for easy navigation
- Use existing UI components and styling patterns
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <[email protected]>
Diffstat (limited to 'frontend/components')
| -rw-r--r-- | frontend/components/drive/DriveHeader.tsx | 14 | ||||
| -rw-r--r-- | frontend/components/history/HistoryView.tsx | 138 |
2 files changed, 150 insertions, 2 deletions
diff --git a/frontend/components/drive/DriveHeader.tsx b/frontend/components/drive/DriveHeader.tsx index 718d031..76b0e40 100644 --- a/frontend/components/drive/DriveHeader.tsx +++ b/frontend/components/drive/DriveHeader.tsx | |||
| @@ -1,5 +1,7 @@ | |||
| 1 | import { HardDrive } from "lucide-react" | 1 | import { HardDrive, Clock } from "lucide-react" |
| 2 | import { AuthButtons } from "@/components/auth/AuthButtons" | 2 | import { AuthButtons } from "@/components/auth/AuthButtons" |
| 3 | import Link from "next/link" | ||
| 4 | import { Button } from "@/components/ui/button" | ||
| 3 | 5 | ||
| 4 | export async function DriveHeader() { | 6 | export async function DriveHeader() { |
| 5 | return ( | 7 | return ( |
| @@ -9,7 +11,15 @@ export async function DriveHeader() { | |||
| 9 | <h1 className="text-2xl font-bold">FCT Drive</h1> | 11 | <h1 className="text-2xl font-bold">FCT Drive</h1> |
| 10 | </div> | 12 | </div> |
| 11 | 13 | ||
| 12 | <AuthButtons /> | 14 | <div className="flex items-center gap-2"> |
| 15 | <Link href="/history"> | ||
| 16 | <Button variant="outline" size="sm"> | ||
| 17 | <Clock className="mr-2 h-4 w-4" /> | ||
| 18 | History | ||
| 19 | </Button> | ||
| 20 | </Link> | ||
| 21 | <AuthButtons /> | ||
| 22 | </div> | ||
| 13 | </div> | 23 | </div> |
| 14 | ) | 24 | ) |
| 15 | } \ No newline at end of file | 25 | } \ No newline at end of file |
diff --git a/frontend/components/history/HistoryView.tsx b/frontend/components/history/HistoryView.tsx new file mode 100644 index 0000000..1fe3cd2 --- /dev/null +++ b/frontend/components/history/HistoryView.tsx | |||
| @@ -0,0 +1,138 @@ | |||
| 1 | import { DriveLogEntry } from "@/lib/drive_types" | ||
| 2 | import { DriveHeader } from "@/components/drive/DriveHeader" | ||
| 3 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | ||
| 4 | import { Button } from "@/components/ui/button" | ||
| 5 | import { ChevronLeft, ChevronRight } from "lucide-react" | ||
| 6 | import Link from "next/link" | ||
| 7 | |||
| 8 | interface HistoryViewProps { | ||
| 9 | entries: DriveLogEntry[] | ||
| 10 | currentPage: number | ||
| 11 | hasNextPage: boolean | ||
| 12 | hasPrevPage: boolean | ||
| 13 | totalEntries: number | ||
| 14 | } | ||
| 15 | |||
| 16 | function formatFileSize(bytes: number): string { | ||
| 17 | if (bytes === 0) return "0 Bytes" | ||
| 18 | const k = 1024 | ||
| 19 | const sizes = ["Bytes", "KB", "MB", "GB"] | ||
| 20 | const i = Math.floor(Math.log(bytes) / Math.log(k)) | ||
| 21 | return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] | ||
| 22 | } | ||
| 23 | |||
| 24 | function formatDateTime(timestamp: number): string { | ||
| 25 | return new Date(timestamp * 1000).toLocaleString() | ||
| 26 | } | ||
| 27 | |||
| 28 | function PaginationControls({ currentPage, hasNextPage, hasPrevPage }: { | ||
| 29 | currentPage: number | ||
| 30 | hasNextPage: boolean | ||
| 31 | hasPrevPage: boolean | ||
| 32 | }) { | ||
| 33 | return ( | ||
| 34 | <div className="flex items-center justify-center gap-2"> | ||
| 35 | <Link href={`/history?page=${currentPage - 1}`}> | ||
| 36 | <Button | ||
| 37 | variant="outline" | ||
| 38 | size="sm" | ||
| 39 | disabled={!hasPrevPage} | ||
| 40 | className={!hasPrevPage ? "opacity-50 cursor-not-allowed" : ""} | ||
| 41 | > | ||
| 42 | <ChevronLeft className="h-4 w-4 mr-1" /> | ||
| 43 | Previous | ||
| 44 | </Button> | ||
| 45 | </Link> | ||
| 46 | |||
| 47 | <span className="text-sm text-muted-foreground px-4"> | ||
| 48 | Page {currentPage + 1} | ||
| 49 | </span> | ||
| 50 | |||
| 51 | <Link href={`/history?page=${currentPage + 1}`}> | ||
| 52 | <Button | ||
| 53 | variant="outline" | ||
| 54 | size="sm" | ||
| 55 | disabled={!hasNextPage} | ||
| 56 | className={!hasNextPage ? "opacity-50 cursor-not-allowed" : ""} | ||
| 57 | > | ||
| 58 | Next | ||
| 59 | <ChevronRight className="h-4 w-4 ml-1" /> | ||
| 60 | </Button> | ||
| 61 | </Link> | ||
| 62 | </div> | ||
| 63 | ) | ||
| 64 | } | ||
| 65 | |||
| 66 | export function HistoryView({ entries, currentPage, hasNextPage, hasPrevPage, totalEntries }: HistoryViewProps) { | ||
| 67 | return ( | ||
| 68 | <div className="container mx-auto p-6 space-y-6"> | ||
| 69 | <DriveHeader /> | ||
| 70 | |||
| 71 | <div className="space-y-4"> | ||
| 72 | <div className="flex items-center justify-between"> | ||
| 73 | <h1 className="text-2xl font-bold">Activity History</h1> | ||
| 74 | <div className="text-sm text-muted-foreground"> | ||
| 75 | Showing {entries.length} of {totalEntries} entries | ||
| 76 | </div> | ||
| 77 | </div> | ||
| 78 | |||
| 79 | <PaginationControls | ||
| 80 | currentPage={currentPage} | ||
| 81 | hasNextPage={hasNextPage} | ||
| 82 | hasPrevPage={hasPrevPage} | ||
| 83 | /> | ||
| 84 | |||
| 85 | <div className="border rounded-lg"> | ||
| 86 | <Table> | ||
| 87 | <TableHeader> | ||
| 88 | <TableRow> | ||
| 89 | <TableHead>Timestamp</TableHead> | ||
| 90 | <TableHead>Action</TableHead> | ||
| 91 | <TableHead>User</TableHead> | ||
| 92 | <TableHead>Path</TableHead> | ||
| 93 | <TableHead>Size</TableHead> | ||
| 94 | </TableRow> | ||
| 95 | </TableHeader> | ||
| 96 | <TableBody> | ||
| 97 | {entries.length === 0 ? ( | ||
| 98 | <TableRow> | ||
| 99 | <TableCell colSpan={5} className="text-center py-8 text-muted-foreground"> | ||
| 100 | No activity history found | ||
| 101 | </TableCell> | ||
| 102 | </TableRow> | ||
| 103 | ) : ( | ||
| 104 | entries.map((entry) => ( | ||
| 105 | <TableRow key={entry.log_id} className="hover:bg-muted/50"> | ||
| 106 | <TableCell className="font-mono text-sm"> | ||
| 107 | {formatDateTime(entry.timestamp)} | ||
| 108 | </TableCell> | ||
| 109 | <TableCell> | ||
| 110 | <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> | ||
| 111 | {entry.action} | ||
| 112 | </span> | ||
| 113 | </TableCell> | ||
| 114 | <TableCell className="font-medium"> | ||
| 115 | {entry.email} | ||
| 116 | </TableCell> | ||
| 117 | <TableCell className="font-mono text-sm max-w-md truncate"> | ||
| 118 | {entry.path} | ||
| 119 | </TableCell> | ||
| 120 | <TableCell> | ||
| 121 | {formatFileSize(entry.size)} | ||
| 122 | </TableCell> | ||
| 123 | </TableRow> | ||
| 124 | )) | ||
| 125 | )} | ||
| 126 | </TableBody> | ||
| 127 | </Table> | ||
| 128 | </div> | ||
| 129 | |||
| 130 | <PaginationControls | ||
| 131 | currentPage={currentPage} | ||
| 132 | hasNextPage={hasNextPage} | ||
| 133 | hasPrevPage={hasPrevPage} | ||
| 134 | /> | ||
| 135 | </div> | ||
| 136 | </div> | ||
| 137 | ) | ||
| 138 | } \ No newline at end of file | ||
