codeflash-internal/js/cf-webapp/src/app/(dashboard)/review-optimizations/page.tsx
HeshamHM28 ac99349a92
[Fix] Filter Query and Has Repository Query (#1951)
https://github.com/user-attachments/assets/bff0d449-0fbf-424e-9f5e-93054d8f7da6

### The problem was that there was no AND between Has Repo and the rest
of the query, and for Quality, the issue was that we didn’t check for
repository IDs or user IDs.



### How to test? 
Make the CF-WebApp connect to the production database, and then try
filtering and searching in staging. and make sure you are accessing the
correct data

---------

Co-authored-by: mashraf-222 <ashraf@codeflash.ai>
Co-authored-by: Sarthak Agarwal <sarthak.saga@gmail.com>
2025-11-10 10:04:19 -08:00

951 lines
32 KiB
TypeScript

"use client"
import { useState, useEffect, useCallback, useRef } from "react"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Search,
FileCode2,
Zap,
Clock,
ChevronLeft,
ChevronRight,
Filter,
ArrowUpDown,
ArrowUp,
ArrowDown,
} from "lucide-react"
import { formatDistanceToNow } from "date-fns"
import { useRouter } from "next/navigation"
import { getUserId, getUserIdAndUsername } from "@/app/utils/auth"
import { Button } from "@/components/ui/button"
import { getAllOptimizationEvents } from "./action"
import Image from "next/image"
import { useViewMode } from "@/app/app/ViewModeContext"
import { ReviewQualityBadge } from "@/components/ui/quality_badge"
// Type definitions
interface Repository {
id: string
full_name?: string
}
interface DiffContent {
oldContent: string
newContent: string
}
interface EventMetadata {
diffContents?: Record<string, DiffContent>
[key: string]: unknown
}
interface OptimizationEvent {
id: string
function_name?: string
file_path?: string
repository?: Repository | null | undefined
speedup_x?: number
speedup_pct?: number
metadata?: EventMetadata | null | undefined
created_at: string
status?: string
event_type?: string
trace_id: string
review_quality: string
}
interface FilterState {
search: string
hasRepo: string
status: string
eventType: string
reviewQuality: string
sortBy: string
page: number
}
function TableSkeleton() {
return (
<>
{Array.from({ length: 5 }).map((_, index) => (
<TableRow key={index}>
<TableCell>
<div className="flex items-start gap-3">
<div className="h-4 w-4 bg-muted animate-pulse rounded mt-1 flex-shrink-0" />
<div className="flex-1 min-w-0 space-y-2">
<div className="h-4 bg-muted animate-pulse rounded w-3/4" />
<div className="h-3 bg-muted animate-pulse rounded w-1/2" />
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-3">
<div className="h-8 w-8 bg-muted animate-pulse rounded-full flex-shrink-0" />
<div className="h-4 bg-muted animate-pulse rounded w-32" />
</div>
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-20 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-24 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-16 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="h-6 bg-muted animate-pulse rounded-full w-24 mx-auto" />
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-2">
<div className="h-6 bg-muted animate-pulse rounded-full w-12" />
<div className="h-6 bg-muted animate-pulse rounded-full w-12" />
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<div className="h-3 w-3 bg-muted animate-pulse rounded flex-shrink-0" />
<div className="h-4 bg-muted animate-pulse rounded w-24" />
</div>
</TableCell>
</TableRow>
))}
</>
)
}
// Helper function to calculate diff stats
function calculateDiffStats(
diffContents: Record<string, { oldContent: string; newContent: string }>,
) {
let totalAdditions = 0
let totalDeletions = 0
const fileStats: Record<string, { additions: number; deletions: number }> = {}
Object.entries(diffContents).forEach(([filePath, { oldContent, newContent }]) => {
const oldLines = oldContent.split("\n").filter(line => line.trim() !== "")
const newLines = newContent.split("\n").filter(line => line.trim() !== "")
const oldLineMap = new Map<string, number>()
const newLineMap = new Map<string, number>()
oldLines.forEach(line => {
const trimmed = line.trim()
oldLineMap.set(trimmed, (oldLineMap.get(trimmed) || 0) + 1)
})
newLines.forEach(line => {
const trimmed = line.trim()
newLineMap.set(trimmed, (newLineMap.get(trimmed) || 0) + 1)
})
let additions = 0
let deletions = 0
// Count deletions
for (const [line, oldCount] of Array.from(oldLineMap)) {
const newCount = newLineMap.get(line) || 0
if (oldCount > newCount) {
deletions += oldCount - newCount
}
}
// Count additions
for (const [line, newCount] of Array.from(newLineMap)) {
const oldCount = oldLineMap.get(line) || 0
if (newCount > oldCount) {
additions += newCount - oldCount
}
}
fileStats[filePath] = { additions, deletions }
totalAdditions += additions
totalDeletions += deletions
})
return {
totalAdditions,
totalDeletions,
fileStats,
}
}
// Client component for handling row clicks
function ClickableTableRow({
event,
children,
onRowClick,
}: {
event: OptimizationEvent
children: React.ReactNode
onRowClick: (eventId: string) => void
}) {
const handleRowClick = useCallback(
(e: React.MouseEvent) => {
if ((e.target as HTMLElement).closest('a[href^="http"]')) {
return
}
onRowClick(event.trace_id)
},
[event.trace_id, onRowClick],
)
return (
<TableRow
key={event.id}
className="group cursor-pointer hover:bg-muted"
onClick={handleRowClick}
>
{children}
</TableRow>
)
}
export default function StagingPage() {
const [userId, setUserId] = useState<string | null>(null)
const [isLoadingUser, setIsLoadingUser] = useState(true)
const router = useRouter()
const [events, setEvents] = useState<OptimizationEvent[]>([])
const [totalCount, setTotalCount] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const { currentOrg } = useViewMode()
// Combined filter state
const [filters, setFilters] = useState<FilterState>({
search: "",
hasRepo: "all",
status: "all",
eventType: "all",
reviewQuality: "all",
sortBy: "created_at_desc",
page: 1,
})
const pageSize = 10
// Refs to track if initial load is done
const isInitialMount = useRef(true)
const debounceTimer = useRef<NodeJS.Timeout>()
// Load user ID on mount - only once
useEffect(() => {
const loadUserId = async () => {
try {
const id = await getUserId()
setUserId(id)
} catch (err) {
console.error("Failed to get user ID:", err)
setUserId(null)
} finally {
setIsLoadingUser(false)
}
}
loadUserId()
}, [])
// Memoized load events function
const loadEvents = useCallback(async () => {
if (!userId) return
setIsLoading(true)
setError(null)
try {
const filter: Record<string, any> = {}
if (filters.hasRepo === "yes") {
filter.repository_id = { not: null }
} else if (filters.hasRepo === "no") {
filter.repository_id = null
}
if (filters.status !== "all") {
filter.status = filters.status
}
if (filters.eventType !== "all") {
filter.event_type = filters.eventType
}
if (filters.reviewQuality !== "all") {
filter.review_quality = filters.reviewQuality
}
// Parse sort parameter
const [sortField, sortDirection] = filters.sortBy.split("_").reduce(
(acc, part, index, arr) => {
if (index === arr.length - 1 && (part === "asc" || part === "desc")) {
return [acc[0], part]
}
return [acc[0] ? `${acc[0]}_${part}` : part, acc[1]]
},
["", "desc"] as [string, string],
)
const sort: Record<string, "asc" | "desc"> = {
[sortField]: sortDirection as "asc" | "desc",
}
const userSession = await getUserIdAndUsername()
const data = await getAllOptimizationEvents({
payload: currentOrg
? { orgId: currentOrg.id }
: { userId: userSession.userId, username: userSession.username },
search: filters.search,
filter,
sort,
page: filters.page,
pageSize,
})
// Transform the events to ensure metadata is properly typed
const transformedEvents: OptimizationEvent[] = (data?.events || []).map((event: any) => ({
...event,
metadata: event.metadata as EventMetadata | null | undefined,
repository: event.repository
? {
id: event.repository.id,
full_name: event.repository.full_name || event.repository.name,
}
: null,
}))
setEvents(transformedEvents)
setTotalCount(data?.totalCount || 0)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load events")
} finally {
setIsLoading(false)
}
}, [userId, filters, currentOrg, pageSize])
// Load events when filters change - with debounce for search
useEffect(() => {
if (!isLoadingUser && userId) {
// Skip initial mount
if (isInitialMount.current) {
isInitialMount.current = false
loadEvents()
return
}
// Clear existing timer
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
// Debounce only for search changes
const hasSearchChanged = filters.search !== ""
if (hasSearchChanged) {
debounceTimer.current = setTimeout(() => {
loadEvents()
}, 300)
} else {
loadEvents()
}
}
return () => {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
}
}, [userId, isLoadingUser, filters, loadEvents])
const handleRowClick = useCallback(
(traceId: string) => {
router.push(`/review-optimizations/${traceId}`)
},
[router],
)
// Update filter functions
const updateFilter = useCallback((key: keyof FilterState, value: string | number) => {
setFilters(prev => ({
...prev,
[key]: value,
// Reset page when filters change (except page itself)
...(key !== "page" && { page: 1 }),
}))
}, [])
const clearFilters = useCallback(() => {
setFilters({
search: "",
hasRepo: "all",
status: "all",
eventType: "all",
reviewQuality: "all",
sortBy: "created_at_desc",
page: 1,
})
}, [])
const hasActiveFilters =
filters.search ||
filters.hasRepo !== "all" ||
filters.status !== "all" ||
filters.eventType !== "all" ||
filters.reviewQuality !== "all" ||
filters.sortBy !== "created_at_desc"
const totalPages = Math.ceil(totalCount / pageSize)
const handlePageChange = useCallback(
(newPage: number) => {
if (newPage >= 1 && newPage <= totalPages) {
updateFilter("page", newPage)
}
},
[totalPages, updateFilter],
)
const getSortIcon = useCallback(
(field: string) => {
if (filters.sortBy.startsWith(field)) {
return filters.sortBy.endsWith("_asc") ? (
<ArrowUp className="h-4 w-4" />
) : (
<ArrowDown className="h-4 w-4" />
)
}
return <ArrowUpDown className="h-4 w-4 opacity-50" />
},
[filters.sortBy],
)
const toggleSort = useCallback(
(field: string) => {
const newSort = filters.sortBy.startsWith(field)
? filters.sortBy === `${field}_desc`
? `${field}_asc`
: `${field}_desc`
: `${field}_desc`
updateFilter("sortBy", newSort)
},
[filters.sortBy, updateFilter],
)
const getSpeedupBadge = useCallback((speedup?: number, speedupPct?: number) => {
if (typeof speedup !== "number" || typeof speedupPct !== "number") return null
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
const x = clamp(speedup, 1, 300)
const t = (x - 1) / 299
const hue = 137
const lightness = 92 - t * (92 - 28)
const saturation = 65 + t * (95 - 65)
const textColor = lightness < 55 ? "#fff" : "#14532d"
const borderLightness = lightness > 60 ? lightness - 12 : lightness + 12
const borderSaturation = saturation > 80 ? saturation - 10 : saturation + 10
const bgColor = `hsl(${hue}, ${saturation}%, ${lightness}%)`
const borderColor = `hsl(${hue}, ${borderSaturation}%, ${borderLightness}%)`
return (
<Badge
variant="default"
className="font-mono text-[11px] px-2 py-0.5 whitespace-nowrap"
style={{
backgroundColor: bgColor,
color: textColor,
border: `1px solid ${borderColor}`,
}}
>
{speedup.toFixed(2)}x ({speedupPct.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}%)
</Badge>
)
}, [])
const getStatusBadge = useCallback((status?: string) => {
if (!status) return null
const variants: Record<string, { className: string; label: string }> = {
approved: {
className:
"bg-green-100 text-green-800 border-green-300 dark:bg-green-900/30 dark:text-green-100 dark:border-green-700",
label: "Approved",
},
rejected: {
className:
"bg-red-100 text-red-800 border-red-300 dark:bg-red-900/30 dark:text-red-100 dark:border-red-700",
label: "Rejected",
},
}
const variant = variants[status] || {
className: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100",
label: status,
}
return (
<Badge variant="secondary" className={variant.className}>
{variant.label}
</Badge>
)
}, [])
const getEventTypeBadge = useCallback((eventType?: string) => {
if (!eventType) return null
const variants: Record<string, { className: string; label: string }> = {
pr_created: {
className:
"bg-blue-100 text-blue-800 border-blue-300 dark:bg-blue-900/30 dark:text-blue-100 dark:border-blue-700",
label: "PR Created",
},
pr_merged: {
className:
"bg-purple-100 text-purple-800 border-purple-300 dark:bg-purple-900/30 dark:text-purple-100 dark:border-purple-700",
label: "PR Merged",
},
pr_closed: {
className:
"bg-orange-100 text-orange-800 border-orange-300 dark:bg-orange-900/30 dark:text-orange-100 dark:border-orange-700",
label: "PR Closed",
},
"no-pr": {
className:
"bg-gray-100 text-gray-800 border-gray-300 dark:bg-gray-800 dark:text-gray-100 dark:border-gray-600",
label: "Staged Changes",
},
}
const variant = variants[eventType] || {
className: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100",
label: eventType,
}
return (
<Badge variant="secondary" className={variant.className}>
{variant.label}
</Badge>
)
}, [])
if (isLoadingUser) {
return (
<div className="flex flex-col items-center justify-center h-[70vh]">
<div className="animate-spin rounded-full h-10 w-10 sm:h-12 sm:w-12 border-t-2 border-b-2 border-primary mb-4"></div>
</div>
)
}
if (!userId) {
return (
<div className="container mx-auto py-8 px-4 max-w-7xl">
<div className="text-center py-12">
<p className="text-muted-foreground">Please sign in to view staging events</p>
</div>
</div>
)
}
return (
<div className="py-8 px-4">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Review Optimizations</h1>
</div>
{/* Search and Filters */}
<div className="mb-6">
<div className="flex flex-wrap items-center gap-3">
{/* Search Input */}
<div className="relative flex-1 min-w-[200px] max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search by function name or file path..."
value={filters.search}
onChange={e => updateFilter("search", e.target.value)}
className="pl-10 w-full"
/>
</div>
{/* Filter Icon and Label */}
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Filter className="h-4 w-4" />
<span className="hidden sm:inline">Filters:</span>
</div>
{/* Filter Controls */}
<Select value={filters.hasRepo} onValueChange={value => updateFilter("hasRepo", value)}>
<SelectTrigger className="w-[140px] sm:w-[180px]">
<SelectValue placeholder="Repository" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Repositories</SelectItem>
<SelectItem value="yes">With Repository</SelectItem>
<SelectItem value="no">Without Repository</SelectItem>
</SelectContent>
</Select>
<Select value={filters.status} onValueChange={value => updateFilter("status", value)}>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Reviews</SelectItem>
<SelectItem value="approved">Approved</SelectItem>
<SelectItem value="rejected">Rejected</SelectItem>
</SelectContent>
</Select>
<Select
value={filters.eventType}
onValueChange={value => updateFilter("eventType", value)}
>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectValue placeholder="Event Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Status</SelectItem>
<SelectItem value="pr_created">PR Created</SelectItem>
<SelectItem value="pr_merged">PR Merged</SelectItem>
<SelectItem value="pr_closed">PR Closed</SelectItem>
<SelectItem value="no-pr">Staged Changes</SelectItem>
</SelectContent>
</Select>
<Select
value={filters.reviewQuality}
onValueChange={value => updateFilter("reviewQuality", value)}
>
<SelectTrigger className="w-[120px] sm:w-[150px]">
<SelectValue placeholder="Quality" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Quality</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="low">Low</SelectItem>
</SelectContent>
</Select>
<Select value={filters.sortBy} onValueChange={value => updateFilter("sortBy", value)}>
<SelectTrigger className="w-[140px] sm:w-[200px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="created_at_desc">Newest</SelectItem>
<SelectItem value="created_at_asc">Oldest</SelectItem>
<SelectItem value="speedup_x_desc">Speedup (Highest)</SelectItem>
<SelectItem value="speedup_x_asc">Speedup (Lowest)</SelectItem>
<SelectItem value="review_quality_desc">Quality (High to Low)</SelectItem>
<SelectItem value="review_quality_asc">Quality (Low to High)</SelectItem>
</SelectContent>
</Select>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="text-muted-foreground hover:text-foreground"
>
Clear
</Button>
)}
</div>
</div>
{/* Error State */}
{error && (
<div className="mb-6 p-4 rounded-lg bg-destructive/10 border border-destructive/20">
<p className="text-destructive text-sm">Error: {error}</p>
<Button
variant="outline"
size="sm"
onClick={loadEvents}
className="mt-2"
disabled={isLoading}
>
Retry
</Button>
</div>
)}
{/* Combined Table */}
<div className="rounded-lg border bg-card">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[25%]">FUNCTION / FILE</TableHead>
<TableHead className="w-[18%]">REPOSITORY</TableHead>
<TableHead className="text-center">REVIEW</TableHead>
<TableHead className="text-center">STATUS</TableHead>
<TableHead
className="text-center cursor-pointer hover:bg-muted/50"
onClick={() => toggleSort("review_quality")}
>
<div className="flex items-center justify-center gap-1">
<span>QUALITY</span>
{getSortIcon("review_quality")}
</div>
</TableHead>
<TableHead
className="text-center cursor-pointer hover:bg-muted/50"
onClick={() => toggleSort("speedup_x")}
>
<div className="flex items-center justify-center gap-1">
<span>SPEEDUP</span>
{getSortIcon("speedup_x")}
</div>
</TableHead>
<TableHead className="text-center">CHANGES</TableHead>
<TableHead
className="text-right cursor-pointer hover:bg-muted/50"
onClick={() => toggleSort("created_at")}
>
<div className="flex items-center justify-end gap-1">
<span>CREATED AT</span>
{getSortIcon("created_at")}
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableSkeleton />
) : events.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-12">
<div className="text-muted-foreground">
<Zap className="h-12 w-12 mx-auto mb-4 opacity-20" />
<p>No optimization events found</p>
{hasActiveFilters && <p className="text-sm mt-2">Try adjusting your filters</p>}
</div>
</TableCell>
</TableRow>
) : (
events.map((event: OptimizationEvent) => {
// Extract diffContents safely from metadata
let diffStats: { totalAdditions: number; totalDeletions: number } = {
totalAdditions: 0,
totalDeletions: 0,
}
if (
event.metadata &&
typeof event.metadata === "object" &&
event.metadata !== null &&
typeof event.metadata.diffContents === "object" &&
event.metadata.diffContents !== null
) {
const diffContentsRaw = event.metadata.diffContents
if (diffContentsRaw && typeof diffContentsRaw === "object") {
let valid = true
for (const value of Object.values(diffContentsRaw as object)) {
if (
!value ||
typeof value !== "object" ||
typeof (value as DiffContent).oldContent !== "string" ||
typeof (value as DiffContent).newContent !== "string"
) {
valid = false
break
}
}
if (valid) {
const diffContents = diffContentsRaw as Record<
string,
{ oldContent: string; newContent: string }
>
diffStats = calculateDiffStats(diffContents)
}
}
}
return (
<ClickableTableRow key={event.id} event={event} onRowClick={handleRowClick}>
<TableCell className="w-auto min-w-0">
<div className="flex items-start gap-3">
<FileCode2 className="h-4 w-4 text-muted-foreground mt-1 flex-shrink-0" />
<div className="flex-1 min-w-0 overflow-hidden">
<div className="font-mono text-sm font-medium truncate">
{event.function_name || "Unknown Function"}
</div>
<div className="text-xs text-muted-foreground truncate">
{event.file_path || "No file path"}
</div>
</div>
</div>
</TableCell>
<TableCell className="w-auto min-w-0">
<div className="flex items-center gap-3">
{event.repository ? (
<>
<div className="relative h-8 w-8 flex-shrink-0">
{event.repository.full_name && (
<Image
src={`https://github.com/${event.repository.full_name.split("/")[0]}.png`}
alt={event.repository.full_name}
fill
className="rounded-full object-cover"
onError={e => {
e.currentTarget.style.display = "none"
}}
/>
)}
</div>
<div className="flex-1 min-w-0 overflow-hidden">
<div className="flex items-center gap-1">
<span className="text-sm font-medium truncate">
{event.repository.full_name || "Unknown Repository"}
</span>
</div>
</div>
</>
) : (
<>
<div className="h-8 w-8 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
<Zap className="h-4 w-4 text-muted-foreground" />
</div>
<span className="text-sm text-muted-foreground">
Untracked repository
</span>
</>
)}
</div>
</TableCell>
<TableCell className="text-center">{getStatusBadge(event.status)}</TableCell>
<TableCell className="text-center">
{getEventTypeBadge(event.event_type)}
</TableCell>
<TableCell className="text-center">
<ReviewQualityBadge quality={event.review_quality} />
</TableCell>
<TableCell className="text-center">
{getSpeedupBadge(event.speedup_x, event.speedup_pct)}
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-2 flex-wrap">
{diffStats.totalAdditions > 0 && (
<Badge
variant="secondary"
className="bg-green-100 text-green-800 border-green-300 dark:bg-green-900 dark:text-green-100 dark:border-green-700 whitespace-nowrap"
>
+{diffStats.totalAdditions}
</Badge>
)}
{diffStats.totalDeletions > 0 && (
<Badge
variant="secondary"
className="bg-red-100 text-red-800 border-red-300 dark:bg-red-900 dark:text-red-100 dark:border-red-700 whitespace-nowrap"
>
-{diffStats.totalDeletions}
</Badge>
)}
{diffStats.totalAdditions === 0 && diffStats.totalDeletions === 0 && (
<span className="text-muted-foreground">-</span>
)}
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Clock className="h-3 w-3 text-muted-foreground flex-shrink-0" />
<span className="text-sm text-muted-foreground whitespace-nowrap">
{formatDistanceToNow(new Date(event.created_at), {
addSuffix: true,
})}
</span>
</div>
</TableCell>
</ClickableTableRow>
)
})
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
{!isLoading && totalPages > 1 && (
<div className="flex items-center justify-between mt-6">
<p className="text-sm text-muted-foreground">
Showing {(filters.page - 1) * pageSize + 1} to{" "}
{Math.min(filters.page * pageSize, totalCount)} of {totalCount} events
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={filters.page === 1}
onClick={() => handlePageChange(filters.page - 1)}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum: number
if (totalPages <= 5) {
pageNum = i + 1
} else if (filters.page <= 3) {
pageNum = i + 1
} else if (filters.page >= totalPages - 2) {
pageNum = totalPages - 4 + i
} else {
pageNum = filters.page - 2 + i
}
return (
<Button
key={i}
variant={filters.page === pageNum ? "default" : "outline"}
size="sm"
className="w-8 h-8 p-0"
onClick={() => handlePageChange(pageNum)}
>
{pageNum}
</Button>
)
})}
{totalPages > 5 && filters.page < totalPages - 2 && <span className="px-2">...</span>}
{totalPages > 5 && filters.page < totalPages - 2 && (
<Button
variant="outline"
size="sm"
className="w-8 h-8 p-0"
onClick={() => handlePageChange(totalPages)}
>
{totalPages}
</Button>
)}
</div>
<Button
variant="outline"
size="sm"
disabled={filters.page === totalPages}
onClick={() => handlePageChange(filters.page + 1)}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</div>
)
}