Skip to content

Commit 70705d5

Browse files
tianzhouclaude
andcommitted
feat(audit-log): move Export to tab bar, drop Reset, add back button
Remove the filter Reset button, move a single Export CSV button to the tab bar row (right-aligned) that exports the active tab, and add a "Back to editor" button that returns to the connection-scoped editor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1000876 commit 70705d5

1 file changed

Lines changed: 75 additions & 36 deletions

File tree

src/pages/AuditLog.tsx

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { useMemo, useState, type ReactNode } from 'react'
1+
import { useEffect, useMemo, useState, type ReactNode } from 'react'
22
import { useNavigate } from 'react-router-dom'
3-
import { Download, RotateCcw, ScrollText } from 'lucide-react'
3+
import { ArrowLeft, Download, ScrollText } from 'lucide-react'
44
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select'
55
import { Label } from '@/components/ui/label'
66
import { 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

Comments
 (0)