Audit Event Stream
blockdevopsReal-time live streaming audit log viewer and security telemetry feed with status indicators, stream pause/resume controls, regex and keyword log filtering, severity badges, expandable JSON payload inspectors, and ingestion telemetry stats.
Installation
$pnpm dlx uipkge-ng@latest add audit-event-stream$npx uipkge-ng@latest add audit-event-stream$yarn dlx uipkge-ng@latest add audit-event-stream$bunx uipkge-ng@latest add audit-event-streamManifest:
https://uipkge.dev/r/angular/audit-event-stream.jsonInstalls to:src/app/components/blocks/audit-event-stream/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.
AuditStreamEventinterface AuditStreamEvent {
id: string
timestamp: string
severity: EventSeverity
action: string
actor: {
name: string
type: 'user' | 'service' | 'system'
}
sourceIp: string
geo: string
target: string
summary: string
metadata: Record<string, unknown>
}Files installed (3)
src/app/components/blocks/audit-event-stream/audit-event-stream.component.ts31.8 kB
import { ChangeDetectionStrategy, Component, Input, OnInit, computed, signal } from '@angular/core' import { Activity, AlertCircle, AlertTriangle, Check, ChevronDown, ChevronRight, Download, Filter, Info, Layers, LucideAngularModule, Pause, Play, RotateCcw, Search, Shield, ShieldAlert, Trash2, Zap, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiAvatarComponent, UiAvatarFallbackComponent } from '@/ui/avatar/avatar.component' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiEventDetailDrawerComponent } from './event-detail-drawer.component' export type EventSeverity = 'info' | 'warning' | 'critical' export interface AuditStreamEvent { id: string timestamp: string severity: EventSeverity action: string actor: { name: string type: 'user' | 'service' | 'system' } sourceIp: string geo: string target: string summary: string metadata: Record<string, unknown> } const defaultEvents: AuditStreamEvent[] = [ { id: 'evt-1094', timestamp: '2026-08-21T14:32:05.812Z', severity: 'critical', action: 'policy.access.denied', actor: { name: '[email protected]', type: 'user', }, sourceIp: '45.33.32.156', geo: 'AP-East (HK)', target: 'arn:aws:s3:::prod-customer-pii-cold-storage/*', summary: 'Cross-boundary access violation to restricted cold storage bucket', metadata: { request_id: 'req_88f912c9a01', resource_arn: 'arn:aws:s3:::prod-customer-pii-cold-storage/*', requested_action: 's3:GetObject', violation_code: 'UNAUTHORIZED_GEOLOCATION_ACCESS', risk_score: 94, enforcement: 'blocked_by_guardduty', user_agent: 'aws-cli/2.15.30 Python/3.11.8 Darwin/24.1.0', }, }, { id: 'evt-1093', timestamp: '2026-08-21T14:31:58.240Z', severity: 'critical', action: 'vpc.security_group.ingress_created', actor: { name: 'devops-ci-orchestrator', type: 'service', }, sourceIp: '172.31.0.4', geo: 'EU-West (IE)', target: 'sg-08f912c9aa30 (prod-bastion-sg)', summary: 'Security group rule added opening SSH (0.0.0.0/0:22) to public internet', metadata: { request_id: 'req_39b201a44e', security_group_id: 'sg-08f912c9aa30', rule: { protocol: 'tcp', from_port: 22, to_port: 22, cidr: '0.0.0.0/0', }, compliance_impact: 'NON_COMPLIANT_SOC2_CC6', alert: 'AUTOMATIC_SECURITY_REMEDIATION_TRIGGERED', quarantine_status: 'isolated_pending_approval', }, }, { id: 'evt-1092', timestamp: '2026-08-21T14:31:44.102Z', severity: 'warning', action: 'api.rate_limit.exceeded', actor: { name: 'api-gateway-edge', type: 'service', }, sourceIp: '203.0.113.195', geo: 'EU-Central (FRA)', target: 'api.acme.com/v2/organizations/billing/sync', summary: 'Customer exceeded tier rate limit of 10,000 req/min (current: 14,820 req/min)', metadata: { request_id: 'req_a44019bf08', client_id: 'app_pub_8829104', tier: 'pro_tier', limit: '10,000 req/min', current_rate: '14,820 req/min', burst_allowance: 'exhausted', enforced_action: 'http_429_throttled', retry_after_seconds: 45, }, }, { id: 'evt-1091', timestamp: '2026-08-21T14:31:22.955Z', severity: 'warning', action: 'auth.mfa.challenge_failed', actor: { name: '[email protected]', type: 'user', }, sourceIp: '192.0.2.142', geo: 'US-West (SFO)', target: 'identity.acme.corp/mfa/challenge', summary: 'Multiple consecutive invalid TOTP code attempts detected', metadata: { request_id: 'req_77d812ef90', user_id: 'usr_8830192a', attempt_count: 3, max_allowed: 5, failure_reason: 'totp_token_mismatch', client_fingerprint: 'fp_993b2a0018f', risk_flag: 'SUSPICIOUS_DEVICE_FINGERPRINT', }, }, { id: 'evt-1090', timestamp: '2026-08-21T14:31:05.419Z', severity: 'warning', action: 'storage.bucket.policy_updated', actor: { name: '[email protected]', type: 'user', }, sourceIp: '84.190.201.3', geo: 'EU-Central (BER)', target: 'prod-exports-transfers-bucket', summary: 'CORS origins and bucket policy permissions updated', metadata: { request_id: 'req_1204891ff3', bucket: 'prod-exports-transfers-bucket', changes: { public_read: false, cors_origins: ['https://app.acme.com', 'https://staging.acme.com'], ssl_enforced: true, }, review_status: 'PENDING_AUDIT_SIGNOFF', }, }, { id: 'evt-1089', timestamp: '2026-08-21T14:30:48.331Z', severity: 'info', action: 'iam.user.login_success', actor: { name: '[email protected]', type: 'user', }, sourceIp: '198.51.100.24', geo: 'US-East (NYC)', target: 'auth.acme.corp/oauth/callback', summary: 'Successful SAML SSO login with WebAuthn hardware token verification', metadata: { request_id: 'req_66c29188e1', auth_provider: 'okta_saml_enterprise', mfa_type: 'fido2_yubikey', session_id: 'sess_9f82c1a003', user_agent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4) AppleWebKit/537.36', }, }, { id: 'evt-1088', timestamp: '2026-08-21T14:30:19.780Z', severity: 'info', action: 'db.secret.rotated', actor: { name: 'vault-agent-worker', type: 'service', }, sourceIp: '10.240.12.88', geo: 'US-West (OR)', target: 'kv/prod/aurora-primary/credentials', summary: 'Automated 30-day credential rotation for primary PostgreSQL cluster', metadata: { request_id: 'req_55104882c9', vault_cluster: 'prod-vault-cluster-01', path: 'kv/prod/aurora-primary/credentials', ttl_seconds: 2592000, databases_updated: ['aurora-writer-01', 'aurora-reader-01', 'aurora-reader-02'], status: 'ROTATION_SUCCESS', }, }, { id: 'evt-1087', timestamp: '2026-08-21T14:29:55.104Z', severity: 'info', action: 'repo.deployment.started', actor: { name: 'github-actions[bot]', type: 'service', }, sourceIp: '140.82.112.22', geo: 'US-East (IAD)', target: 'uipkge/core-registry:main (a3f89b1)', summary: 'Production deployment triggered via merge pull request #412', metadata: { request_id: 'req_44910283aa', repository: 'uipkge/core-registry', commit_hash: 'a3f89b14e9f', environment: 'production', pipeline_id: 'pipe_883019', runner: 'runner-edge-iad-4', }, }, { id: 'evt-1086', timestamp: '2026-08-21T14:29:12.650Z', severity: 'info', action: 'kms.key.decrypted', actor: { name: 'payment-service-daemon', type: 'service', }, sourceIp: '10.128.4.19', geo: 'US-East (VA)', target: 'arn:aws:kms:us-east-1:992019482:key/cmk-cardholder-01', summary: 'Authorized envelope decryption operation for card tokenization', metadata: { request_id: 'req_338192001e', key_arn: 'arn:aws:kms:us-east-1:992019482:key/cmk-cardholder-01', algorithm: 'AES_GCM_256', caller_service: 'checkout-v2-pod-88x', auth_context: { transaction_id: 'tx_9981203', environment: 'production' }, }, }, { id: 'evt-1085', timestamp: '2026-08-21T14:28:40.091Z', severity: 'info', action: 'service.token.revoked', actor: { name: 'security-automations', type: 'system', }, sourceIp: '127.0.0.1', geo: 'Internal', target: 'tok_expired_929104882', summary: 'Routine cleanup revoked expired machine-to-machine JWT token', metadata: { request_id: 'req_227198300a', token_id: 'tok_expired_929104882', reason: 'ttl_expiration', issued_at: '2026-08-14T00:00:00Z', revoked_by: 'system_cron_daemon', }, }, ] function initials(name: string): string { if (name.includes('@')) { return name.slice(0, 2).toUpperCase() } return ( name .replace(/[^a-zA-Z0-9\s]/g, '') .split(' ') .filter(Boolean) .map((part) => part[0]) .slice(0, 2) .join('') .toUpperCase() || 'SA' ) } function matchesSearch(event: AuditStreamEvent, query: string): boolean { if (!query.trim()) return true const q = query.trim() if (q.startsWith('/') && q.length > 1) { const lastSlash = q.lastIndexOf('/') if (lastSlash > 0) { const pattern = q.slice(1, lastSlash) const flags = q.slice(lastSlash + 1) try { const re = new RegExp(pattern, flags) return ( re.test(event.action) || re.test(event.actor.name) || re.test(event.sourceIp) || re.test(event.geo) || re.test(event.target) || re.test(event.summary) || re.test(JSON.stringify(event.metadata)) ) } catch { // Fall back to plain search on invalid regex syntax } } } const lower = q.toLowerCase() return ( event.action.toLowerCase().includes(lower) || event.actor.name.toLowerCase().includes(lower) || event.sourceIp.toLowerCase().includes(lower) || event.geo.toLowerCase().includes(lower) || event.target.toLowerCase().includes(lower) || event.summary.toLowerCase().includes(lower) || JSON.stringify(event.metadata).toLowerCase().includes(lower) ) } function formatTimestamp(iso: string): string { try { const d = new Date(iso) return d.toISOString().replace('T', ' ').replace('Z', ' UTC') } catch { return iso } } const PILL_BASE = 'focus-visible:ring-ring inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none' /** * AuditEventStream -- the React block, 1:1: live audit log viewer with pause/resume, * regex + keyword search, severity filter pills, expandable JSON payload rows and * ingestion telemetry stats. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-audit-event-stream, [ui-audit-event-stream]', standalone: true, host: { class: 'block' }, imports: [ LucideAngularModule, UiAvatarComponent, UiAvatarFallbackComponent, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiInputComponent, UiSeparatorComponent, UiEventDetailDrawerComponent, ], template: ` <div data-slot="audit-event-stream" [class]="rootClass"> <!-- Top Header & Live Telemetry Status --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-3"> <div class="bg-card flex size-10 items-center justify-center rounded-lg border shadow-xs"> <lucide-icon [img]="ShieldAlert" class="text-primary size-5" aria-hidden="true" /> </div> <div> <div class="flex items-center gap-2"> <h2 class="text-2xl font-bold tracking-tight">Audit Event Stream</h2> @if (!isPaused()) { <div class="border-success/30 bg-success/10 text-success inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium" > <span class="relative flex size-2 shrink-0"> <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span> <span class="bg-success relative inline-flex size-2 rounded-full"></span> </span> Live Stream · Connected </div> } @else { <div class="border-warning/30 bg-warning/10 text-warning inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium" > <span class="bg-warning relative inline-flex size-2 rounded-full"></span> Stream Paused </div> } </div> <p class="text-muted-foreground text-xs"> Real-time security telemetry feed and structured audit trail inspector </p> </div> </div> <div class="flex flex-wrap items-center gap-2"> <button ui-button variant="outline" size="sm" [class]="isPaused() ? 'border-warning/40 text-warning' : ''" (click)="togglePause()" > @if (isPaused()) { <lucide-icon [img]="Play" class="size-3.5 fill-current" aria-hidden="true" /> } @else { <lucide-icon [img]="Pause" class="size-3.5" aria-hidden="true" /> } <span>{{ isPaused() ? 'Resume Stream' : 'Pause Stream' }}</span> </button> <button ui-button variant="outline" size="sm" [disabled]="events().length === 0" (click)="clearStream()"> <lucide-icon [img]="Trash2" class="size-3.5" aria-hidden="true" /> <span>Clear Stream</span> </button> <button ui-button aria-label="Download attachment" variant="default" size="sm" [disabled]="filteredEvents().length === 0" (click)="exportJson()" > @if (exportedNotice()) { <lucide-icon [img]="Check" class="text-success size-3.5" aria-hidden="true" /> } @else { <lucide-icon [img]="Download" class="size-3.5" aria-hidden="true" /> } <span>{{ exportedNotice() ? 'Exported JSON' : 'Export JSON' }}</span> </button> </div> </div> <!-- Filter & Search Toolbar --> <ui-card class="border-border shadow-xs"> <ui-card-content class="p-3"> <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> <!-- Search Input with Regex Support --> <div class="relative flex-1"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2" aria-hidden="true" /> <ui-input [value]="searchQuery()" (valueChange)="searchQuery.set($event)" placeholder="Search event action, actor, IP, target, or /regex/..." class="pl-9 text-xs" /> </div> <!-- Severity Filter Pills --> <div class="flex flex-wrap items-center gap-1.5"> <span class="text-muted-foreground mr-1 flex items-center gap-1 text-xs font-medium"> <lucide-icon [img]="Filter" class="size-3" aria-hidden="true" /> Severity: </span> <button type="button" [class]="pillClass('all')" (click)="selectedSeverity.set('all')"> All <span [class]="countClass('all')">{{ severityCounts().all }}</span> </button> <button type="button" [class]="pillClass('critical')" (click)="selectedSeverity.set('critical')"> <span class="bg-destructive size-1.5 rounded-full"></span> Critical <span [class]="countClass('critical')">{{ severityCounts().critical }}</span> </button> <button type="button" [class]="pillClass('warning')" (click)="selectedSeverity.set('warning')"> <span class="bg-warning size-1.5 rounded-full"></span> Warning <span [class]="countClass('warning')">{{ severityCounts().warning }}</span> </button> <button type="button" [class]="pillClass('info')" (click)="selectedSeverity.set('info')"> <span class="bg-info size-1.5 rounded-full"></span> Info <span [class]="countClass('info')">{{ severityCounts().info }}</span> </button> </div> </div> </ui-card-content> </ui-card> <!-- Main Log Stream Feed Terminal Card --> <ui-card class="border-border shadow-xs"> <!-- Terminal Header Bar --> <div class="bg-muted/40 flex items-center justify-between gap-x-2 border-b px-4 py-2.5"> <div class="flex items-center gap-2"> <div class="flex items-center gap-1.5"> <span class="bg-destructive/80 size-2.5 rounded-full"></span> <span class="bg-warning/80 size-2.5 rounded-full"></span> <span class="bg-success/80 size-2.5 rounded-full"></span> </div> <span class="text-muted-foreground font-mono text-xs"> telemetry-stream-us-east-1 · ingester-v2 · tcp+tls </span> </div> <div class="flex items-center gap-3"> <span class="text-muted-foreground hidden font-mono text-xs sm:inline"> Matches: {{ filteredEvents().length }} / {{ events().length }} </span> <button ui-button variant="ghost" size="xs" class="h-6 gap-1 px-2 text-xs" [disabled]="filteredEvents().length === 0" (click)="toggleAllExpand()" > @if (allExpanded()) { <lucide-icon [img]="ChevronDown" class="size-3" aria-hidden="true" /> } @else { <lucide-icon [img]="ChevronRight" class="size-3" aria-hidden="true" /> } <span>{{ allExpanded() ? 'Collapse All' : 'Expand All' }}</span> </button> </div> </div> <!-- Feed Rows Container --> <ui-card-content class="p-0"> @if (filteredEvents().length === 0) { <!-- Empty State when 0 matching events --> <div class="flex flex-col items-center justify-center gap-3 px-4 py-16 text-center"> <div class="bg-muted flex size-12 items-center justify-center rounded-full"> <lucide-icon [img]="Shield" class="text-muted-foreground size-6 opacity-60" aria-hidden="true" /> </div> <div class="space-y-1"> <p class="text-foreground text-sm font-semibold">No audit events match your criteria</p> <p class="text-muted-foreground text-xs"> {{ events().length === 0 ? 'The event buffer was cleared. Restore default stream data to resume inspection.' : 'Try loosening your severity filter or search query.' }} </p> </div> <button ui-button variant="outline" size="sm" class="mt-2 text-xs" (click)="resetStream()"> <lucide-icon [img]="RotateCcw" class="size-3.5" aria-hidden="true" /> Restore stream data </button> </div> } @else { <ul class="divide-border/60 divide-y"> @for (event of filteredEvents(); track event.id) { <li [class]="rowClass(event.id)"> <!-- Primary Row Trigger --> <div role="button" tabindex="0" [attr.aria-expanded]="isExpanded(event.id)" class="focus-visible:ring-ring flex cursor-pointer flex-col gap-3 p-3.5 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset sm:flex-row sm:items-center sm:justify-between" (click)="toggleExpand(event.id)" (keydown)="onRowKeydown($event, event.id)" > <!-- Left Rail: Expand Indicator + Timestamp + Severity + Action --> <div class="flex min-w-0 items-start gap-3 sm:items-center"> <button type="button" aria-label="Toggle payload details" [class]="chevronClass(event.id)"> <lucide-icon [img]="ChevronRight" class="size-4" aria-hidden="true" /> </button> <div class="space-y-1 sm:space-y-0.5"> <div class="flex flex-wrap items-center gap-2"> <span class="text-muted-foreground font-mono text-xs whitespace-nowrap"> {{ formatTimestamp(event.timestamp) }} </span> @if (event.severity === 'critical') { <span ui-badge variant="destructive" class="gap-1 font-mono text-xs uppercase"> <lucide-icon [img]="AlertCircle" class="size-3" aria-hidden="true" /> CRIT </span> } @else if (event.severity === 'warning') { <span ui-badge variant="warning" class="gap-1 font-mono text-xs uppercase"> <lucide-icon [img]="AlertTriangle" class="size-3" aria-hidden="true" /> WARN </span> } @else { <span ui-badge variant="info" class="gap-1 font-mono text-xs uppercase"> <lucide-icon [img]="Info" class="size-3" aria-hidden="true" /> INFO </span> } <span class="text-foreground font-mono text-xs font-semibold">{{ event.action }}</span> </div> <p class="text-muted-foreground text-xs font-normal">{{ event.summary }}</p> </div> </div> <!-- Right Rail: Actor Avatar & Tag + IP & Geo Flag --> <div class="flex shrink-0 flex-wrap items-center gap-3 sm:justify-end"> <div class="bg-muted/60 flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs"> <ui-avatar class="size-4"> <ui-avatar-fallback class="bg-primary/10 text-primary text-xs font-medium"> {{ initials(event.actor.name) }} </ui-avatar-fallback> </ui-avatar> <span class="text-foreground max-w-[130px] truncate font-medium sm:max-w-[180px]"> {{ event.actor.name }} </span> </div> <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs"> <span>{{ event.sourceIp }}</span> <span class="opacity-50">·</span> <span class="text-foreground/80">{{ event.geo }}</span> </div> </div> </div> <!-- Expanded Payload Accordion Detail View --> @if (isExpanded(event.id)) { <ui-event-detail-drawer [event]="event" [copied]="copiedId() === event.id" (copy)="copyPayload(event.id, event.metadata)" /> } </li> } </ul> } </ui-card-content> <!-- Bottom Telemetry Stats Bar --> <div class="bg-muted/40 flex flex-col gap-2.5 border-t px-4 py-3 text-xs sm:flex-row sm:items-center sm:justify-between" > <div class="text-muted-foreground flex flex-wrap items-center gap-4"> <div class="text-foreground flex items-center gap-1.5 font-medium"> <lucide-icon [img]="Activity" class="text-success size-3.5" aria-hidden="true" /> <span>Ingestion Rate:</span> <span class="font-mono">{{ isPaused() ? '0 evt/s (Paused)' : '42 events/sec' }}</span> </div> <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" /> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Layers" class="size-3.5" aria-hidden="true" /> <span>Buffer Size:</span> <span class="text-foreground font-mono">1.2 MB / 10 MB (12%)</span> </div> <ui-separator orientation="vertical" class="hidden h-3.5 sm:block" /> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Zap" class="text-warning size-3.5" aria-hidden="true" /> <span>Latency:</span> <span class="text-foreground font-mono">~18ms p99</span> </div> </div> <div class="text-muted-foreground flex items-center gap-2 font-mono"> <span class="bg-success size-1.5 rounded-full"></span> <span>TLS v1.3 · TCP Keep-Alive · us-east-1a</span> </div> </div> </ui-card> </div> `, }) export class UiAuditEventStreamComponent implements OnInit { @Input() initialEvents?: AuditStreamEvent[] @Input() initialPaused = false @Input() initialSeverity: 'all' | EventSeverity = 'all' @Input() initialSearch = '' @Input('class') className?: string readonly events = signal<AuditStreamEvent[]>([...defaultEvents]) readonly isPaused = signal(false) readonly selectedSeverity = signal<'all' | EventSeverity>('all') readonly searchQuery = signal('') readonly expandedIds = signal<Record<string, boolean>>({ 'evt-1094': true }) readonly copiedId = signal<string | null>(null) readonly exportedNotice = signal(false) readonly filteredEvents = computed(() => { const severity = this.selectedSeverity() const query = this.searchQuery() return this.events().filter((event) => { const matchesSeverity = severity === 'all' || event.severity === severity return matchesSeverity && matchesSearch(event, query) }) }) readonly severityCounts = computed(() => { const events = this.events() return { all: events.length, critical: events.filter((e) => e.severity === 'critical').length, warning: events.filter((e) => e.severity === 'warning').length, info: events.filter((e) => e.severity === 'info').length, } }) readonly allExpanded = computed(() => { const filtered = this.filteredEvents() const expanded = this.expandedIds() return filtered.length > 0 && filtered.every((e) => expanded[e.id]) }) protected readonly Activity = Activity protected readonly AlertCircle = AlertCircle protected readonly AlertTriangle = AlertTriangle protected readonly Check = Check protected readonly ChevronDown = ChevronDown protected readonly ChevronRight = ChevronRight protected readonly Download = Download protected readonly Filter = Filter protected readonly Info = Info protected readonly Layers = Layers protected readonly Pause = Pause protected readonly Play = Play protected readonly RotateCcw = RotateCcw protected readonly Search = Search protected readonly Shield = Shield protected readonly ShieldAlert = ShieldAlert protected readonly Trash2 = Trash2 protected readonly Zap = Zap protected readonly initials = initials protected readonly formatTimestamp = formatTimestamp ngOnInit(): void { this.events.set(this.initialEvents ? [...this.initialEvents] : [...defaultEvents]) this.isPaused.set(this.initialPaused) this.selectedSeverity.set(this.initialSeverity) this.searchQuery.set(this.initialSearch) } get rootClass(): string { return cn('w-full space-y-4', this.className) } isExpanded(id: string): boolean { return !!this.expandedIds()[id] } rowClass(id: string): string { return cn('group transition-colors', this.isExpanded(id) ? 'bg-muted/30' : 'hover:bg-muted/20') } chevronClass(id: string): string { return cn( 'text-muted-foreground group-hover:text-foreground mt-0.5 shrink-0 transition-transform sm:mt-0', this.isExpanded(id) && 'text-foreground rotate-90', ) } pillClass(severity: 'all' | EventSeverity): string { const active = this.selectedSeverity() === severity switch (severity) { case 'all': return cn( PILL_BASE, active ? 'border-primary bg-primary text-primary-foreground shadow-xs' : 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground', ) case 'critical': return cn( PILL_BASE, active ? 'border-destructive bg-destructive text-white shadow-xs' : 'border-border bg-card text-muted-foreground hover:border-destructive/40 hover:text-destructive', ) case 'warning': return cn( PILL_BASE, active ? 'border-warning bg-warning text-white shadow-xs' : 'border-border bg-card text-muted-foreground hover:text-warning hover:border-warning/40', ) case 'info': return cn( PILL_BASE, active ? 'border-info bg-info text-white shadow-xs' : 'border-border bg-card text-muted-foreground hover:text-info hover:border-info/40', ) } } countClass(severity: 'all' | EventSeverity): string { const active = this.selectedSeverity() === severity switch (severity) { case 'all': return cn( 'py-0.2 rounded-full px-1.5 text-xs', active ? 'bg-primary-foreground/20 text-primary-foreground' : 'bg-muted text-muted-foreground', ) case 'critical': return cn( 'py-0.2 rounded-full px-1.5 text-xs', active ? 'bg-white/20 text-white' : 'bg-destructive/10 text-destructive', ) case 'warning': return cn( 'py-0.2 rounded-full px-1.5 text-xs', active ? 'bg-white/20 text-white' : 'bg-warning/10 text-warning', ) case 'info': return cn('py-0.2 rounded-full px-1.5 text-xs', active ? 'bg-white/20 text-white' : 'bg-info/10 text-info') } } togglePause(): void { this.isPaused.update((prev) => !prev) } toggleExpand(id: string): void { this.expandedIds.update((prev) => ({ ...prev, [id]: !prev[id] })) } onRowKeydown(e: KeyboardEvent, id: string): void { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() this.toggleExpand(id) } } toggleAllExpand(): void { if (this.allExpanded()) { this.expandedIds.set({}) } else { const next: Record<string, boolean> = {} this.filteredEvents().forEach((e) => { next[e.id] = true }) this.expandedIds.set(next) } } clearStream(): void { this.events.set([]) this.expandedIds.set({}) } resetStream(): void { this.events.set(this.initialEvents ? [...this.initialEvents] : [...defaultEvents]) this.searchQuery.set('') this.selectedSeverity.set('all') this.expandedIds.set({ 'evt-1094': true }) } copyPayload(id: string, metadata: Record<string, unknown>): void { if (typeof navigator !== 'undefined' && navigator.clipboard) { navigator.clipboard.writeText(JSON.stringify(metadata, null, 2)) this.copiedId.set(id) setTimeout(() => { this.copiedId.update((current) => (current === id ? null : current)) }, 2000) } } exportJson(): void { const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(this.filteredEvents(), null, 2)) if (typeof document !== 'undefined') { const downloadAnchor = document.createElement('a') downloadAnchor.setAttribute('href', dataStr) downloadAnchor.setAttribute( 'download', `audit-event-stream-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`, ) document.body.appendChild(downloadAnchor) downloadAnchor.click() downloadAnchor.remove() this.exportedNotice.set(true) setTimeout(() => { this.exportedNotice.set(false) }, 2000) } } }src/app/components/blocks/audit-event-stream/event-detail-drawer.component.ts3.7 kB
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core' import { Bot, Check, Copy, LucideAngularModule, Server, Terminal, User } from 'lucide-angular' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import type { AuditStreamEvent } from './audit-event-stream.component' /** Expanded payload view for one audit event: target banner + syntax-styled JSON metadata box. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-event-detail-drawer, [ui-event-detail-drawer]', standalone: true, host: { class: 'block' }, imports: [LucideAngularModule, UiBadgeComponent, UiButtonComponent], template: ` <div class="bg-muted/15 border-t px-4 py-3.5"> <div class="space-y-3"> <!-- Target Resource Banner --> <div class="bg-card flex flex-col gap-2 rounded-md border p-2.5 text-xs sm:flex-row sm:items-center sm:justify-between" > <div class="flex min-w-0 items-center gap-2"> <span class="text-muted-foreground shrink-0 font-medium">Target:</span> <span class="text-foreground truncate font-mono font-medium">{{ event.target }}</span> </div> <div class="flex items-center gap-2"> <span class="text-muted-foreground text-xs">Actor Type:</span> <span ui-badge variant="outline" class="font-mono text-xs capitalize"> @if (event.actor.type === 'user') { <lucide-icon [img]="User" class="size-3" aria-hidden="true" /> } @else if (event.actor.type === 'service') { <lucide-icon [img]="Bot" class="size-3" aria-hidden="true" /> } @else { <lucide-icon [img]="Server" class="size-3" aria-hidden="true" /> } {{ event.actor.type }} </span> </div> </div> <!-- Syntax Styled JSON Metadata Box --> <div class="overflow-hidden rounded-md border bg-zinc-950 font-mono text-xs text-zinc-100 shadow-inner"> <div class="flex items-center justify-between gap-x-2 border-b border-zinc-800 bg-zinc-900/90 px-3 py-1.5"> <div class="flex items-center gap-2"> <lucide-icon [img]="Terminal" class="text-muted-foreground size-3.5" aria-hidden="true" /> <span class="text-muted-foreground text-xs">payload_metadata.json</span> </div> <button ui-button variant="ghost" size="xs" class="h-6 gap-1 px-2 text-xs text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100" (click)="copy.emit()" > @if (copied) { <lucide-icon [img]="Check" class="text-success size-3" aria-hidden="true" /> } @else { <lucide-icon [img]="Copy" class="size-3" aria-hidden="true" /> } <span>{{ copied ? 'Copied' : 'Copy JSON' }}</span> </button> </div> <pre class="max-h-56 overflow-x-auto overflow-y-auto p-3 text-xs leading-relaxed select-text" ><code class="text-success">{{ json }}</code></pre> </div> </div> </div> `, }) export class UiEventDetailDrawerComponent { @Input({ required: true }) event!: AuditStreamEvent @Input() copied = false @Output() readonly copy = new EventEmitter<void>() protected readonly Bot = Bot protected readonly Check = Check protected readonly Copy = Copy protected readonly Server = Server protected readonly Terminal = Terminal protected readonly User = User get json(): string { return JSON.stringify(this.event.metadata, null, 2) } }src/app/components/blocks/audit-event-stream/index.ts0.2 kB
export { UiAuditEventStreamComponent, type AuditStreamEvent, type EventSeverity } from './audit-event-stream.component' export { UiEventDetailDrawerComponent } from './event-detail-drawer.component'
Raw manifest:https://uipkge.dev/r/angular/audit-event-stream.json