[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>
This commit is contained in:
parent
f37308fa79
commit
ac99349a92
2 changed files with 368 additions and 353 deletions
|
|
@ -45,139 +45,132 @@ export async function getAllOptimizationEvents({
|
|||
}
|
||||
|
||||
if (filter) {
|
||||
// Merge filter into where, excluding review_quality since it's in a different table
|
||||
Object.keys(filter).forEach(key => {
|
||||
// COMMENTED OUT: Repository and quality filtering
|
||||
// if (key === "repository_id") {
|
||||
// where[key] = filter[key]
|
||||
// } else if (key !== "review_quality") {
|
||||
// where[key] = filter[key]
|
||||
// }
|
||||
|
||||
// Only apply non-repository and non-quality filters
|
||||
if (key !== "repository_id" && key !== "review_quality") {
|
||||
if (key === "repository_id") {
|
||||
where.AND = where.AND || []
|
||||
where.AND.push({ [key]: filter[key] })
|
||||
} else if (key !== "review_quality") {
|
||||
where[key] = filter[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Check if we need to join with optimization_features for quality sorting/filtering
|
||||
// const needsOptimizationFeaturesJoin =
|
||||
// (sort && Object.keys(sort).some(k => k.toLowerCase() === "review_quality")) ||
|
||||
// (filter && Object.keys(filter).some(k => k.toLowerCase() === "review_quality"))
|
||||
|
||||
// Always use standard Prisma query (no quality joins)
|
||||
const needsOptimizationFeaturesJoin = false
|
||||
const needsOptimizationFeaturesJoin =
|
||||
(sort && Object.keys(sort).some(k => k.toLowerCase() === "review_quality")) ||
|
||||
(filter && Object.keys(filter).some(k => k.toLowerCase() === "review_quality"))
|
||||
|
||||
if (needsOptimizationFeaturesJoin) {
|
||||
// ENTIRE BLOCK COMMENTED OUT - This was for quality filtering/sorting
|
||||
// Build the WHERE clause for raw SQL
|
||||
// const whereConditions = []
|
||||
// const params: any[] = []
|
||||
// let paramIndex = 1
|
||||
// // Add is_staging condition
|
||||
// whereConditions.push(`oe.is_staging = true`)
|
||||
// // Add search conditions
|
||||
// if (search) {
|
||||
// whereConditions.push(
|
||||
// `(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex})`,
|
||||
// )
|
||||
// params.push(`%${search}%`)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// // Add filter conditions
|
||||
// if (filter) {
|
||||
// if (filter.status) {
|
||||
// whereConditions.push(`oe.status = $${paramIndex}`)
|
||||
// params.push(filter.status)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// if (filter.event_type) {
|
||||
// whereConditions.push(`oe.event_type = $${paramIndex}`)
|
||||
// params.push(filter.event_type)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// if (filter.review_quality) {
|
||||
// whereConditions.push(`of.review_quality = $${paramIndex}`)
|
||||
// params.push(filter.review_quality)
|
||||
// paramIndex += 1
|
||||
// }
|
||||
// if (filter.repository_id !== undefined) {
|
||||
// if (filter.repository_id === null) {
|
||||
// whereConditions.push(`oe.repository_id IS NULL`)
|
||||
// } else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
|
||||
// whereConditions.push(`oe.repository_id IS NOT NULL`)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const whereClause = whereConditions.join(" AND ")
|
||||
// // Build ORDER BY clause - handle all sort fields
|
||||
// const orderByClauses: string[] = []
|
||||
// if (sort && Object.keys(sort).length > 0) {
|
||||
// Object.entries(sort).forEach(([key, direction]) => {
|
||||
// const dir = direction.toUpperCase()
|
||||
// if (key.toLowerCase() === "review_quality") {
|
||||
// // Special handling for review_quality (case-insensitive)
|
||||
// orderByClauses.push(`
|
||||
// CASE
|
||||
// WHEN LOWER(of.review_quality) = 'high' THEN 3
|
||||
// WHEN LOWER(of.review_quality) = 'medium' THEN 2
|
||||
// WHEN LOWER(of.review_quality) = 'low' THEN 1
|
||||
// ELSE 0
|
||||
// END ${dir}
|
||||
// `)
|
||||
// } else {
|
||||
// // Regular field sorting
|
||||
// orderByClauses.push(`oe.${key} ${dir}`)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// // Add default sort by created_at only if sort is falsy
|
||||
// if (!sort) {
|
||||
// orderByClauses.push("oe.created_at DESC")
|
||||
// }
|
||||
// const orderByClause = orderByClauses.join(", ")
|
||||
// // Get events with custom ordering - join with optimization_features table
|
||||
// const events = await prisma.$queryRawUnsafe<any[]>(
|
||||
// `
|
||||
// SELECT
|
||||
// oe.*,
|
||||
// of.review_quality,
|
||||
// of.review_explanation
|
||||
// FROM optimization_events oe
|
||||
// LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
// WHERE ${whereClause}
|
||||
// ORDER BY ${orderByClause}
|
||||
// LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
// `,
|
||||
// ...params,
|
||||
// pageSize,
|
||||
// (page - 1) * pageSize,
|
||||
// )
|
||||
// // Get total count
|
||||
// const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>(
|
||||
// `
|
||||
// SELECT COUNT(*) as count
|
||||
// FROM optimization_events oe
|
||||
// LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
// WHERE ${whereClause}
|
||||
// `,
|
||||
// ...params,
|
||||
// )
|
||||
// const totalCount = Number(countResult[0].count)
|
||||
// // Fetch repository data for the events
|
||||
// const eventsWithRepo = await Promise.all(
|
||||
// events.map(async event => {
|
||||
// if (event.repository_id) {
|
||||
// const repository = await prisma.repositories.findUnique({
|
||||
// where: { id: event.repository_id },
|
||||
// })
|
||||
// return { ...event, repository }
|
||||
// }
|
||||
// return { ...event, repository: null }
|
||||
// }),
|
||||
// )
|
||||
// return { events: eventsWithRepo, totalCount }
|
||||
const whereConditions = []
|
||||
const params: any[] = []
|
||||
let paramIndex = 1
|
||||
whereConditions.push(`oe.is_staging = true`)
|
||||
if ("orgId" in payload) {
|
||||
whereConditions.push(`oe.repository_id IN (${repoIds.map(id => `'${id}'`).join(",")})`)
|
||||
} else {
|
||||
whereConditions.push(
|
||||
`(
|
||||
oe.repository_id IN (${repoIds.map(id => `'${id}'`).join(",")})
|
||||
OR oe.user_id = '${payload.userId}'
|
||||
OR oe.current_username = '${payload.username}'
|
||||
)`,
|
||||
)
|
||||
}
|
||||
// Add search conditions
|
||||
if (search) {
|
||||
whereConditions.push(
|
||||
`(oe.function_name ILIKE $${paramIndex} OR oe.file_path ILIKE $${paramIndex})`,
|
||||
)
|
||||
params.push(`%${search}%`)
|
||||
paramIndex += 1
|
||||
}
|
||||
// Add filter conditions
|
||||
if (filter) {
|
||||
if (filter.status) {
|
||||
whereConditions.push(`oe.status = $${paramIndex}`)
|
||||
params.push(filter.status)
|
||||
paramIndex += 1
|
||||
}
|
||||
if (filter.event_type) {
|
||||
whereConditions.push(`oe.event_type = $${paramIndex}`)
|
||||
params.push(filter.event_type)
|
||||
paramIndex += 1
|
||||
}
|
||||
if (filter.review_quality) {
|
||||
whereConditions.push(`of.review_quality = $${paramIndex}`)
|
||||
params.push(filter.review_quality)
|
||||
paramIndex += 1
|
||||
}
|
||||
if (filter.repository_id !== undefined) {
|
||||
if (filter.repository_id === null) {
|
||||
whereConditions.push(`oe.repository_id IS NULL`)
|
||||
} else if (filter.repository_id.not !== undefined && filter.repository_id.not === null) {
|
||||
whereConditions.push(`oe.repository_id IS NOT NULL`)
|
||||
}
|
||||
}
|
||||
}
|
||||
const whereClause = whereConditions.join(" AND ")
|
||||
const orderByClauses: string[] = []
|
||||
if (sort && Object.keys(sort).length > 0) {
|
||||
Object.entries(sort).forEach(([key, direction]) => {
|
||||
const dir = direction.toUpperCase()
|
||||
if (key.toLowerCase() === "review_quality") {
|
||||
orderByClauses.push(`
|
||||
CASE
|
||||
WHEN LOWER(of.review_quality) = 'high' THEN 3
|
||||
WHEN LOWER(of.review_quality) = 'medium' THEN 2
|
||||
WHEN LOWER(of.review_quality) = 'low' THEN 1
|
||||
ELSE 0
|
||||
END ${dir}
|
||||
`)
|
||||
} else {
|
||||
orderByClauses.push(`oe.${key} ${dir}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (!sort) {
|
||||
orderByClauses.push("oe.created_at DESC")
|
||||
}
|
||||
const orderByClause = orderByClauses.join(", ")
|
||||
const events = await prisma.$queryRawUnsafe<any[]>(
|
||||
`
|
||||
SELECT
|
||||
oe.*,
|
||||
of.review_quality,
|
||||
of.review_explanation
|
||||
FROM optimization_events oe
|
||||
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
WHERE ${whereClause}
|
||||
ORDER BY ${orderByClause}
|
||||
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
||||
`,
|
||||
...params,
|
||||
pageSize,
|
||||
(page - 1) * pageSize,
|
||||
)
|
||||
// Get total count
|
||||
const countResult = await prisma.$queryRawUnsafe<[{ count: bigint }]>(
|
||||
`
|
||||
SELECT COUNT(*) as count
|
||||
FROM optimization_events oe
|
||||
LEFT JOIN optimization_features of ON oe.trace_id = of.trace_id
|
||||
WHERE ${whereClause}
|
||||
`,
|
||||
...params,
|
||||
)
|
||||
const totalCount = Number(countResult[0].count)
|
||||
// Fetch repository data for the events
|
||||
const eventsWithRepo = await Promise.all(
|
||||
events.map(async event => {
|
||||
if (event.repository_id) {
|
||||
const repository = await prisma.repositories.findUnique({
|
||||
where: { id: event.repository_id },
|
||||
})
|
||||
return { ...event, repository }
|
||||
}
|
||||
return { ...event, repository: null }
|
||||
}),
|
||||
)
|
||||
return { events: eventsWithRepo, totalCount }
|
||||
} else {
|
||||
// Standard Prisma query with native orderBy
|
||||
const orderBy = sort || { created_at: "desc" }
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
"use client"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { useState, useEffect, useCallback, useRef } from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
|
|
@ -30,13 +30,12 @@ import {
|
|||
ArrowDown,
|
||||
} from "lucide-react"
|
||||
import { formatDistanceToNow } from "date-fns"
|
||||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
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"
|
||||
// COMMENTED OUT: Quality badge import
|
||||
import { ReviewQualityBadge } from "@/components/ui/quality_badge"
|
||||
|
||||
// Type definitions
|
||||
|
|
@ -70,6 +69,66 @@ interface OptimizationEvent {
|
|||
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 }>,
|
||||
|
|
@ -126,22 +185,6 @@ function calculateDiffStats(
|
|||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate coverage percentage
|
||||
function getCoverageInfo(diffContents: Record<string, { oldContent: string; newContent: string }>) {
|
||||
// This is a simplified coverage calculation
|
||||
// In practice, you'd want to integrate with actual coverage data
|
||||
const { totalAdditions, totalDeletions } = calculateDiffStats(diffContents)
|
||||
const totalLines = Object.values(diffContents).reduce((acc, { newContent }) => {
|
||||
return acc + newContent.split("\n").filter(line => line.trim() !== "").length
|
||||
}, 0)
|
||||
|
||||
// Simple heuristic: assume coverage decreases with more changes
|
||||
const changeRatio = (totalAdditions + totalDeletions) / Math.max(totalLines, 1)
|
||||
const coverage = Math.max(0.5, 1 - changeRatio * 0.3) // Keep between 50-100%
|
||||
|
||||
return Math.round(coverage * 100)
|
||||
}
|
||||
|
||||
// Client component for handling row clicks
|
||||
function ClickableTableRow({
|
||||
event,
|
||||
|
|
@ -152,13 +195,15 @@ function ClickableTableRow({
|
|||
children: React.ReactNode
|
||||
onRowClick: (eventId: string) => void
|
||||
}) {
|
||||
const handleRowClick = (e: React.MouseEvent) => {
|
||||
// Don't navigate if clicking on external link
|
||||
if ((e.target as HTMLElement).closest('a[href^="http"]')) {
|
||||
return
|
||||
}
|
||||
onRowClick(event.id)
|
||||
}
|
||||
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
|
||||
|
|
@ -175,37 +220,32 @@ export default function StagingPage() {
|
|||
const [userId, setUserId] = useState<string | null>(null)
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(true)
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [events, setEvents] = useState<OptimizationEvent[]>([])
|
||||
const [totalCount, setTotalCount] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Local page state instead of URL-based
|
||||
const [page, setPage] = useState(1)
|
||||
const search = searchParams.get("search") || ""
|
||||
// COMMENTED OUT: Repository filter from URL
|
||||
// const hasRepo = searchParams.get("hasRepo") || "all"
|
||||
const status = searchParams.get("status") || "all"
|
||||
const eventType = searchParams.get("eventType") || "all"
|
||||
// COMMENTED OUT: Quality filter from URL
|
||||
// const reviewQuality = searchParams.get("reviewQuality") || "all"
|
||||
const sortBy = searchParams.get("sortBy") || "created_at_desc"
|
||||
const pageSize = 10
|
||||
|
||||
// Local state for filter inputs
|
||||
const [searchInput, setSearchInput] = useState(search)
|
||||
// COMMENTED OUT: Repository filter state
|
||||
// const [hasRepoFilter, setHasRepoFilter] = useState(hasRepo)
|
||||
const [statusFilter, setStatusFilter] = useState(status)
|
||||
const [eventTypeFilter, setEventTypeFilter] = useState(eventType)
|
||||
// COMMENTED OUT: Quality filter state
|
||||
// const [reviewQualityFilter, setReviewQualityFilter] = useState(reviewQuality)
|
||||
const [sortByFilter, setSortByFilter] = useState(sortBy)
|
||||
const { currentOrg } = useViewMode()
|
||||
|
||||
// Load user ID on mount
|
||||
// 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 {
|
||||
|
|
@ -221,6 +261,7 @@ export default function StagingPage() {
|
|||
loadUserId()
|
||||
}, [])
|
||||
|
||||
// Memoized load events function
|
||||
const loadEvents = useCallback(async () => {
|
||||
if (!userId) return
|
||||
|
||||
|
|
@ -230,28 +271,26 @@ export default function StagingPage() {
|
|||
try {
|
||||
const filter: Record<string, any> = {}
|
||||
|
||||
// COMMENTED OUT: Repository filter logic
|
||||
// if (hasRepo === "yes") {
|
||||
// filter.repository_id = { not: null }
|
||||
// } else if (hasRepo === "no") {
|
||||
// filter.repository_id = null
|
||||
// }
|
||||
|
||||
if (status !== "all") {
|
||||
filter.status = status
|
||||
if (filters.hasRepo === "yes") {
|
||||
filter.repository_id = { not: null }
|
||||
} else if (filters.hasRepo === "no") {
|
||||
filter.repository_id = null
|
||||
}
|
||||
|
||||
if (eventType !== "all") {
|
||||
filter.event_type = eventType
|
||||
if (filters.status !== "all") {
|
||||
filter.status = filters.status
|
||||
}
|
||||
|
||||
// COMMENTED OUT: Quality filter logic
|
||||
// if (reviewQuality !== "all") {
|
||||
// filter.review_quality = reviewQuality
|
||||
// }
|
||||
if (filters.eventType !== "all") {
|
||||
filter.event_type = filters.eventType
|
||||
}
|
||||
|
||||
if (filters.reviewQuality !== "all") {
|
||||
filter.review_quality = filters.reviewQuality
|
||||
}
|
||||
|
||||
// Parse sort parameter
|
||||
const [sortField, sortDirection] = sortBy.split("_").reduce(
|
||||
const [sortField, sortDirection] = filters.sortBy.split("_").reduce(
|
||||
(acc, part, index, arr) => {
|
||||
if (index === arr.length - 1 && (part === "asc" || part === "desc")) {
|
||||
return [acc[0], part]
|
||||
|
|
@ -264,15 +303,16 @@ export default function StagingPage() {
|
|||
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,
|
||||
search: filters.search,
|
||||
filter,
|
||||
sort,
|
||||
page,
|
||||
page: filters.page,
|
||||
pageSize,
|
||||
})
|
||||
|
||||
|
|
@ -295,105 +335,116 @@ export default function StagingPage() {
|
|||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [
|
||||
userId,
|
||||
search,
|
||||
// COMMENTED OUT: hasRepo dependency
|
||||
// hasRepo,
|
||||
status,
|
||||
eventType,
|
||||
// COMMENTED OUT: reviewQuality dependency
|
||||
// reviewQuality,
|
||||
sortBy,
|
||||
page,
|
||||
pageSize,
|
||||
currentOrg,
|
||||
])
|
||||
}, [userId, filters, currentOrg, pageSize])
|
||||
|
||||
// Load events when filters change - with debounce for search
|
||||
useEffect(() => {
|
||||
if (!isLoadingUser && userId) {
|
||||
loadEvents()
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}, [userId, isLoadingUser, loadEvents])
|
||||
|
||||
const handleRowClick = (traceId: string) => {
|
||||
router.push(`/review-optimizations/${traceId}`)
|
||||
}
|
||||
return () => {
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current)
|
||||
}
|
||||
}
|
||||
}, [userId, isLoadingUser, filters, loadEvents])
|
||||
|
||||
// Update URL when filters change
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
const params = new URLSearchParams()
|
||||
if (searchInput) params.set("search", searchInput)
|
||||
// COMMENTED OUT: Repository filter param
|
||||
// if (hasRepoFilter !== "all") params.set("hasRepo", hasRepoFilter)
|
||||
if (statusFilter !== "all") params.set("status", statusFilter)
|
||||
if (eventTypeFilter !== "all") params.set("eventType", eventTypeFilter)
|
||||
// COMMENTED OUT: Quality filter param
|
||||
// if (reviewQualityFilter !== "all") params.set("reviewQuality", reviewQualityFilter)
|
||||
if (sortByFilter !== "created_at_desc") params.set("sortBy", sortByFilter)
|
||||
const handleRowClick = useCallback(
|
||||
(traceId: string) => {
|
||||
router.push(`/review-optimizations/${traceId}`)
|
||||
},
|
||||
[router],
|
||||
)
|
||||
|
||||
// Reset to page 1 when filters change
|
||||
setPage(1)
|
||||
router.push(`?${params.toString()}`)
|
||||
}, 300) // 300ms debounce
|
||||
// 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 }),
|
||||
}))
|
||||
}, [])
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [
|
||||
searchInput,
|
||||
// COMMENTED OUT: hasRepoFilter dependency
|
||||
// hasRepoFilter,
|
||||
statusFilter,
|
||||
eventTypeFilter,
|
||||
// COMMENTED OUT: reviewQualityFilter dependency
|
||||
// reviewQualityFilter,
|
||||
sortByFilter,
|
||||
router,
|
||||
])
|
||||
|
||||
const clearFilters = () => {
|
||||
setSearchInput("")
|
||||
// COMMENTED OUT: Reset repository filter
|
||||
// setHasRepoFilter("all")
|
||||
setStatusFilter("all")
|
||||
setEventTypeFilter("all")
|
||||
// COMMENTED OUT: Reset quality filter
|
||||
// setReviewQualityFilter("all")
|
||||
setSortByFilter("created_at_desc")
|
||||
setPage(1)
|
||||
router.push("/review-optimizations")
|
||||
}
|
||||
const clearFilters = useCallback(() => {
|
||||
setFilters({
|
||||
search: "",
|
||||
hasRepo: "all",
|
||||
status: "all",
|
||||
eventType: "all",
|
||||
reviewQuality: "all",
|
||||
sortBy: "created_at_desc",
|
||||
page: 1,
|
||||
})
|
||||
}, [])
|
||||
|
||||
const hasActiveFilters =
|
||||
searchInput ||
|
||||
// COMMENTED OUT: hasRepoFilter check
|
||||
// hasRepoFilter !== "all" ||
|
||||
statusFilter !== "all" ||
|
||||
eventTypeFilter !== "all" ||
|
||||
// COMMENTED OUT: reviewQualityFilter check
|
||||
// reviewQualityFilter !== "all" ||
|
||||
sortByFilter !== "created_at_desc"
|
||||
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 = (newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
setPage(newPage)
|
||||
}
|
||||
}
|
||||
const handlePageChange = useCallback(
|
||||
(newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages) {
|
||||
updateFilter("page", newPage)
|
||||
}
|
||||
},
|
||||
[totalPages, updateFilter],
|
||||
)
|
||||
|
||||
const getSortIcon = (field: string) => {
|
||||
if (sortByFilter.startsWith(field)) {
|
||||
return sortByFilter.endsWith("_asc") ? (
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
)
|
||||
}
|
||||
return <ArrowUpDown className="h-4 w-4 opacity-50" />
|
||||
}
|
||||
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 getSpeedupBadge = (speedup?: number, speedupPct?: number) => {
|
||||
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)
|
||||
|
|
@ -423,9 +474,9 @@ export default function StagingPage() {
|
|||
{speedup.toFixed(2)}x ({speedupPct.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")}%)
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getStatusBadge = (status?: string) => {
|
||||
const getStatusBadge = useCallback((status?: string) => {
|
||||
if (!status) return null
|
||||
|
||||
const variants: Record<string, { className: string; label: string }> = {
|
||||
|
|
@ -451,9 +502,9 @@ export default function StagingPage() {
|
|||
{variant.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const getEventTypeBadge = (eventType?: string) => {
|
||||
const getEventTypeBadge = useCallback((eventType?: string) => {
|
||||
if (!eventType) return null
|
||||
|
||||
const variants: Record<string, { className: string; label: string }> = {
|
||||
|
|
@ -489,7 +540,7 @@ export default function StagingPage() {
|
|||
{variant.label}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLoadingUser) {
|
||||
return (
|
||||
|
|
@ -523,8 +574,8 @@ export default function StagingPage() {
|
|||
<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={searchInput}
|
||||
onChange={e => setSearchInput(e.target.value)}
|
||||
value={filters.search}
|
||||
onChange={e => updateFilter("search", e.target.value)}
|
||||
className="pl-10 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -536,19 +587,18 @@ export default function StagingPage() {
|
|||
</div>
|
||||
|
||||
{/* Filter Controls */}
|
||||
|
||||
{/* <Select value={hasRepoFilter} onValueChange={setHasRepoFilter}>
|
||||
<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">Has Repository</SelectItem>
|
||||
<SelectItem value="no">No Repository</SelectItem>
|
||||
<SelectItem value="yes">With Repository</SelectItem>
|
||||
<SelectItem value="no">Without Repository</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
</Select>
|
||||
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<Select value={filters.status} onValueChange={value => updateFilter("status", value)}>
|
||||
<SelectTrigger className="w-[120px] sm:w-[150px]">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -559,7 +609,10 @@ export default function StagingPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={eventTypeFilter} onValueChange={setEventTypeFilter}>
|
||||
<Select
|
||||
value={filters.eventType}
|
||||
onValueChange={value => updateFilter("eventType", value)}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] sm:w-[150px]">
|
||||
<SelectValue placeholder="Event Type" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -572,20 +625,22 @@ export default function StagingPage() {
|
|||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* COMMENTED OUT: Quality Filter */}
|
||||
{/* <Select value={reviewQualityFilter} onValueChange={setReviewQualityFilter}>
|
||||
<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 qualities</SelectItem>
|
||||
<SelectItem value="all">All Quality</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
</Select>
|
||||
|
||||
<Select value={sortByFilter} onValueChange={setSortByFilter}>
|
||||
<Select value={filters.sortBy} onValueChange={value => updateFilter("sortBy", value)}>
|
||||
<SelectTrigger className="w-[140px] sm:w-[200px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
|
|
@ -594,9 +649,8 @@ export default function StagingPage() {
|
|||
<SelectItem value="created_at_asc">Oldest</SelectItem>
|
||||
<SelectItem value="speedup_x_desc">Speedup (Highest)</SelectItem>
|
||||
<SelectItem value="speedup_x_asc">Speedup (Lowest)</SelectItem>
|
||||
{/* COMMENTED OUT: Quality sort options */}
|
||||
{/* <SelectItem value="review_quality_desc">Quality (High to Low)</SelectItem>
|
||||
<SelectItem value="review_quality_asc">Quality (Low to High)</SelectItem> */}
|
||||
<SelectItem value="review_quality_desc">Quality (High to Low)</SelectItem>
|
||||
<SelectItem value="review_quality_asc">Quality (Low to High)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
|
|
@ -638,33 +692,18 @@ export default function StagingPage() {
|
|||
<TableHead className="w-[18%]">REPOSITORY</TableHead>
|
||||
<TableHead className="text-center">REVIEW</TableHead>
|
||||
<TableHead className="text-center">STATUS</TableHead>
|
||||
{/* COMMENTED OUT: Quality column header with sorting */}
|
||||
<TableHead
|
||||
className="text-center cursor-pointer hover:bg-muted/50"
|
||||
// onClick={() => {
|
||||
// const newSort = sortByFilter.startsWith("review_quality")
|
||||
// ? sortByFilter === "review_quality_desc"
|
||||
// ? "review_quality_asc"
|
||||
// : "review_quality_desc"
|
||||
// : "review_quality_desc"
|
||||
// setSortByFilter(newSort)
|
||||
// }}
|
||||
onClick={() => toggleSort("review_quality")}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span>QUALITY</span>
|
||||
{/* {getSortIcon("review_quality")} */}
|
||||
{getSortIcon("review_quality")}
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead
|
||||
className="text-center cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
const newSort = sortByFilter.startsWith("speedup_x")
|
||||
? sortByFilter === "speedup_x_desc"
|
||||
? "speedup_x_asc"
|
||||
: "speedup_x_desc"
|
||||
: "speedup_x_desc"
|
||||
setSortByFilter(newSort)
|
||||
}}
|
||||
onClick={() => toggleSort("speedup_x")}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span>SPEEDUP</span>
|
||||
|
|
@ -674,14 +713,7 @@ export default function StagingPage() {
|
|||
<TableHead className="text-center">CHANGES</TableHead>
|
||||
<TableHead
|
||||
className="text-right cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => {
|
||||
const newSort = sortByFilter.startsWith("created_at")
|
||||
? sortByFilter === "created_at_desc"
|
||||
? "created_at_asc"
|
||||
: "created_at_desc"
|
||||
: "created_at_desc"
|
||||
setSortByFilter(newSort)
|
||||
}}
|
||||
onClick={() => toggleSort("created_at")}
|
||||
>
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<span>CREATED AT</span>
|
||||
|
|
@ -691,7 +723,9 @@ export default function StagingPage() {
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{events.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<TableSkeleton />
|
||||
) : events.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-12">
|
||||
<div className="text-muted-foreground">
|
||||
|
|
@ -708,7 +742,7 @@ export default function StagingPage() {
|
|||
totalAdditions: 0,
|
||||
totalDeletions: 0,
|
||||
}
|
||||
let coverage: number | null = null
|
||||
|
||||
if (
|
||||
event.metadata &&
|
||||
typeof event.metadata === "object" &&
|
||||
|
|
@ -717,7 +751,6 @@ export default function StagingPage() {
|
|||
event.metadata.diffContents !== null
|
||||
) {
|
||||
const diffContentsRaw = event.metadata.diffContents
|
||||
// Check that every value is an object with oldContent and newContent as strings
|
||||
if (diffContentsRaw && typeof diffContentsRaw === "object") {
|
||||
let valid = true
|
||||
for (const value of Object.values(diffContentsRaw as object)) {
|
||||
|
|
@ -737,19 +770,12 @@ export default function StagingPage() {
|
|||
{ oldContent: string; newContent: string }
|
||||
>
|
||||
diffStats = calculateDiffStats(diffContents)
|
||||
coverage = getCoverageInfo(diffContents)
|
||||
}
|
||||
}
|
||||
}
|
||||
// fallback for coverage if not available
|
||||
if (coverage === null) coverage = 100
|
||||
|
||||
return (
|
||||
<ClickableTableRow
|
||||
key={event.id}
|
||||
event={event}
|
||||
onRowClick={() => handleRowClick(event.trace_id)}
|
||||
>
|
||||
<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" />
|
||||
|
|
@ -767,7 +793,6 @@ export default function StagingPage() {
|
|||
<div className="flex items-center gap-3">
|
||||
{event.repository ? (
|
||||
<>
|
||||
{/* Repository image */}
|
||||
<div className="relative h-8 w-8 flex-shrink-0">
|
||||
{event.repository.full_name && (
|
||||
<Image
|
||||
|
|
@ -777,13 +802,10 @@ export default function StagingPage() {
|
|||
className="rounded-full object-cover"
|
||||
onError={e => {
|
||||
e.currentTarget.style.display = "none"
|
||||
e.currentTarget.nextElementSibling?.classList.remove("hidden")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Repository name */}
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm font-medium truncate">
|
||||
|
|
@ -794,7 +816,6 @@ export default function StagingPage() {
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Placeholder for untracked optimization */}
|
||||
<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>
|
||||
|
|
@ -856,18 +877,19 @@ export default function StagingPage() {
|
|||
</Table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
{/* Pagination */}
|
||||
{!isLoading && totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Showing {(page - 1) * pageSize + 1} to {Math.min(page * pageSize, totalCount)} of{" "}
|
||||
{totalCount} events
|
||||
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={page === 1}
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
disabled={filters.page === 1}
|
||||
onClick={() => handlePageChange(filters.page - 1)}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
Previous
|
||||
|
|
@ -879,18 +901,18 @@ export default function StagingPage() {
|
|||
|
||||
if (totalPages <= 5) {
|
||||
pageNum = i + 1
|
||||
} else if (page <= 3) {
|
||||
} else if (filters.page <= 3) {
|
||||
pageNum = i + 1
|
||||
} else if (page >= totalPages - 2) {
|
||||
} else if (filters.page >= totalPages - 2) {
|
||||
pageNum = totalPages - 4 + i
|
||||
} else {
|
||||
pageNum = page - 2 + i
|
||||
pageNum = filters.page - 2 + i
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={i}
|
||||
variant={page === pageNum ? "default" : "outline"}
|
||||
variant={filters.page === pageNum ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="w-8 h-8 p-0"
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
|
|
@ -899,8 +921,8 @@ export default function StagingPage() {
|
|||
</Button>
|
||||
)
|
||||
})}
|
||||
{totalPages > 5 && page < totalPages - 2 && <span className="px-2">...</span>}
|
||||
{totalPages > 5 && page < totalPages - 2 && (
|
||||
{totalPages > 5 && filters.page < totalPages - 2 && <span className="px-2">...</span>}
|
||||
{totalPages > 5 && filters.page < totalPages - 2 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
@ -915,8 +937,8 @@ export default function StagingPage() {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page === totalPages}
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
disabled={filters.page === totalPages}
|
||||
onClick={() => handlePageChange(filters.page + 1)}
|
||||
>
|
||||
Next
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
|
|
|
|||
Loading…
Reference in a new issue