diff --git a/src/renderer/src/components/settings/enterprise/AuditLogScrollRegion.tsx b/src/renderer/src/components/settings/enterprise/AuditLogScrollRegion.tsx new file mode 100644 index 00000000000..bdf5f750bec --- /dev/null +++ b/src/renderer/src/components/settings/enterprise/AuditLogScrollRegion.tsx @@ -0,0 +1,28 @@ +import type React from 'react' + +/** + * A long audit history scrolls in place so the settings page keeps its length: the header, export + * and filters stay above it, and Show more pages in at the bottom. About ten rows (384px), or 60% + * of the window when that is smaller. The top edge is ruled so more reads as more, and it runs to + * the card's edges so the scrollbar sits against the card border. Focusable so the keyboard can + * scroll it. + */ +export function AuditLogScrollRegion({ + label, + children +}: { + label: string + children: React.ReactNode +}): React.JSX.Element { + return ( +
+ {children} +
+ ) +} diff --git a/src/renderer/src/components/settings/enterprise/EnterpriseAuditLog.tsx b/src/renderer/src/components/settings/enterprise/EnterpriseAuditLog.tsx index 7fff22bab9c..c39671c6575 100644 --- a/src/renderer/src/components/settings/enterprise/EnterpriseAuditLog.tsx +++ b/src/renderer/src/components/settings/enterprise/EnterpriseAuditLog.tsx @@ -2,6 +2,7 @@ // category and by person, with CSV export. Owners and admins only. A team whose plan does not // include it sees the upsell instead; the control plane refuses the data either way. import { Download, History, Loader2 } from 'lucide-react' +import { cn } from '@/lib/class-names' import { Button } from '@/components/ui/button' import { InlineError } from '@/components/ui/inline-error' import { @@ -26,6 +27,7 @@ import { CloudApiSkeletonRows } from '../cloud-api/cloud-api-parts' import { SettingsGroup } from '../section-layout/SettingsGroup' +import { AuditLogScrollRegion } from './AuditLogScrollRegion' import { EnterpriseUpsell, EnterpriseUpsellLine, type EnterpriseContact } from './EnterpriseUpsell' import { auditCategoryLabel, auditEventSentence, unnamedPersonIn } from './enterprise-audit-copy' import { useAppStore } from '@/store' @@ -364,55 +366,60 @@ export function EnterpriseAuditLog(props: EnterpriseAuditLogProps): React.JSX.El /> ) : ( - - )} - {log.moreError ? ( -
- -
- ) : log.hasMore ? ( -
- -
- ) : null} + {log.events.map((event) => ( +
  • + + + {auditCategoryLabel(event.category, t)} + + + + + + {formatRelativeFromNow(event.at, props.now, locale)} + +
  • + ))} + + {log.moreError ? ( +
    + +
    + ) : log.hasMore ? ( +
    + +
    + ) : null} + + )} )} diff --git a/src/renderer/src/components/settings/enterprise/EnterpriseSettingsView.test.tsx b/src/renderer/src/components/settings/enterprise/EnterpriseSettingsView.test.tsx index b5ac2e83762..6b6cfdfe5f4 100644 --- a/src/renderer/src/components/settings/enterprise/EnterpriseSettingsView.test.tsx +++ b/src/renderer/src/components/settings/enterprise/EnterpriseSettingsView.test.tsx @@ -170,6 +170,26 @@ describe('audit log', () => { expect(screen.queryByRole('button', { name: 'Show more' })).toBeNull() }) + it('scrolls a long log in place: rows and Show more inside, header, export and filters outside', () => { + render( + + ) + const region = screen.getByRole('region', { name: 'Audit log entries' }) + // Capped so the settings page keeps a normal length, and reachable by keyboard to scroll. + expect(region.className).toContain('max-h-[min(24rem,60vh)]') + expect(region.className).toContain('overflow-y-auto') + expect(region.tabIndex).toBe(0) + expect(region.querySelector('ul[aria-label="Audit log"]')).toBeTruthy() + expect(region.contains(screen.getByRole('button', { name: 'Show more' }))).toBe(true) + expect(region.contains(screen.getByRole('button', { name: 'Export CSV' }))).toBe(false) + expect(region.contains(screen.getByRole('combobox', { name: 'Filter by activity' }))).toBe( + false + ) + expect(region.contains(screen.getByText('Audit log', { selector: 'h3' }))).toBe(false) + }) + it('offers both filters and the export, and exports once per click', () => { const onExport = vi.fn() const { rerender } = render() diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 7d06a645ac1..57e6b99c9fd 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -1507,6 +1507,7 @@ "emptyBody": "Changes to members, API keys, secrets, workspaces and settings will show up here.", "clearFilters": "Clear filters", "showMore": "Show more", + "scrollLabel": "Audit log entries", "category": { "members": "Members", "security": "Security",