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>
951 lines
32 KiB
TypeScript
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>
|
|
)
|
|
}
|