1+ import { useState } from 'react' ;
2+ import { useQuery } from '@tanstack/react-query' ;
13import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
24import { Badge } from '@/components/ui/badge' ;
5+ import { Input } from '@/components/ui/input' ;
6+ import { Label } from '@/components/ui/label' ;
7+ import {
8+ Select ,
9+ SelectContent ,
10+ SelectItem ,
11+ SelectTrigger ,
12+ SelectValue ,
13+ } from '@/components/ui/select' ;
14+ import {
15+ Table ,
16+ TableBody ,
17+ TableCell ,
18+ TableHead ,
19+ TableHeader ,
20+ TableRow ,
21+ } from '@/components/ui/table' ;
22+
23+ interface AuditEvent {
24+ id : string ;
25+ eventType : string ;
26+ objectName : string ;
27+ recordId ?: string ;
28+ userId ?: string ;
29+ timestamp : string ;
30+ changes ?: Array < {
31+ field : string ;
32+ oldValue : any ;
33+ newValue : any ;
34+ } > ;
35+ metadata ?: Record < string , any > ;
36+ }
37+
38+ const eventTypeColors : Record < string , string > = {
39+ 'data.create' : 'default' ,
40+ 'data.update' : 'secondary' ,
41+ 'data.delete' : 'destructive' ,
42+ 'data.find' : 'outline' ,
43+ 'job.enqueued' : 'secondary' ,
44+ 'job.completed' : 'outline' ,
45+ 'job.failed' : 'destructive' ,
46+ } ;
347
448export default function AuditPage ( ) {
49+ const [ objectFilter , setObjectFilter ] = useState < string > ( '' ) ;
50+ const [ userFilter , setUserFilter ] = useState < string > ( '' ) ;
51+ const [ eventTypeFilter , setEventTypeFilter ] = useState < string > ( 'all' ) ;
52+ const [ startDate , setStartDate ] = useState < string > ( '' ) ;
53+ const [ endDate , setEndDate ] = useState < string > ( '' ) ;
54+
55+ const { data : eventsData , isLoading } = useQuery ( {
56+ queryKey : [ 'audit' , 'events' , objectFilter , userFilter , eventTypeFilter , startDate , endDate ] ,
57+ queryFn : async ( ) => {
58+ const params = new URLSearchParams ( ) ;
59+ if ( objectFilter ) params . append ( 'objectName' , objectFilter ) ;
60+ if ( userFilter ) params . append ( 'userId' , userFilter ) ;
61+ if ( eventTypeFilter !== 'all' ) params . append ( 'eventType' , eventTypeFilter ) ;
62+ if ( startDate ) params . append ( 'startDate' , startDate ) ;
63+ if ( endDate ) params . append ( 'endDate' , endDate ) ;
64+ params . append ( 'limit' , '100' ) ;
65+
66+ const response = await fetch ( `/api/v1/audit/events?${ params } ` ) ;
67+ if ( ! response . ok ) throw new Error ( 'Failed to fetch audit events' ) ;
68+ return response . json ( ) ;
69+ } ,
70+ } ) ;
71+
72+ const events : AuditEvent [ ] = eventsData ?. data || [ ] ;
73+
574 return (
675 < div className = "space-y-6" >
776 < div >
@@ -11,20 +80,147 @@ export default function AuditPage() {
1180 </ p >
1281 </ div >
1382
83+ { /* Filters */ }
84+ < Card >
85+ < CardHeader >
86+ < CardTitle > Filters</ CardTitle >
87+ < CardDescription > Filter audit events by various criteria</ CardDescription >
88+ </ CardHeader >
89+ < CardContent >
90+ < div className = "grid gap-4 md:grid-cols-2 lg:grid-cols-3" >
91+ < div className = "space-y-2" >
92+ < Label htmlFor = "object" > Object Name</ Label >
93+ < Input
94+ id = "object"
95+ placeholder = "e.g., accounts"
96+ value = { objectFilter }
97+ onChange = { ( e ) => setObjectFilter ( e . target . value ) }
98+ />
99+ </ div >
100+ < div className = "space-y-2" >
101+ < Label htmlFor = "user" > User ID</ Label >
102+ < Input
103+ id = "user"
104+ placeholder = "Filter by user"
105+ value = { userFilter }
106+ onChange = { ( e ) => setUserFilter ( e . target . value ) }
107+ />
108+ </ div >
109+ < div className = "space-y-2" >
110+ < Label htmlFor = "eventType" > Event Type</ Label >
111+ < Select value = { eventTypeFilter } onValueChange = { setEventTypeFilter } >
112+ < SelectTrigger id = "eventType" >
113+ < SelectValue />
114+ </ SelectTrigger >
115+ < SelectContent >
116+ < SelectItem value = "all" > All Types</ SelectItem >
117+ < SelectItem value = "data.create" > Create</ SelectItem >
118+ < SelectItem value = "data.update" > Update</ SelectItem >
119+ < SelectItem value = "data.delete" > Delete</ SelectItem >
120+ < SelectItem value = "data.find" > Read</ SelectItem >
121+ < SelectItem value = "job.enqueued" > Job Enqueued</ SelectItem >
122+ < SelectItem value = "job.completed" > Job Completed</ SelectItem >
123+ < SelectItem value = "job.failed" > Job Failed</ SelectItem >
124+ </ SelectContent >
125+ </ Select >
126+ </ div >
127+ < div className = "space-y-2" >
128+ < Label htmlFor = "startDate" > Start Date</ Label >
129+ < Input
130+ id = "startDate"
131+ type = "date"
132+ value = { startDate }
133+ onChange = { ( e ) => setStartDate ( e . target . value ) }
134+ />
135+ </ div >
136+ < div className = "space-y-2" >
137+ < Label htmlFor = "endDate" > End Date</ Label >
138+ < Input
139+ id = "endDate"
140+ type = "date"
141+ value = { endDate }
142+ onChange = { ( e ) => setEndDate ( e . target . value ) }
143+ />
144+ </ div >
145+ </ div >
146+ </ CardContent >
147+ </ Card >
148+
149+ { /* Event Log Table */ }
14150 < Card >
15151 < CardHeader >
16- < div className = "flex items-center justify-between gap-2" >
17- < CardTitle > Event Log</ CardTitle >
18- < Badge variant = "secondary" > Scaffold</ Badge >
152+ < div className = "flex items-center justify-between" >
153+ < div >
154+ < CardTitle > Event Log</ CardTitle >
155+ < CardDescription >
156+ Showing { events . length } event{ events . length !== 1 ? 's' : '' }
157+ </ CardDescription >
158+ </ div >
19159 </ div >
20- < CardDescription >
21- A chronological stream of CRUD events, field-level changes, and login activity.
22- </ CardDescription >
23160 </ CardHeader >
24161 < CardContent >
25- < p className = "text-sm text-muted-foreground" >
26- Connect to < code > @objectos/audit</ code > API to display events here.
27- </ p >
162+ { isLoading ? (
163+ < div className = "flex items-center justify-center py-8" >
164+ < div className = "animate-spin rounded-full size-8 border-2 border-muted border-t-primary" />
165+ </ div >
166+ ) : events . length === 0 ? (
167+ < div className = "text-center py-8 text-muted-foreground" >
168+ No audit events found
169+ </ div >
170+ ) : (
171+ < Table >
172+ < TableHeader >
173+ < TableRow >
174+ < TableHead > Timestamp</ TableHead >
175+ < TableHead > Event Type</ TableHead >
176+ < TableHead > Object</ TableHead >
177+ < TableHead > Record ID</ TableHead >
178+ < TableHead > User</ TableHead >
179+ < TableHead > Changes</ TableHead >
180+ </ TableRow >
181+ </ TableHeader >
182+ < TableBody >
183+ { events . map ( ( event ) => (
184+ < TableRow key = { event . id } >
185+ < TableCell className = "text-sm text-muted-foreground" >
186+ { new Date ( event . timestamp ) . toLocaleString ( ) }
187+ </ TableCell >
188+ < TableCell >
189+ < Badge variant = { eventTypeColors [ event . eventType ] as any || 'secondary' } >
190+ { event . eventType }
191+ </ Badge >
192+ </ TableCell >
193+ < TableCell className = "font-medium" > { event . objectName } </ TableCell >
194+ < TableCell className = "text-sm text-muted-foreground" >
195+ { event . recordId || '-' }
196+ </ TableCell >
197+ < TableCell className = "text-sm text-muted-foreground" >
198+ { event . userId || 'System' }
199+ </ TableCell >
200+ < TableCell className = "text-sm" >
201+ { event . changes && event . changes . length > 0 ? (
202+ < div className = "space-y-1" >
203+ { event . changes . slice ( 0 , 2 ) . map ( ( change , idx ) => (
204+ < div key = { idx } className = "text-xs" >
205+ < span className = "font-medium" > { change . field } :</ span > { ' ' }
206+ { JSON . stringify ( change . oldValue ) } → { JSON . stringify ( change . newValue ) }
207+ </ div >
208+ ) ) }
209+ { event . changes . length > 2 && (
210+ < div className = "text-xs text-muted-foreground" >
211+ +{ event . changes . length - 2 } more
212+ </ div >
213+ ) }
214+ </ div >
215+ ) : (
216+ '-'
217+ ) }
218+ </ TableCell >
219+ </ TableRow >
220+ ) ) }
221+ </ TableBody >
222+ </ Table >
223+ ) }
28224 </ CardContent >
29225 </ Card >
30226 </ div >
0 commit comments