From aa743407065d0d5f6f429f26353b9761ce6d07dc Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Fri, 2 Jan 2026 16:07:07 +0000 Subject: [PATCH] Implement pagination and server-side search for detection records Update client-side to handle pagination and debounced search input, refactor server API routes and storage to support offset, limit, and search queries on detection records. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 0ad70992-7e31-48d6-8e52-b2442cc2a623 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/C6BdLIt --- client/src/pages/Detections.tsx | 151 +++++++++++++++++++++++++------- server/routes.ts | 23 +++-- server/storage.ts | 47 +++++++--- 3 files changed, 167 insertions(+), 54 deletions(-) diff --git a/client/src/pages/Detections.tsx b/client/src/pages/Detections.tsx index 2c1efae..5989974 100644 --- a/client/src/pages/Detections.tsx +++ b/client/src/pages/Detections.tsx @@ -5,40 +5,74 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; -import { AlertTriangle, Search, Shield, Globe, MapPin, Building2, ShieldPlus, ShieldCheck, Unlock } from "lucide-react"; +import { AlertTriangle, Search, Shield, Globe, MapPin, Building2, ShieldPlus, ShieldCheck, Unlock, ChevronLeft, ChevronRight } from "lucide-react"; import { format } from "date-fns"; -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import type { Detection, Whitelist } from "@shared/schema"; import { getFlag } from "@/lib/country-flags"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { useToast } from "@/hooks/use-toast"; +const ITEMS_PER_PAGE = 50; + +interface DetectionsResponse { + detections: Detection[]; + total: number; +} + export default function Detections() { - const [searchQuery, setSearchQuery] = useState(""); + const [searchInput, setSearchInput] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); const [anomalyTypeFilter, setAnomalyTypeFilter] = useState("all"); const [minScore, setMinScore] = useState(0); const [maxScore, setMaxScore] = useState(100); + const [currentPage, setCurrentPage] = useState(1); const { toast } = useToast(); - // Build query params - const queryParams = new URLSearchParams(); - queryParams.set("limit", "5000"); - if (anomalyTypeFilter !== "all") { - queryParams.set("anomalyType", anomalyTypeFilter); - } - if (minScore > 0) { - queryParams.set("minScore", minScore.toString()); - } - if (maxScore < 100) { - queryParams.set("maxScore", maxScore.toString()); - } + // Debounce search input + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(searchInput); + setCurrentPage(1); // Reset to first page on search + }, 300); + return () => clearTimeout(timer); + }, [searchInput]); - const { data: detections, isLoading } = useQuery({ - queryKey: ["/api/detections", anomalyTypeFilter, minScore, maxScore], - queryFn: () => fetch(`/api/detections?${queryParams.toString()}`).then(r => r.json()), - refetchInterval: 5000, + // Reset page on filter change + useEffect(() => { + setCurrentPage(1); + }, [anomalyTypeFilter, minScore, maxScore]); + + // Build query params with pagination and search + const queryParams = useMemo(() => { + const params = new URLSearchParams(); + params.set("limit", ITEMS_PER_PAGE.toString()); + params.set("offset", ((currentPage - 1) * ITEMS_PER_PAGE).toString()); + if (anomalyTypeFilter !== "all") { + params.set("anomalyType", anomalyTypeFilter); + } + if (minScore > 0) { + params.set("minScore", minScore.toString()); + } + if (maxScore < 100) { + params.set("maxScore", maxScore.toString()); + } + if (debouncedSearch.trim()) { + params.set("search", debouncedSearch.trim()); + } + return params.toString(); + }, [currentPage, anomalyTypeFilter, minScore, maxScore, debouncedSearch]); + + const { data, isLoading } = useQuery({ + queryKey: ["/api/detections", currentPage, anomalyTypeFilter, minScore, maxScore, debouncedSearch], + queryFn: () => fetch(`/api/detections?${queryParams}`).then(r => r.json()), + refetchInterval: 10000, }); + const detections = data?.detections || []; + const totalCount = data?.total || 0; + const totalPages = Math.ceil(totalCount / ITEMS_PER_PAGE); + // Fetch whitelist to check if IP is already whitelisted const { data: whitelistData } = useQuery({ queryKey: ["/api/whitelist"], @@ -47,11 +81,6 @@ export default function Detections() { // Create a Set of whitelisted IPs for fast lookup const whitelistedIps = new Set(whitelistData?.map(w => w.ipAddress) || []); - const filteredDetections = detections?.filter((d) => - d.sourceIp.toLowerCase().includes(searchQuery.toLowerCase()) || - d.anomalyType.toLowerCase().includes(searchQuery.toLowerCase()) - ); - // Mutation per aggiungere a whitelist const addToWhitelistMutation = useMutation({ mutationFn: async (detection: Detection) => { @@ -137,9 +166,9 @@ export default function Detections() {
setSearchQuery(e.target.value)} + placeholder="Cerca per IP, paese, organizzazione..." + value={searchInput} + onChange={(e) => setSearchInput(e.target.value)} className="pl-9" data-testid="input-search" /> @@ -191,9 +220,36 @@ export default function Detections() { {/* Detections List */} - - - Rilevamenti ({filteredDetections?.length || 0}) + +
+ + Rilevamenti ({totalCount}) +
+ {totalPages > 1 && ( +
+ + + Pagina {currentPage} di {totalPages} + + +
+ )}
@@ -201,9 +257,9 @@ export default function Detections() {
Caricamento...
- ) : filteredDetections && filteredDetections.length > 0 ? ( + ) : detections.length > 0 ? (
- {filteredDetections.map((detection) => ( + {detections.map((detection) => (

Nessun rilevamento trovato

- {searchQuery && ( + {debouncedSearch && (

Prova con un altro termine di ricerca

)}
)} + + {/* Bottom pagination */} + {totalPages > 1 && detections.length > 0 && ( +
+ + + Pagina {currentPage} di {totalPages} ({totalCount} totali) + + +
+ )}
diff --git a/server/routes.ts b/server/routes.ts index a41ec82..b2d795a 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -77,18 +77,22 @@ export async function registerRoutes(app: Express): Promise { // Detections app.get("/api/detections", async (req, res) => { try { - const limit = req.query.limit ? parseInt(req.query.limit as string) : 500; + const limit = req.query.limit ? parseInt(req.query.limit as string) : 50; + const offset = req.query.offset ? parseInt(req.query.offset as string) : 0; const anomalyType = req.query.anomalyType as string | undefined; const minScore = req.query.minScore ? parseFloat(req.query.minScore as string) : undefined; const maxScore = req.query.maxScore ? parseFloat(req.query.maxScore as string) : undefined; + const search = req.query.search as string | undefined; - const detections = await storage.getAllDetections({ + const result = await storage.getAllDetections({ limit, + offset, anomalyType, minScore, - maxScore + maxScore, + search }); - res.json(detections); + res.json(result); } catch (error) { console.error('[DB ERROR] Failed to fetch detections:', error); res.status(500).json({ error: "Failed to fetch detections" }); @@ -474,14 +478,15 @@ export async function registerRoutes(app: Express): Promise { app.get("/api/stats", async (req, res) => { try { const routers = await storage.getAllRouters(); - const detections = await storage.getAllDetections({ limit: 1000 }); + const detectionsResult = await storage.getAllDetections({ limit: 1000 }); const recentLogs = await storage.getRecentLogs(1000); const whitelist = await storage.getAllWhitelist(); const latestTraining = await storage.getLatestTraining(); - const blockedCount = detections.filter(d => d.blocked).length; - const criticalCount = detections.filter(d => parseFloat(d.riskScore) >= 85).length; - const highCount = detections.filter(d => parseFloat(d.riskScore) >= 70 && parseFloat(d.riskScore) < 85).length; + const detectionsList = detectionsResult.detections; + const blockedCount = detectionsList.filter(d => d.blocked).length; + const criticalCount = detectionsList.filter(d => parseFloat(d.riskScore) >= 85).length; + const highCount = detectionsList.filter(d => parseFloat(d.riskScore) >= 70 && parseFloat(d.riskScore) < 85).length; res.json({ routers: { @@ -489,7 +494,7 @@ export async function registerRoutes(app: Express): Promise { enabled: routers.filter(r => r.enabled).length }, detections: { - total: detections.length, + total: detectionsResult.total, blocked: blockedCount, critical: criticalCount, high: highCount diff --git a/server/storage.ts b/server/storage.ts index 57217f6..c894843 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -43,10 +43,12 @@ export interface IStorage { // Detections getAllDetections(options: { limit?: number; + offset?: number; anomalyType?: string; minScore?: number; maxScore?: number; - }): Promise; + search?: string; + }): Promise<{ detections: Detection[]; total: number }>; getDetectionByIp(sourceIp: string): Promise; createDetection(detection: InsertDetection): Promise; updateDetection(id: string, detection: Partial): Promise; @@ -174,11 +176,13 @@ export class DatabaseStorage implements IStorage { // Detections async getAllDetections(options: { limit?: number; + offset?: number; anomalyType?: string; minScore?: number; maxScore?: number; - }): Promise { - const { limit = 5000, anomalyType, minScore, maxScore } = options; + search?: string; + }): Promise<{ detections: Detection[]; total: number }> { + const { limit = 50, offset = 0, anomalyType, minScore, maxScore, search } = options; // Build WHERE conditions const conditions = []; @@ -196,17 +200,36 @@ export class DatabaseStorage implements IStorage { conditions.push(sql`${detections.riskScore}::numeric <= ${maxScore}`); } - const query = db - .select() - .from(detections) - .orderBy(desc(detections.detectedAt)) - .limit(limit); - - if (conditions.length > 0) { - return await query.where(and(...conditions)); + // Search by IP or anomaly type (case-insensitive) + if (search && search.trim()) { + const searchLower = search.trim().toLowerCase(); + conditions.push(sql`( + LOWER(${detections.sourceIp}) LIKE ${'%' + searchLower + '%'} OR + LOWER(${detections.anomalyType}) LIKE ${'%' + searchLower + '%'} OR + LOWER(COALESCE(${detections.country}, '')) LIKE ${'%' + searchLower + '%'} OR + LOWER(COALESCE(${detections.organization}, '')) LIKE ${'%' + searchLower + '%'} + )`); } - return await query; + const whereClause = conditions.length > 0 ? and(...conditions) : undefined; + + // Get total count for pagination + const countResult = await db + .select({ count: sql`count(*)::int` }) + .from(detections) + .where(whereClause); + const total = countResult[0]?.count || 0; + + // Get paginated results + const results = await db + .select() + .from(detections) + .where(whereClause) + .orderBy(desc(detections.detectedAt)) + .limit(limit) + .offset(offset); + + return { detections: results, total }; } async getDetectionByIp(sourceIp: string): Promise {