@@ -10,9 +10,18 @@ import {
1010 DialogHeader ,
1111 DialogTitle ,
1212} from "@/components/ui/dialog" ;
13+ import { Input } from "@/components/ui/input" ;
14+ import {
15+ Pagination ,
16+ PaginationContent ,
17+ PaginationItem ,
18+ PaginationLink ,
19+ PaginationNext ,
20+ PaginationPrevious ,
21+ } from "@/components/ui/pagination" ;
1322import type { Rule } from "@/lib/models/rule" ;
14- import { BookOpen , Trash2 } from "lucide-react" ;
15- import { useState } from "react" ;
23+ import { BookOpen , Search , Trash2 } from "lucide-react" ;
24+ import { useCallback , useEffect , useMemo , useState } from "react" ;
1625
1726interface ViewRulesModalProps {
1827 isOpen : boolean ;
@@ -23,14 +32,47 @@ export const ViewRulesModal = ({
2332 isOpen,
2433 onOpenChange,
2534} : ViewRulesModalProps ) => {
26- const { data : rules = [ ] , isLoading, refetch } = useRules ( ) ;
35+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
36+ const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
37+ const [ debouncedSearch , setDebouncedSearch ] = useState ( "" ) ;
2738 const [ editRuleId , setEditRuleId ] = useState < number | null > ( null ) ;
2839 const [ isAddRuleModalOpen , setIsAddRuleModalOpen ] = useState ( false ) ;
2940 const [ loadingId , setLoadingId ] = useState < number | null > ( null ) ;
3041 const [ confirmDeleteRule , setConfirmDeleteRule ] = useState < Rule | null > ( null ) ;
3142 const [ confirmLoading , setConfirmLoading ] = useState ( false ) ;
3243 const deleteRuleMutation = useDeleteRule ( ) ;
3344
45+ const pageSize = 5 ;
46+
47+ // Debounce search term
48+ useEffect ( ( ) => {
49+ const timer = setTimeout ( ( ) => {
50+ setDebouncedSearch ( searchTerm ) ;
51+ setCurrentPage ( 1 ) ; // Reset to first page on search
52+ } , 300 ) ;
53+
54+ return ( ) => clearTimeout ( timer ) ;
55+ } , [ searchTerm ] ) ;
56+
57+ const queryParams = useMemo (
58+ ( ) => ( {
59+ page : currentPage ,
60+ page_size : pageSize ,
61+ search : debouncedSearch || undefined ,
62+ } ) ,
63+ [ currentPage , pageSize , debouncedSearch ]
64+ ) ;
65+
66+ const { data : response , isLoading, refetch } = useRules ( queryParams ) ;
67+
68+ const rules = response ?. rules || [ ] ;
69+ const totalItems = response ?. total || 0 ;
70+ const totalPages = Math . ceil ( totalItems / pageSize ) ;
71+
72+ const handlePageChange = useCallback ( ( page : number ) => {
73+ setCurrentPage ( page ) ;
74+ } , [ ] ) ;
75+
3476 const openDeleteDialog = ( rule : Rule ) => {
3577 setConfirmDeleteRule ( rule ) ;
3678 setConfirmLoading ( false ) ;
@@ -50,57 +92,121 @@ export const ViewRulesModal = ({
5092 return (
5193 < >
5294 < Dialog open = { isOpen } onOpenChange = { onOpenChange } >
53- < DialogContent className = "sm:max-w-[425px] " >
95+ < DialogContent className = "sm:max-w-[600px] max-h-[80vh] overflow-y-auto " >
5496 < DialogHeader >
5597 < DialogTitle className = "flex items-center gap-2" >
5698 < BookOpen className = "h-5 w-5" />
5799 View Rules
58100 </ DialogTitle >
59101 </ DialogHeader >
60102 < div className = "grid gap-4 py-4" >
103+ { /* Search Input */ }
104+ < div className = "relative" >
105+ < Search className = "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
106+ < Input
107+ placeholder = "Search rules by name or description..."
108+ value = { searchTerm }
109+ onChange = { ( e ) => setSearchTerm ( e . target . value ) }
110+ className = "pl-10"
111+ />
112+ </ div >
113+
114+ { /* Rules List */ }
61115 { isLoading ? (
62116 < RuleListSkeleton count = { 3 } />
63117 ) : rules . length === 0 ? (
64- < div className = "text-muted-foreground text-center" >
65- No rules to display yet.
118+ < div className = "text-muted-foreground text-center py-8" >
119+ { debouncedSearch
120+ ? "No rules match your search."
121+ : "No rules to display yet." }
66122 </ div >
67123 ) : (
68- < div className = "grid gap-4" >
69- { rules . map ( ( rule ) => (
70- < div
71- key = { rule . id }
72- className = "flex items-center justify-between p-4 rounded-lg border border-border"
73- >
74- < div >
75- < div className = "font-medium" > { rule . name } </ div >
76- { rule . description && (
77- < div className = "text-sm text-muted-foreground" >
78- { rule . description }
79- </ div >
80- ) }
81- </ div >
82- < div className = "flex gap-2" >
83- < Button
84- variant = "outline"
85- size = "sm"
86- onClick = { ( ) => setEditRuleId ( rule . id ) }
87- >
88- Edit
89- </ Button >
90- < Button
91- variant = "destructive"
92- size = "sm"
93- disabled = { loadingId === rule . id }
94- onClick = { ( ) => openDeleteDialog ( rule ) }
95- >
96- < Trash2 className = "h-4 w-4" />
97- < span className = "sr-only" > Delete</ span >
98- </ Button >
124+ < >
125+ < div className = "grid gap-4" >
126+ { rules . map ( ( rule ) => (
127+ < div
128+ key = { rule . id }
129+ className = "flex items-center justify-between p-4 rounded-lg border border-border"
130+ >
131+ < div className = "flex-1 min-w-0" >
132+ < div className = "font-medium truncate" > { rule . name } </ div >
133+ { rule . description && (
134+ < div className = "text-sm text-muted-foreground truncate" >
135+ { rule . description }
136+ </ div >
137+ ) }
138+ </ div >
139+ < div className = "flex gap-2 ml-4" >
140+ < Button
141+ variant = "outline"
142+ size = "sm"
143+ onClick = { ( ) => setEditRuleId ( rule . id ) }
144+ >
145+ Edit
146+ </ Button >
147+ < Button
148+ variant = "destructive"
149+ size = "sm"
150+ disabled = { loadingId === rule . id }
151+ onClick = { ( ) => openDeleteDialog ( rule ) }
152+ >
153+ < Trash2 className = "h-4 w-4" />
154+ < span className = "sr-only" > Delete</ span >
155+ </ Button >
156+ </ div >
99157 </ div >
158+ ) ) }
159+ </ div >
160+
161+ { /* Pagination */ }
162+ { totalPages > 1 && (
163+ < div className = "flex items-center justify-between text-sm text-muted-foreground" >
164+ < Pagination >
165+ < PaginationContent >
166+ < PaginationItem >
167+ < PaginationPrevious
168+ onClick = { ( ) => handlePageChange ( currentPage - 1 ) }
169+ className = {
170+ currentPage <= 1
171+ ? "pointer-events-none opacity-50"
172+ : "cursor-pointer"
173+ }
174+ />
175+ </ PaginationItem >
176+
177+ { Array . from (
178+ { length : totalPages } ,
179+ ( _ , i ) => i + 1
180+ ) . map ( ( page ) => (
181+ < PaginationItem key = { page } >
182+ < PaginationLink
183+ onClick = { ( ) => handlePageChange ( page ) }
184+ isActive = { currentPage === page }
185+ className = "cursor-pointer"
186+ >
187+ { page }
188+ </ PaginationLink >
189+ </ PaginationItem >
190+ ) ) }
191+
192+ < PaginationItem >
193+ < PaginationNext
194+ onClick = { ( ) => handlePageChange ( currentPage + 1 ) }
195+ className = {
196+ currentPage >= totalPages
197+ ? "pointer-events-none opacity-50"
198+ : "cursor-pointer"
199+ }
200+ />
201+ </ PaginationItem >
202+ </ PaginationContent >
203+ </ Pagination >
100204 </ div >
101- ) ) }
102- </ div >
205+ ) }
206+ </ >
103207 ) }
208+
209+ { /* Add Rule Button */ }
104210 < Button
105211 onClick = { ( ) => setIsAddRuleModalOpen ( true ) }
106212 className = "w-full"
0 commit comments