Menu

Change language

GitHub repo

Kafka Stream Monitor

blockdevops

Apache Kafka & Redpanda topic stream telemetry dashboard: partition offset monitor, leader broker distribution, consumer group lag tracker, and real-time message payload inspector with deserialized JSON viewer.

Installation

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

Variants

Loading interactive previews…

Schema

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

KafkaStreamMessage
interface KafkaStreamMessage {
  id: string
  offset: number
  partition: number
  key: string
  timestamp: string
  eventType: string
  sizeBytes: number
  schemaId: number
  traceId: string
  payload: Record<string, unknown>
}
PartitionTelemetryRow
interface PartitionTelemetryRow {
  partitionId: number
  leaderBroker: string
  rack: string
  logEndOffset: number
  consumerOffset: number
  lag: number
  messageRate: string
  isrCount: number
  replicasCount: number
  status: 'healthy' | 'warning'
}

Files installed (2)

  • src/app/components/blocks/kafka-stream-monitor/kafka-stream-monitor.component.ts37.5 kB
    // Apache Kafka topic stream telemetry dashboard: partition offset monitor, leader broker
    // distribution, consumer group lag tracker, and real-time message payload inspector with
    // deserialized JSON viewer. Port of the React block 1:1 (sample data, copy, filters).
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      Activity,
      Check,
      CheckCircle2,
      ChevronDown,
      ChevronRight,
      Copy,
      Filter,
      Gauge,
      HardDrive,
      Layers,
      LucideAngularModule,
      Pause,
      Play,
      PlusCircle,
      Radio,
      RotateCcw,
      Search,
      ShieldCheck,
      Terminal,
      Trash2,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export interface KafkaStreamMessage {
      id: string
      offset: number
      partition: number
      key: string
      timestamp: string
      eventType: string
      sizeBytes: number
      schemaId: number
      traceId: string
      payload: Record<string, unknown>
    }
    
    export interface PartitionTelemetryRow {
      partitionId: number
      leaderBroker: string
      rack: string
      logEndOffset: number
      consumerOffset: number
      lag: number
      messageRate: string
      isrCount: number
      replicasCount: number
      status: 'healthy' | 'warning'
    }
    
    const defaultPartitions: PartitionTelemetryRow[] = [
      {
        partitionId: 0,
        leaderBroker: 'Broker #102',
        rack: 'us-east-1a',
        logEndOffset: 1482910,
        consumerOffset: 1482910,
        lag: 0,
        messageRate: '1.2k msg/s',
        isrCount: 3,
        replicasCount: 3,
        status: 'healthy',
      },
      {
        partitionId: 1,
        leaderBroker: 'Broker #103',
        rack: 'us-east-1b',
        logEndOffset: 1482884,
        consumerOffset: 1482884,
        lag: 0,
        messageRate: '1.1k msg/s',
        isrCount: 3,
        replicasCount: 3,
        status: 'healthy',
      },
      {
        partitionId: 2,
        leaderBroker: 'Broker #101',
        rack: 'us-east-1c',
        logEndOffset: 1482905,
        consumerOffset: 1482905,
        lag: 0,
        messageRate: '1.4k msg/s',
        isrCount: 3,
        replicasCount: 3,
        status: 'healthy',
      },
      {
        partitionId: 3,
        leaderBroker: 'Broker #104',
        rack: 'us-east-1a',
        logEndOffset: 1482872,
        consumerOffset: 1482872,
        lag: 0,
        messageRate: '1.2k msg/s',
        isrCount: 3,
        replicasCount: 3,
        status: 'healthy',
      },
    ]
    
    const defaultMessages: KafkaStreamMessage[] = [
      {
        id: 'msg-1482910',
        offset: 1482910,
        partition: 0,
        key: 'cust_99201a',
        timestamp: '2026-08-21 14:32:05.812 UTC',
        eventType: 'checkout.order_completed',
        sizeBytes: 1420,
        schemaId: 104,
        traceId: '00-4bf92f3577b34da6a3ce929d0e0e4736',
        payload: {
          order_id: 'ord_9920184a',
          customer_id: 'cust_99201a',
          checkout_session: 'cs_live_992019482',
          currency: 'USD',
          total_amount: 349.5,
          item_count: 3,
          items: [
            { sku: 'SKU-PRO-4K', name: 'UltraHD Pro Monitor 32-inch', qty: 1, price: 299.0 },
            { sku: 'SKU-USB-C-2M', name: 'Braided Thunderbolt 4 Cable 2m', qty: 2, price: 25.25 },
          ],
          payment_status: 'authorized',
          fraud_risk_score: 0.02,
          shipping_destination: 'US-CA-94105',
          produced_at_epoch_ms: 1787322725812,
        },
      },
      {
        id: 'msg-1482909',
        offset: 1482909,
        partition: 2,
        key: 'cust_88301b',
        timestamp: '2026-08-21 14:31:58.240 UTC',
        eventType: 'checkout.payment_authorized',
        sizeBytes: 890,
        schemaId: 104,
        traceId: '00-7c201a9981f34ec189aa1029481920aa',
        payload: {
          transaction_id: 'txn_8830192b',
          customer_id: 'cust_88301b',
          payment_provider: 'stripe_us_direct',
          amount: 129.0,
          currency: 'USD',
          card_brand: 'visa',
          last4: '4242',
          avs_check: 'matched',
          cvv_check: 'matched',
          '3ds_authenticated': true,
          latency_ms: 142,
        },
      },
      {
        id: 'msg-1482908',
        offset: 1482908,
        partition: 1,
        key: 'cust_44109c',
        timestamp: '2026-08-21 14:31:44.102 UTC',
        eventType: 'checkout.cart_updated',
        sizeBytes: 1150,
        schemaId: 104,
        traceId: '00-11209e8832a44bb0902188492019ab23',
        payload: {
          cart_id: 'cart_4410912c',
          customer_id: 'cust_44109c',
          action: 'item_added',
          sku: 'SKU-MECH-KEYBOARD',
          name: 'Custom Wireless Mechanical Keyboard',
          quantity: 1,
          unit_price: 189.0,
          applied_promo_code: 'DEVCON2026',
          discount_amount: 18.9,
          cart_subtotal: 170.1,
        },
      },
      {
        id: 'msg-1482907',
        offset: 1482907,
        partition: 3,
        key: 'cust_11094d',
        timestamp: '2026-08-21 14:31:22.955 UTC',
        eventType: 'checkout.shipping_calculated',
        sizeBytes: 760,
        schemaId: 104,
        traceId: '00-66a9104882194cc2810992384719bb81',
        payload: {
          quote_id: 'shp_1109482d',
          customer_id: 'cust_11094d',
          carrier: 'FedEx Express',
          service_tier: 'Priority Overnight',
          estimated_delivery: '2026-08-22T10:30:00Z',
          rate_usd: 24.5,
          origin_postal: '94105',
          destination_postal: '10001',
          weight_kg: 1.85,
        },
      },
      {
        id: 'msg-1482906',
        offset: 1482906,
        partition: 0,
        key: 'cust_77210e',
        timestamp: '2026-08-21 14:31:05.419 UTC',
        eventType: 'checkout.address_validated',
        sizeBytes: 640,
        schemaId: 104,
        traceId: '00-559012bb99304ff091920394881029cc',
        payload: {
          validation_id: 'val_7721098e',
          customer_id: 'cust_77210e',
          country: 'US',
          state: 'CA',
          city: 'San Francisco',
          postal_code: '94107',
          is_residential: true,
          standardized: true,
          dpv_match: 'Y',
          service: 'smartystreets_v2',
        },
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-kafka-stream-monitor, [ui-kafka-stream-monitor]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
        UiProgressComponent,
        UiSeparatorComponent,
        UiTableComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="kafka-stream-monitor" [class]="rootClass">
          <!-- Header Section -->
          <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <div class="space-y-1.5">
              <div class="flex flex-wrap items-center gap-2.5">
                <div
                  class="bg-muted text-muted-foreground border-border flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                  aria-hidden="true"
                >
                  <lucide-icon [img]="Radio" class="text-primary size-4.5" />
                </div>
                <div class="flex flex-wrap items-center gap-2">
                  <span class="text-muted-foreground font-mono text-xs font-medium">production-kafka-us-east-1</span>
                  <span class="text-muted-foreground text-xs">/</span>
                  <h1 class="text-foreground font-mono text-xl font-bold tracking-tight break-all sm:text-2xl">
                    events.customer.checkout_v2
                  </h1>
                </div>
                <span ui-badge variant="success" class="gap-1.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>
                  <span>Healthy · Zero Consumer Lag</span>
                </span>
              </div>
              <p class="text-muted-foreground text-xs">
                12 Partitions · Replication Factor: 3 · min.insync.replicas: 2 · Cleanup Policy: delete,compact
              </p>
            </div>
    
            <div class="flex flex-wrap items-center gap-2">
              <button ui-button variant="outline" size="sm" class="gap-1.5 text-xs" (click)="resetStream()">
                <lucide-icon [img]="RotateCcw" class="size-3.5" />
                <span>Reset</span>
              </button>
              <button ui-button variant="default" size="sm" class="gap-1.5 text-xs" (click)="produceTestMessage()">
                <lucide-icon [img]="PlusCircle" class="size-3.5" />
                <span>Produce Test Message</span>
              </button>
            </div>
          </div>
    
          <!-- Active Produce Alert Banner -->
          @if (produceNotice()) {
            <div
              class="border-border bg-card flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs"
              role="status"
            >
              <div class="flex items-center gap-2">
                <lucide-icon [img]="CheckCircle2" class="text-primary size-4 shrink-0" />
                <span class="text-foreground font-medium">{{ produceNotice() }}</span>
              </div>
              <span ui-badge variant="outline" class="font-mono text-xs">Offset Ack: OK (2ms)</span>
            </div>
          }
    
          <!-- 4 Kafka Telemetry Metric Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header class="pb-2">
                <div class="flex items-center justify-between gap-2">
                  <div class="flex min-w-0 items-center gap-2">
                    <div
                      class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                      aria-hidden="true"
                    >
                      <lucide-icon [img]="Activity" class="size-4" />
                    </div>
                    <h3 ui-card-title class="truncate text-xs font-medium">Message Inflow Rate</h3>
                  </div>
                  <span ui-badge variant="outline" class="shrink-0 text-xs font-normal tabular-nums">+5.8% 1h</span>
                </div>
              </div>
              <div ui-card-content class="space-y-3">
                <div>
                  <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">14,250 msgs/sec</div>
                  <p class="text-muted-foreground text-xs tabular-nums">2.4 MB/s throughput</p>
                </div>
                <div class="space-y-1.5">
                  <div class="text-muted-foreground flex items-center justify-between text-xs">
                    <span>Ingress Bandwidth</span>
                    <span class="text-foreground font-medium tabular-nums">48% of cap</span>
                  </div>
                  <ui-progress [value]="48" class="h-1.5" />
                </div>
              </div>
            </div>
    
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header class="pb-2">
                <div class="flex items-center justify-between gap-2">
                  <div class="flex min-w-0 items-center gap-2">
                    <div
                      class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                      aria-hidden="true"
                    >
                      <lucide-icon [img]="Gauge" class="size-4" />
                    </div>
                    <h3 ui-card-title class="truncate text-xs font-medium">Consumer Group Lag</h3>
                  </div>
                  <span ui-badge variant="success" class="shrink-0 text-xs">Real-time</span>
                </div>
              </div>
              <div ui-card-content class="space-y-3">
                <div>
                  <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">0 messages lag</div>
                  <p class="text-muted-foreground text-xs">across 4 consumer groups</p>
                </div>
                <div class="space-y-1.5">
                  <div class="text-muted-foreground flex items-center justify-between text-xs">
                    <span>Processing Drift</span>
                    <span class="text-foreground font-medium tabular-nums">0 ms lag</span>
                  </div>
                  <ui-progress [value]="100" class="h-1.5" />
                </div>
              </div>
            </div>
    
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header class="pb-2">
                <div class="flex items-center justify-between gap-2">
                  <div class="flex min-w-0 items-center gap-2">
                    <div
                      class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                      aria-hidden="true"
                    >
                      <lucide-icon [img]="HardDrive" class="size-4" />
                    </div>
                    <h3 ui-card-title class="truncate text-xs font-medium">Total Retention</h3>
                  </div>
                  <span ui-badge variant="outline" class="shrink-0 text-xs font-normal">7 Days</span>
                </div>
              </div>
              <div ui-card-content class="space-y-3">
                <div>
                  <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">480 GB total footprint</div>
                  <p class="text-muted-foreground text-xs tabular-nums">40 GB avg / partition</p>
                </div>
                <div class="space-y-1.5">
                  <div class="text-muted-foreground flex items-center justify-between text-xs">
                    <span>Disk Footprint</span>
                    <span class="text-foreground font-medium tabular-nums">480 GB / 1.0 TB</span>
                  </div>
                  <ui-progress [value]="48" class="h-1.5" />
                </div>
              </div>
            </div>
    
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header class="pb-2">
                <div class="flex items-center justify-between gap-2">
                  <div class="flex min-w-0 items-center gap-2">
                    <div
                      class="bg-muted text-muted-foreground border-border flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                      aria-hidden="true"
                    >
                      <lucide-icon [img]="ShieldCheck" class="size-4" />
                    </div>
                    <h3 ui-card-title class="truncate text-xs font-medium">Under-Replicated</h3>
                  </div>
                  <span ui-badge variant="success" class="shrink-0 text-xs">In-Sync</span>
                </div>
              </div>
              <div ui-card-content class="space-y-3">
                <div>
                  <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">0 / 12 Partitions</div>
                  <p class="text-muted-foreground text-xs">All 36 replicas fully in-sync (ISR)</p>
                </div>
                <div class="space-y-1.5">
                  <div class="text-muted-foreground flex items-center justify-between text-xs">
                    <span>Replication Health</span>
                    <span class="text-foreground font-medium tabular-nums">100% ISR</span>
                  </div>
                  <ui-progress [value]="100" class="h-1.5" />
                </div>
              </div>
            </div>
          </div>
    
          <!-- Partitions & Offset Distribution Table -->
          <div ui-card class="shadow-xs">
            <div ui-card-header>
              <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <h3 ui-card-title class="text-base">Partitions & Offset Distribution</h3>
                  <p ui-card-description class="text-xs">
                    Per-partition log high-water marks, consumer group commitments, and leader broker assignments.
                  </p>
                </div>
                <span ui-badge variant="outline" class="w-fit font-mono text-xs font-normal tabular-nums">
                  Showing 4 of 12 Partitions
                </span>
              </div>
            </div>
            <div ui-card-content class="p-0 sm:p-6 sm:pt-0">
              <div class="overflow-x-auto">
                <table ui-table>
                  <thead ui-table-header>
                    <tr ui-table-row>
                      <th ui-table-head class="text-xs">Partition ID</th>
                      <th ui-table-head class="text-xs">Leader Broker</th>
                      <th ui-table-head class="text-right text-xs">Log End Offset</th>
                      <th ui-table-head class="text-right text-xs">Consumer Offset</th>
                      <th ui-table-head class="text-right text-xs">Lag</th>
                      <th ui-table-head class="text-right text-xs">Message Rate</th>
                      <th ui-table-head class="text-right text-xs">Replicas (ISR)</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (partition of partitions(); track partition.partitionId) {
                      <tr ui-table-row>
                        <td ui-table-cell class="text-xs font-medium">
                          <div class="flex items-center gap-2">
                            <span class="bg-success size-2 rounded-full" aria-hidden="true"></span>
                            <span class="font-mono">Partition {{ partition.partitionId }}</span>
                          </div>
                        </td>
                        <td ui-table-cell class="text-xs">
                          <div class="flex items-center gap-1.5">
                            <span class="text-foreground font-medium">{{ partition.leaderBroker }}</span>
                            <span class="text-muted-foreground font-mono text-xs">({{ partition.rack }})</span>
                          </div>
                        </td>
                        <td ui-table-cell class="text-foreground text-right font-mono text-xs font-medium tabular-nums">
                          {{ partition.logEndOffset.toLocaleString() }}
                        </td>
                        <td ui-table-cell class="text-muted-foreground text-right font-mono text-xs tabular-nums">
                          {{ partition.consumerOffset.toLocaleString() }}
                        </td>
                        <td ui-table-cell class="text-right">
                          <span ui-badge variant="success" class="font-mono text-xs tabular-nums">{{ partition.lag }}</span>
                        </td>
                        <td ui-table-cell class="text-foreground text-right font-mono text-xs font-medium tabular-nums">
                          {{ partition.messageRate }}
                        </td>
                        <td ui-table-cell class="text-right">
                          <span ui-badge variant="outline" class="font-mono text-xs tabular-nums">
                            {{ partition.isrCount }} / {{ partition.replicasCount }} In-Sync
                          </span>
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
    
          <!-- Live Message Stream Inspector -->
          <div ui-card class="shadow-xs">
            <div class="bg-muted/40 flex flex-col gap-3 border-b p-3.5 sm:flex-row sm:items-center sm:justify-between">
              <div class="flex items-center gap-2.5">
                <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>
                <div class="flex items-center gap-2">
                  <span class="text-foreground text-xs font-semibold">Live Message Stream Inspector</span>
                  @if (!isPaused()) {
                    <div
                      class="border-border bg-card inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-medium"
                    >
                      <span class="relative flex size-1.5 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-1.5 rounded-full"></span>
                      </span>
                      <span class="text-muted-foreground">Streaming</span>
                    </div>
                  } @else {
                    <div
                      class="border-border bg-card inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-medium"
                    >
                      <span class="bg-warning relative inline-flex size-1.5 rounded-full"></span>
                      <span class="text-muted-foreground">Paused</span>
                    </div>
                  }
                </div>
              </div>
    
              <div class="flex flex-wrap items-center gap-2">
                <button
                  ui-button
                  variant="outline"
                  size="xs"
                  class="h-7 gap-1 text-xs"
                  [disabled]="filteredMessages().length === 0"
                  (click)="toggleAllExpand()"
                >
                  @if (allExpanded()) {
                    <lucide-icon [img]="ChevronDown" class="size-3" />
                  } @else {
                    <lucide-icon [img]="ChevronRight" class="size-3" />
                  }
                  <span>{{ allExpanded() ? 'Collapse All' : 'Expand All' }}</span>
                </button>
                <button ui-button variant="outline" size="xs" class="h-7 gap-1 text-xs" (click)="togglePause()">
                  @if (isPaused()) {
                    <lucide-icon [img]="Play" class="size-3" />
                  } @else {
                    <lucide-icon [img]="Pause" class="size-3" />
                  }
                  <span>{{ isPaused() ? 'Resume' : 'Pause' }}</span>
                </button>
                <button
                  ui-button
                  variant="outline"
                  size="xs"
                  class="h-7 gap-1 text-xs"
                  [disabled]="messages().length === 0"
                  (click)="clearStream()"
                >
                  <lucide-icon [img]="Trash2" class="size-3" />
                  <span>Clear</span>
                </button>
              </div>
            </div>
    
            <div class="bg-card/50 flex flex-col gap-3 border-b p-3 sm:flex-row sm:items-center sm:justify-between">
              <div class="relative flex-1">
                <lucide-icon
                  [img]="Search"
                  class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2"
                />
                <ui-input
                  [value]="searchQuery()"
                  (valueChange)="searchQuery.set($event)"
                  placeholder="Filter message key, offset, event type, or JSON payload..."
                  class="h-8 pl-8 text-xs"
                />
              </div>
              <div class="flex flex-wrap items-center gap-1">
                <span class="text-muted-foreground mr-1 flex items-center gap-1 text-xs font-medium">
                  <lucide-icon [img]="Filter" class="size-3" />
                  Partition:
                </span>
                <button type="button" [class]="partitionPillClass('all')" (click)="selectedPartition.set('all')">
                  All
                </button>
                @for (p of partitionIds; track p) {
                  <button type="button" [class]="partitionPillClass(p)" (click)="selectedPartition.set(p)">
                    P-{{ p }}
                  </button>
                }
              </div>
            </div>
    
            <div ui-card-content class="p-0">
              @if (filteredMessages().length === 0) {
                <div class="flex flex-col items-center justify-center gap-2.5 px-4 py-12 text-center">
                  <div class="bg-muted flex size-10 items-center justify-center rounded-full">
                    <lucide-icon [img]="Radio" class="text-muted-foreground size-5 opacity-60" />
                  </div>
                  <div class="space-y-1">
                    <p class="text-foreground text-sm font-semibold">No messages found</p>
                    <p class="text-muted-foreground text-xs">
                      {{
                        messages().length === 0
                          ? 'Message buffer was cleared. Reset to restore sample messages or produce a test event.'
                          : 'Try clearing the search query or changing partition filter.'
                      }}
                    </p>
                  </div>
                  <button ui-button variant="outline" size="sm" class="mt-1 gap-1.5 text-xs" (click)="resetStream()">
                    <lucide-icon [img]="RotateCcw" class="size-3.5" />
                    <span>Reset Stream</span>
                  </button>
                </div>
              } @else {
                <ul class="divide-border/60 divide-y">
                  @for (msg of filteredMessages(); track msg.id) {
                    <li [class]="rowClass(msg.offset)">
                      <div
                        role="button"
                        tabindex="0"
                        [attr.aria-expanded]="!!expandedOffsets()[msg.offset]"
                        class="focus-visible:ring-ring flex cursor-pointer flex-col gap-2.5 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(msg.offset)"
                        (keydown.enter)="toggleExpandKey($event, msg.offset)"
                        (keydown.space)="toggleExpandKey($event, msg.offset)"
                      >
                        <div class="flex min-w-0 items-start gap-3 sm:items-center">
                          <span
                            class="text-muted-foreground mt-0.5 shrink-0 transition-transform sm:mt-0"
                            [class.rotate-90]="expandedOffsets()[msg.offset]"
                          >
                            <lucide-icon [img]="ChevronRight" class="size-4" />
                          </span>
                          <div class="space-y-1">
                            <div class="flex flex-wrap items-center gap-2">
                              <span class="text-foreground font-mono text-xs font-bold tabular-nums">
                                #{{ msg.offset.toLocaleString() }}
                              </span>
                              <span ui-badge variant="outline" class="font-mono text-xs">P-{{ msg.partition }}</span>
                              <span ui-badge variant="secondary" class="font-mono text-xs font-normal">
                                key: {{ msg.key }}
                              </span>
                              <span class="text-foreground font-mono text-xs font-semibold">{{ msg.eventType }}</span>
                            </div>
                            <p class="text-muted-foreground font-mono text-xs tabular-nums">{{ msg.timestamp }}</p>
                          </div>
                        </div>
                        <div class="flex shrink-0 items-center gap-2.5 sm:justify-end">
                          <span class="text-muted-foreground font-mono text-xs tabular-nums">
                            {{ formatBytes(msg.sizeBytes) }}
                          </span>
                          <span ui-badge variant="outline" class="hidden font-mono text-xs font-normal sm:inline-flex">
                            schema_id: {{ msg.schemaId }}
                          </span>
                        </div>
                      </div>
    
                      @if (expandedOffsets()[msg.offset]) {
                        <div class="bg-muted/15 border-t px-4 py-3.5">
                          <div class="space-y-3">
                            <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">Trace ID:</span>
                                <span class="text-foreground truncate font-mono">{{ msg.traceId }}</span>
                              </div>
                              <div class="flex items-center gap-2">
                                <span class="text-muted-foreground">Format:</span>
                                <span ui-badge variant="outline" class="font-mono text-xs">json+avro</span>
                              </div>
                            </div>
                            <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 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" />
                                  <span class="text-muted-foreground text-xs">deserialized_payload.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)="copyPayload(msg.id, msg.payload); $event.stopPropagation()"
                                >
                                  @if (copiedId() === msg.id) {
                                    <lucide-icon [img]="Check" class="text-success size-3" />
                                  } @else {
                                    <lucide-icon [img]="Copy" class="size-3" />
                                  }
                                  <span>{{ copiedId() === msg.id ? 'Copied' : 'Copy JSON' }}</span>
                                </button>
                              </div>
                              <pre
                                class="max-h-64 overflow-x-auto overflow-y-auto p-3 text-xs leading-relaxed select-text"
                              ><code class="text-success">{{ stringify(msg.payload) }}</code></pre>
                            </div>
                          </div>
                        </div>
                      }
                    </li>
                  }
                </ul>
              }
            </div>
    
            <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" />
                  <span>Rate:</span>
                  <span class="font-mono">{{ isPaused() ? '0 msg/s (Paused)' : '14,250 msgs/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" />
                  <span>Buffer:</span>
                  <span class="text-foreground font-mono"
                    >{{ filteredMessages().length }} / {{ messages().length }} cached</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" />
                  <span>Avg Latency:</span>
                  <span class="text-foreground font-mono">1.8ms</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>SASL_SSL · LZ4 Compression · Schema Registry OK</span>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiKafkaStreamMonitorComponent {
      protected readonly Activity = Activity
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly ChevronDown = ChevronDown
      protected readonly ChevronRight = ChevronRight
      protected readonly Copy = Copy
      protected readonly Filter = Filter
      protected readonly Gauge = Gauge
      protected readonly HardDrive = HardDrive
      protected readonly Layers = Layers
      protected readonly Pause = Pause
      protected readonly Play = Play
      protected readonly PlusCircle = PlusCircle
      protected readonly Radio = Radio
      protected readonly RotateCcw = RotateCcw
      protected readonly Search = Search
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Terminal = Terminal
      protected readonly Trash2 = Trash2
      protected readonly Zap = Zap
    
      protected readonly partitionIds = [0, 1, 2, 3]
    
      @Input() set initialMessages(v: KafkaStreamMessage[] | undefined) {
        if (v) this.messages.set([...v])
      }
      @Input() set initialPartition(v: number | 'all') {
        this.selectedPartition.set(v ?? 'all')
      }
      @Input() set initialSearch(v: string) {
        this.searchQuery.set(v ?? '')
      }
      @Input('class') className?: string
    
      readonly partitions = signal<PartitionTelemetryRow[]>([...defaultPartitions])
      readonly messages = signal<KafkaStreamMessage[]>([...defaultMessages])
      readonly isPaused = signal(false)
      readonly selectedPartition = signal<number | 'all'>('all')
      readonly searchQuery = signal('')
      readonly expandedOffsets = signal<Record<number, boolean>>({ 1482910: true })
      readonly copiedId = signal<string | null>(null)
      readonly produceNotice = signal<string | null>(null)
      private nextOffset = 1482911
      private copyTimer: ReturnType<typeof setTimeout> | undefined
      private noticeTimer: ReturnType<typeof setTimeout> | undefined
    
      readonly filteredMessages = computed(() => {
        const selected = this.selectedPartition()
        const q = this.searchQuery().trim().toLowerCase()
        return this.messages().filter((msg) => {
          if (selected !== 'all' && msg.partition !== selected) return false
          if (!q) return true
          return (
            msg.key.toLowerCase().includes(q) ||
            msg.eventType.toLowerCase().includes(q) ||
            msg.offset.toString().includes(q) ||
            msg.partition.toString().includes(q) ||
            JSON.stringify(msg.payload).toLowerCase().includes(q)
          )
        })
      })
    
      readonly allExpanded = computed(() => {
        const list = this.filteredMessages()
        return list.length > 0 && list.every((m) => this.expandedOffsets()[m.offset])
      })
    
      get rootClass(): string {
        return cn('mx-auto w-full max-w-6xl space-y-6', this.className)
      }
    
      partitionPillClass(p: number | 'all'): string {
        const active = this.selectedPartition() === p
        return cn(
          'focus-visible:ring-ring inline-flex h-7 items-center rounded-md border px-2.5 font-mono text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none',
          active
            ? 'border-primary bg-primary text-primary-foreground shadow-xs'
            : 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground',
        )
      }
    
      rowClass(offset: number): string {
        return cn('group transition-colors', this.expandedOffsets()[offset] ? 'bg-muted/30' : 'hover:bg-muted/20')
      }
    
      stringify(payload: Record<string, unknown>): string {
        return JSON.stringify(payload, null, 2)
      }
    
      formatBytes(bytes: number): string {
        if (bytes < 1024) return `${bytes} B`
        return `${(bytes / 1024).toFixed(1)} KB`
      }
    
      togglePause(): void {
        this.isPaused.update((v) => !v)
      }
    
      toggleExpand(offset: number): void {
        this.expandedOffsets.update((prev) => ({ ...prev, [offset]: !prev[offset] }))
      }
    
      toggleExpandKey(e: KeyboardEvent, offset: number): void {
        e.preventDefault()
        this.toggleExpand(offset)
      }
    
      toggleAllExpand(): void {
        if (this.allExpanded()) {
          this.expandedOffsets.set({})
        } else {
          const next: Record<number, boolean> = {}
          for (const m of this.filteredMessages()) next[m.offset] = true
          this.expandedOffsets.set(next)
        }
      }
    
      clearStream(): void {
        this.messages.set([])
        this.expandedOffsets.set({})
      }
    
      resetStream(): void {
        this.messages.set([...defaultMessages])
        this.searchQuery.set('')
        this.selectedPartition.set('all')
        this.expandedOffsets.set({ 1482910: true })
      }
    
      copyPayload(id: string, payload: Record<string, unknown>): void {
        try {
          void navigator.clipboard?.writeText(JSON.stringify(payload, null, 2))
          this.copiedId.set(id)
          clearTimeout(this.copyTimer)
          this.copyTimer = setTimeout(() => {
            if (this.copiedId() === id) this.copiedId.set(null)
          }, 2000)
        } catch {
          // Clipboard fallback
        }
      }
    
      produceTestMessage(): void {
        const currentOffset = this.nextOffset++
        const targetPartition = currentOffset % 4
        const rand = Math.random().toString(36).substring(2, 8)
        const newMessage: KafkaStreamMessage = {
          id: `msg-${currentOffset}`,
          offset: currentOffset,
          partition: targetPartition,
          key: `cust_${rand}`,
          timestamp: new Date().toISOString().replace('T', ' ').slice(0, 23) + ' UTC',
          eventType: 'checkout.order_placed',
          sizeBytes: 1280,
          schemaId: 104,
          traceId: `00-${rand}${rand}${rand}${rand}`.slice(0, 35),
          payload: {
            order_id: `ord_${rand}`,
            customer_id: `cust_${rand}`,
            currency: 'USD',
            total_amount: 129.0,
            item_count: 1,
            items: [{ sku: 'SKU-SYNTH-DEVICE', name: 'Developer Workstation Hub', qty: 1, price: 129.0 }],
            payment_status: 'authorized',
            producer: 'console-stream-tester-ui',
            test_message: true,
          },
        }
        this.messages.update((prev) => [newMessage, ...prev])
        this.expandedOffsets.update((prev) => ({ ...prev, [currentOffset]: true }))
        this.produceNotice.set(`Produced message #${currentOffset.toLocaleString()} to Partition ${targetPartition}`)
        clearTimeout(this.noticeTimer)
        this.noticeTimer = setTimeout(() => this.produceNotice.set(null), 3500)
      }
    }
    
  • src/app/components/blocks/kafka-stream-monitor/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/kafka-stream-monitor.json