Kafka Stream Monitor
blockdevopsApache 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
$pnpm dlx uipkge-ng@latest add kafka-stream-monitor$npx uipkge-ng@latest add kafka-stream-monitor$yarn dlx uipkge-ng@latest add kafka-stream-monitor$bunx uipkge-ng@latest add kafka-stream-monitorManifest:
https://uipkge.dev/r/angular/kafka-stream-monitor.jsonInstalls to:src/app/components/blocks/kafka-stream-monitor/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.
KafkaStreamMessageinterface KafkaStreamMessage {
id: string
offset: number
partition: number
key: string
timestamp: string
eventType: string
sizeBytes: number
schemaId: number
traceId: string
payload: Record<string, unknown>
}PartitionTelemetryRowinterface 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
export { UiKafkaStreamMonitorComponent, type KafkaStreamMessage, type PartitionTelemetryRow, } from './kafka-stream-monitor.component'
Raw manifest:https://uipkge.dev/r/angular/kafka-stream-monitor.json