1- import { useMemo , useState , type ReactNode } from 'react'
1+ import { useEffect , useMemo , useState , type ReactNode } from 'react'
22import { useNavigate } from 'react-router-dom'
3- import { Download , RotateCcw , ScrollText } from 'lucide-react'
3+ import { ArrowLeft , Download , ScrollText } from 'lucide-react'
44import { Select , SelectTrigger , SelectValue , SelectContent , SelectItem } from '@/components/ui/select'
55import { Label } from '@/components/ui/label'
66import { Badge } from '@/components/ui/badge'
@@ -162,18 +162,15 @@ function AuditFilterBar({
162162 filters,
163163 onFiltersChange,
164164 filteredCount,
165- onExport,
166165} : {
167166 scope : AuditScope
168167 entries : AuditLogEntry [ ]
169168 filters : AuditFilters
170169 onFiltersChange : ( filters : AuditFilters ) => void
171170 filteredCount : number
172- onExport : ( ) => void
173171} ) {
174172 const actions = uniqueValues ( entries , ( entry ) => entry . action )
175173 const sources = uniqueValues ( entries , ( entry ) => entry . source )
176- const hasFilters = Object . entries ( filters ) . some ( ( [ key , value ] ) => value !== DEFAULT_FILTERS [ key as keyof AuditFilters ] )
177174 const searchId = `audit-${ scope } -search`
178175 const actionId = `audit-${ scope } -action`
179176 const sourceId = `audit-${ scope } -source`
@@ -183,7 +180,7 @@ function AuditFilterBar({
183180
184181 return (
185182 < div className = "mb-4 space-y-3" >
186- < div className = "grid gap-3 md:grid-cols-[minmax(220px,1fr)_160px_140px_140px_140px_140px_auto_auto ]" >
183+ < div className = "grid gap-3 md:grid-cols-[minmax(220px,1fr)_160px_140px_140px_140px_140px ]" >
187184 < div className = "space-y-1.5" >
188185 < Label htmlFor = { searchId } > Search</ Label >
189186 < Input
@@ -262,23 +259,6 @@ function AuditFilterBar({
262259 onChange = { ( event ) => onFiltersChange ( { ...filters , toDate : event . target . value } ) }
263260 />
264261 </ div >
265- < div className = "flex items-end" >
266- < Button
267- variant = "outline"
268- onClick = { ( ) => onFiltersChange ( DEFAULT_FILTERS ) }
269- disabled = { ! hasFilters }
270- aria-label = "Reset audit filters"
271- >
272- < RotateCcw />
273- Reset
274- </ Button >
275- </ div >
276- < div className = "flex items-end" >
277- < Button onClick = { onExport } disabled = { filteredCount === 0 } >
278- < Download />
279- Export CSV
280- </ Button >
281- </ div >
282262 </ div >
283263 < p className = "text-sm text-gray-500" >
284264 Showing { filteredCount } of { entries . length } recent entries
@@ -287,19 +267,27 @@ function AuditFilterBar({
287267 )
288268}
289269
290- function AuditEntriesView ( { entries, scope } : { entries : AuditLogEntry [ ] ; scope : AuditScope } ) {
291- const [ filters , setFilters ] = useState ( DEFAULT_FILTERS )
292- const filteredEntries = useMemo ( ( ) => filterEntries ( entries , filters ) , [ entries , filters ] )
293-
270+ function AuditEntriesView ( {
271+ entries,
272+ scope,
273+ filters,
274+ onFiltersChange,
275+ filteredEntries,
276+ } : {
277+ entries : AuditLogEntry [ ]
278+ scope : AuditScope
279+ filters : AuditFilters
280+ onFiltersChange : ( filters : AuditFilters ) => void
281+ filteredEntries : AuditLogEntry [ ]
282+ } ) {
294283 return (
295284 < >
296285 < AuditFilterBar
297286 scope = { scope }
298287 entries = { entries }
299288 filters = { filters }
300- onFiltersChange = { setFilters }
289+ onFiltersChange = { onFiltersChange }
301290 filteredCount = { filteredEntries . length }
302- onExport = { ( ) => exportAuditEntries ( filteredEntries , scope ) }
303291 />
304292 { filteredEntries . length === 0 ? (
305293 < EmptyState label = "No audit log entries match these filters." />
@@ -421,6 +409,24 @@ export default function AuditLog({ connectionId }: AuditLogProps) {
421409 // System tab is instance-owner only; the query is owner-gated server-side too.
422410 const sysQuery = useSystemAuditLogEntries ( isOwner )
423411
412+ // Filters live here (not per-tab) so the shared Export button on the tab bar can
413+ // export whichever tab is active. Switching connections resets the connection filters,
414+ // matching the previous per-connection remount behaviour.
415+ const [ activeTab , setActiveTab ] = useState < AuditScope > ( 'connection' )
416+ const [ connFilters , setConnFilters ] = useState ( DEFAULT_FILTERS )
417+ const [ sysFilters , setSysFilters ] = useState ( DEFAULT_FILTERS )
418+ useEffect ( ( ) => setConnFilters ( DEFAULT_FILTERS ) , [ connectionId ] )
419+
420+ const connFiltered = useMemo (
421+ ( ) => filterEntries ( connQuery . data ?? [ ] , connFilters ) ,
422+ [ connQuery . data , connFilters ] ,
423+ )
424+ const sysFiltered = useMemo (
425+ ( ) => filterEntries ( sysQuery . data ?? [ ] , sysFilters ) ,
426+ [ sysQuery . data , sysFilters ] ,
427+ )
428+ const activeFiltered = activeTab === 'connection' ? connFiltered : sysFiltered
429+
424430 // Connection tab: resolve the connections query (loading/error/empty) and the per-connection
425431 // admin gate before showing entries — otherwise admins briefly see the denied state on load.
426432 const connectionContent = error ? (
@@ -440,7 +446,15 @@ export default function AuditLog({ connectionId }: AuditLogProps) {
440446 error = { connQuery . error }
441447 emptyLabel = "No audit log entries yet."
442448 >
443- { ( entries ) => < AuditEntriesView key = { connectionId } entries = { entries } scope = "connection" /> }
449+ { ( entries ) => (
450+ < AuditEntriesView
451+ entries = { entries }
452+ scope = "connection"
453+ filters = { connFilters }
454+ onFiltersChange = { setConnFilters }
455+ filteredEntries = { connFiltered }
456+ />
457+ ) }
444458 </ EntriesPanel >
445459 )
446460
@@ -452,20 +466,45 @@ export default function AuditLog({ connectionId }: AuditLogProps) {
452466 error = { sysQuery . error }
453467 emptyLabel = "No system audit log entries yet."
454468 >
455- { ( entries ) => < AuditEntriesView entries = { entries } scope = "system" /> }
469+ { ( entries ) => (
470+ < AuditEntriesView
471+ entries = { entries }
472+ scope = "system"
473+ filters = { sysFilters }
474+ onFiltersChange = { setSysFilters }
475+ filteredEntries = { sysFiltered }
476+ />
477+ ) }
456478 </ EntriesPanel >
457479 )
458480
459481 return (
460482 < div className = "flex-1 bg-white text-gray-900 overflow-auto" >
461483 < div className = "p-8" >
484+ < Button
485+ variant = "ghost"
486+ onClick = { ( ) => navigate ( `/?connectionId=${ connectionId } ` ) }
487+ className = "-ml-2 mb-4"
488+ >
489+ < ArrowLeft />
490+ Back to editor
491+ </ Button >
462492 < h1 className = "text-3xl font-bold mb-8" > Audit Log</ h1 >
463493
464- < Tabs defaultValue = "connection" >
465- < TabsList className = "mb-6" >
466- < TabsTrigger value = "connection" > Connection</ TabsTrigger >
467- { isOwner && < TabsTrigger value = "system" > System</ TabsTrigger > }
468- </ TabsList >
494+ < Tabs value = { activeTab } onValueChange = { ( value ) => setActiveTab ( value as AuditScope ) } >
495+ < div className = "mb-6 flex items-center justify-between" >
496+ < TabsList >
497+ < TabsTrigger value = "connection" > Connection</ TabsTrigger >
498+ { isOwner && < TabsTrigger value = "system" > System</ TabsTrigger > }
499+ </ TabsList >
500+ < Button
501+ onClick = { ( ) => exportAuditEntries ( activeFiltered , activeTab ) }
502+ disabled = { activeFiltered . length === 0 }
503+ >
504+ < Download />
505+ Export CSV
506+ </ Button >
507+ </ div >
469508
470509 < TabsContent value = "connection" >
471510 { /* Connection selector stays outside the admin gate so a user without admin on the
0 commit comments