Menu

Change language

GitHub repo

Audit Event Stream

blockdevops

Real-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

$npx uipkge-ng@latest add audit-event-stream
Manifest:https://uipkge.dev/r/angular/audit-event-stream.jsonInstalls to:src/app/components/blocks/audit-event-stream/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

AuditStreamEvent
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>
}

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
  • src/app/components/blocks/audit-event-stream/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/audit-event-stream.json