Audit Log
blocklegalAdmin activity trail in a SectionCard: live indicator, search across actor/target, action-type filter, and rows pairing an avatar actor with a color-coded action badge, monospace target, IP and relative timestamp. Filtering is functional against the `entries` prop; swap the stub data for your source.
Installation
$pnpm dlx uipkge-ng@latest add audit-log$npx uipkge-ng@latest add audit-log$yarn dlx uipkge-ng@latest add audit-log$bunx uipkge-ng@latest add audit-logManifest:
https://uipkge.dev/r/angular/audit-log.jsonInstalls to:src/app/components/blocks/audit-log/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
AuditEntryinterface AuditEntry {
id: string
actor: string
action: AuditAction
target: string
ip: string
date: Date
}npm dependencies
Files installed (2)
src/app/components/blocks/audit-log/audit-log.component.ts8.3 kB
// Admin activity trail in a SectionCard: live indicator, search across actor/target, // action-type filter, and rows pairing an avatar actor with a color-coded action badge, // monospace target, IP and relative timestamp. Port of the Vue/React block 1:1 — // filtering is functional against the `entries` input; swap the stub data for your source. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Download, Fingerprint, LucideAngularModule, RadioTower, Search } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiRelativeTimeComponent } from '@/ui/relative-time/relative-time.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' export type AuditAction = 'sign-in' | 'user.updated' | 'role.changed' | 'export' | 'delete' export interface AuditEntry { id: string actor: string action: AuditAction target: string ip: string date: Date } const actionTone: Record<AuditAction, BadgeVariant> = { 'sign-in': 'secondary', 'user.updated': 'default', 'role.changed': 'warning', export: 'info', delete: 'destructive', } const now = Date.now() const minutesAgo = (m: number): Date => new Date(now - m * 60_000) const hoursAgo = (h: number): Date => new Date(now - h * 3_600_000) const daysAgo = (d: number): Date => new Date(now - d * 86_400_000) const stubEntries: AuditEntry[] = [ { id: 'a1', actor: 'Amara Osei', action: 'role.changed', target: 'priya@acme.com → Editor', ip: '77.12.44.9', date: minutesAgo(4), }, { id: 'a2', actor: 'System', action: 'export', target: 'payroll-2026-07.csv', ip: '—', date: minutesAgo(38) }, { id: 'a3', actor: 'Jonas Weber', action: 'sign-in', target: 'admin.acme.com', ip: '84.190.201.3', date: hoursAgo(1), }, { id: 'a4', actor: 'Priya Nair', action: 'user.updated', target: 'profile.phone', ip: '103.25.8.77', date: hoursAgo(3), }, { id: 'a5', actor: 'Amara Osei', action: 'delete', target: 'draft-policy-v3.pdf', ip: '77.12.44.9', date: hoursAgo(5), }, { id: 'a6', actor: 'Marcus Lee', action: 'sign-in', target: 'admin.acme.com', ip: '45.62.110.204', date: daysAgo(1) }, { id: 'a7', actor: 'Priya Nair', action: 'user.updated', target: 'team/onboarding-flow', ip: '103.25.8.77', date: daysAgo(1), }, { id: 'a8', actor: 'System', action: 'role.changed', target: 'contractors → Viewer', ip: '—', date: daysAgo(2) }, { id: 'a9', actor: 'Jonas Weber', action: 'export', target: 'audit-trail-q2.json', ip: '84.190.201.3', date: daysAgo(2), }, { id: 'a10', actor: 'Marcus Lee', action: 'delete', target: 'staging-deploy-key', ip: '45.62.110.204', date: daysAgo(3), }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-audit-log, [ui-audit-log]', standalone: true, // SectionCard renders the card chrome itself: the host stays out of layout. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiInputComponent, UiRelativeTimeComponent, UiSectionCardComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, ], template: ` <ui-section-card data-slot="audit-log" title="Audit log" description="Who changed what, when — across your workspace." [class]="rootClass" > <span slot="header-action" class="text-success flex items-center gap-1.5 text-xs font-medium"> <lucide-icon [img]="RadioTower" class="size-3.5" aria-hidden="true" /> live </span> <div class="flex flex-col gap-2 sm:flex-row sm:items-center"> <div class="relative flex-1"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2" aria-hidden="true" /> <ui-input [value]="search()" (valueChange)="search.set($event)" placeholder="Search actor or target…" class="pl-8" /> </div> <ui-select [value]="actionFilter()" (valueChange)="setActionFilter($event)"> <button ui-select-trigger class="sm:w-44"> <ui-select-value placeholder="All actions" /> </button> <ui-select-content> <ui-select-item value="all">All actions</ui-select-item> @for (action of actions; track action) { <ui-select-item [value]="action">{{ action }}</ui-select-item> } </ui-select-content> </ui-select> <button ui-button aria-label="Download attachment" variant="outline" size="sm"> <lucide-icon [img]="Download" aria-hidden="true" /> Export </button> </div> <ul class="-mb-4 divide-y"> @if (filtered().length === 0) { <li class="text-muted-foreground flex flex-col items-center gap-2 py-10 text-sm"> <lucide-icon [img]="Fingerprint" class="size-6 opacity-50" aria-hidden="true" /> No activity matches your filters. </li> } @for (entry of filtered(); track entry.id) { <li class="flex items-center gap-3 py-3"> <ui-avatar class="size-7"> <ui-avatar-fallback class="text-xs">{{ initials(entry.actor) }}</ui-avatar-fallback> </ui-avatar> <div class="min-w-0 flex-1"> <div class="flex items-center gap-2"> <p class="truncate text-sm font-medium">{{ entry.actor }}</p> <span ui-badge [variant]="tone(entry.action)" class="shrink-0">{{ entry.action }}</span> </div> <p class="text-muted-foreground truncate font-mono text-xs">{{ entry.target }}</p> </div> <div class="hidden text-right sm:block"> <ui-relative-time [date]="entry.date" class="text-muted-foreground block text-xs" /> <p class="text-muted-foreground/70 font-mono text-xs">{{ entry.ip }}</p> </div> </li> } </ul> </ui-section-card> `, }) export class UiAuditLogComponent { protected readonly Download = Download protected readonly Fingerprint = Fingerprint protected readonly RadioTower = RadioTower protected readonly Search = Search protected readonly actions: AuditAction[] = ['sign-in', 'user.updated', 'role.changed', 'export', 'delete'] @Input() set entries(v: AuditEntry[] | undefined) { this.entriesInput.set(v) } @Input() set initialSearch(v: string) { this.search.set(v ?? '') } @Input() set initialAction(v: 'all' | AuditAction) { this.actionFilter.set(v ?? 'all') } @Input('class') className?: string readonly entriesInput = signal<AuditEntry[] | undefined>(undefined) readonly search = signal('') readonly actionFilter = signal<'all' | AuditAction>('all') readonly source = computed(() => this.entriesInput() ?? stubEntries) readonly filtered = computed(() => { const query = this.search() const action = this.actionFilter() return this.source().filter((entry) => { const matchesQuery = query === '' || `${entry.actor} ${entry.target}`.toLowerCase().includes(query.toLowerCase()) const matchesAction = action === 'all' || entry.action === action return matchesQuery && matchesAction }) }) get rootClass(): string { return cn(this.className) } tone(action: AuditAction): BadgeVariant { return actionTone[action] } setActionFilter(value: string): void { this.actionFilter.set(value as 'all' | AuditAction) } initials(name: string): string { return name .split(' ') .map((part) => part[0]) .slice(0, 2) .join('') .toUpperCase() } }src/app/components/blocks/audit-log/index.ts0.1 kB
export { UiAuditLogComponent, type AuditAction, type AuditEntry } from './audit-log.component'
Raw manifest:https://uipkge.dev/r/angular/audit-log.json