From dd0e44f78f96d7498a39d5aeb3ec75f9f683aab8 Mon Sep 17 00:00:00 2001 From: marco370 <48531002-marco370@users.noreply.replit.com> Date: Sat, 14 Feb 2026 10:47:35 +0000 Subject: [PATCH] Add pagination and server-side search to the whitelist page Implement server-side pagination and search functionality for the whitelist page, including API route updates, storage layer modifications, and frontend enhancements in `Whitelist.tsx`. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 7a657272-55ba-4a79-9a2e-f1ed9bc7a528 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1971ce5e-1b30-49d5-90b7-63e075ccb563 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/449cf7c4-c97a-45ae-8234-e5c5b8d6a84f/7a657272-55ba-4a79-9a2e-f1ed9bc7a528/RyXWGQA --- client/src/pages/Whitelist.tsx | 126 +++++++++++++++++++++++++++------ server/routes.ts | 12 ++-- server/storage.ts | 38 ++++++++-- 3 files changed, 147 insertions(+), 29 deletions(-) diff --git a/client/src/pages/Whitelist.tsx b/client/src/pages/Whitelist.tsx index e889a57..f49c390 100644 --- a/client/src/pages/Whitelist.tsx +++ b/client/src/pages/Whitelist.tsx @@ -2,9 +2,9 @@ import { useQuery, useMutation } from "@tanstack/react-query"; import { queryClient, apiRequest } from "@/lib/queryClient"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Shield, Plus, Trash2, CheckCircle2, XCircle, Search } from "lucide-react"; +import { Shield, Plus, Trash2, CheckCircle2, XCircle, Search, ChevronLeft, ChevronRight } from "lucide-react"; import { format } from "date-fns"; -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -31,6 +31,8 @@ import { import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; +const ITEMS_PER_PAGE = 50; + const whitelistFormSchema = insertWhitelistSchema.extend({ ipAddress: z.string() .min(7, "Inserisci un IP valido") @@ -41,10 +43,17 @@ const whitelistFormSchema = insertWhitelistSchema.extend({ }, "Ogni ottetto deve essere tra 0 e 255"), }); +interface WhitelistResponse { + items: Whitelist[]; + total: number; +} + export default function WhitelistPage() { const { toast } = useToast(); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); - const [searchQuery, setSearchQuery] = useState(""); + const [searchInput, setSearchInput] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const [currentPage, setCurrentPage] = useState(1); const form = useForm>({ resolver: zodResolver(whitelistFormSchema), @@ -56,16 +65,33 @@ export default function WhitelistPage() { }, }); - const { data: whitelist, isLoading } = useQuery({ - queryKey: ["/api/whitelist"], + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedSearch(searchInput); + setCurrentPage(1); + }, 300); + return () => clearTimeout(timer); + }, [searchInput]); + + const queryParams = useMemo(() => { + const params = new URLSearchParams(); + params.set("limit", ITEMS_PER_PAGE.toString()); + params.set("offset", ((currentPage - 1) * ITEMS_PER_PAGE).toString()); + if (debouncedSearch.trim()) { + params.set("search", debouncedSearch.trim()); + } + return params.toString(); + }, [currentPage, debouncedSearch]); + + const { data, isLoading } = useQuery({ + queryKey: ["/api/whitelist", currentPage, debouncedSearch], + queryFn: () => fetch(`/api/whitelist?${queryParams}`).then(r => r.json()), + refetchInterval: 10000, }); - // Filter whitelist based on search query - const filteredWhitelist = whitelist?.filter((item) => - item.ipAddress.toLowerCase().includes(searchQuery.toLowerCase()) || - item.reason?.toLowerCase().includes(searchQuery.toLowerCase()) || - item.comment?.toLowerCase().includes(searchQuery.toLowerCase()) - ); + const whitelistItems = data?.items || []; + const totalCount = data?.total || 0; + const totalPages = Math.ceil(totalCount / ITEMS_PER_PAGE); const addMutation = useMutation({ mutationFn: async (data: z.infer) => { @@ -203,9 +229,9 @@ export default function WhitelistPage() {
setSearchQuery(e.target.value)} + placeholder="Cerca per IP, motivo, note o sorgente..." + value={searchInput} + onChange={(e) => setSearchInput(e.target.value)} className="pl-9" data-testid="input-search-whitelist" /> @@ -215,9 +241,36 @@ export default function WhitelistPage() { - - - IP Protetti ({filteredWhitelist?.length || 0}{searchQuery && whitelist ? ` di ${whitelist.length}` : ''}) + +
+ + IP Protetti ({totalCount}) +
+ {totalPages > 1 && ( +
+ + + Pagina {currentPage} di {totalPages} + + +
+ )}
@@ -225,9 +278,9 @@ export default function WhitelistPage() {
Caricamento...
- ) : filteredWhitelist && filteredWhitelist.length > 0 ? ( + ) : whitelistItems.length > 0 ? (
- {filteredWhitelist.map((item) => ( + {whitelistItems.map((item) => (
))} + + {/* Bottom pagination */} + {totalPages > 1 && ( +
+ + + Pagina {currentPage} di {totalPages} ({totalCount} totali) + + +
+ )}
) : (

Nessun IP in whitelist

-

Aggiungi indirizzi IP fidati per proteggerli dal blocco automatico

+ {debouncedSearch ? ( +

Prova con un altro termine di ricerca

+ ) : ( +

Aggiungi indirizzi IP fidati per proteggerli dal blocco automatico

+ )}
)}
diff --git a/server/routes.ts b/server/routes.ts index 9973681..5839b43 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -122,8 +122,12 @@ export async function registerRoutes(app: Express): Promise { // Whitelist app.get("/api/whitelist", async (req, res) => { try { - const whitelist = await storage.getAllWhitelist(); - res.json(whitelist); + const limit = parseInt(req.query.limit as string) || 50; + const offset = parseInt(req.query.offset as string) || 0; + const search = req.query.search as string || undefined; + + const result = await storage.getAllWhitelist({ limit, offset, search }); + res.json(result); } catch (error) { console.error('[DB ERROR] Failed to fetch whitelist:', error); res.status(500).json({ error: "Failed to fetch whitelist" }); @@ -480,7 +484,7 @@ export async function registerRoutes(app: Express): Promise { const routers = await storage.getAllRouters(); const detectionsResult = await storage.getAllDetections({ limit: 1000 }); const recentLogs = await storage.getRecentLogs(1000); - const whitelist = await storage.getAllWhitelist(); + const whitelistResult = await storage.getAllWhitelist({ limit: 1 }); const latestTraining = await storage.getLatestTraining(); const detectionsList = detectionsResult.detections; @@ -503,7 +507,7 @@ export async function registerRoutes(app: Express): Promise { recent: recentLogs.length }, whitelist: { - total: whitelist.length + total: whitelistResult.total }, latestTraining: latestTraining }); diff --git a/server/storage.ts b/server/storage.ts index c894843..54520b8 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -55,7 +55,7 @@ export interface IStorage { getUnblockedDetections(): Promise; // Whitelist - getAllWhitelist(): Promise; + getAllWhitelist(options?: { limit?: number; offset?: number; search?: string }): Promise<{ items: Whitelist[]; total: number }>; getWhitelistByIp(ipAddress: string): Promise; createWhitelist(whitelist: InsertWhitelist): Promise; deleteWhitelist(id: string): Promise; @@ -271,12 +271,40 @@ export class DatabaseStorage implements IStorage { } // Whitelist - async getAllWhitelist(): Promise { - return await db + async getAllWhitelist(options?: { limit?: number; offset?: number; search?: string }): Promise<{ items: Whitelist[]; total: number }> { + const limit = options?.limit || 50; + const offset = options?.offset || 0; + const search = options?.search?.trim().toLowerCase(); + + const conditions: any[] = [eq(whitelist.active, true)]; + + if (search) { + conditions.push( + sql`( + LOWER(${whitelist.ipAddress}) LIKE ${'%' + search + '%'} + OR LOWER(COALESCE(${whitelist.reason}, '')) LIKE ${'%' + search + '%'} + OR LOWER(COALESCE(${whitelist.comment}, '')) LIKE ${'%' + search + '%'} + OR LOWER(COALESCE(${whitelist.source}, '')) LIKE ${'%' + search + '%'} + )` + ); + } + + const whereClause = and(...conditions); + + const [countResult] = await db + .select({ count: sql`cast(count(*) as integer)` }) + .from(whitelist) + .where(whereClause); + + const items = await db .select() .from(whitelist) - .where(eq(whitelist.active, true)) - .orderBy(desc(whitelist.createdAt)); + .where(whereClause) + .orderBy(desc(whitelist.createdAt)) + .limit(limit) + .offset(offset); + + return { items, total: countResult.count }; } async getWhitelistByIp(ipAddress: string): Promise {