Menu

Change language

GitHub repo

Stream Processing Flink Topology

blockdevops

Apache Flink & RisingWave stateful streaming graph: real-time DAG topology, window throughput, checkpointing duration, RocksDB state inspector, and backpressure heat monitor.

Installation

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

Variants

Loading interactive previews…

Schema

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

FlinkSubtask
interface FlinkSubtask {
  id: number
  host: string
  status: 'RUNNING' | 'DEPLOYING' | 'RECONCILING'
  inRate: string
  outRate: string
  cpu: string
  heapMemory: string
  backpressure: number
  watermarkLag: string
  checkpointAckMs: number
}
FlinkOperatorNode
interface FlinkOperatorNode {
  id: string
  name: string
  shortTitle: string
  stageNumber: number
  category: 'source' | 'window' | 'inference' | 'sink'
  operatorClass: string
  parallelism: number
  status: 'RUNNING' | 'CANCELED' | 'FAILING'
  backpressure: number
  inputRecordsRate: string
  inputBytesRate: string
  outputRecordsRate: string
  outputBytesRate: string
  totalRecordsIn: number
  totalRecordsOut: number
  managedStateSize: string
  stateBackend: string
  heapMemory: string
  managedMemory: string
  gcTime: string
  cpuPeak: string
  watermarkLag: string
  currentWatermark: string
  description: string
  config: Record<string, string>
  subtasks: FlinkSubtask[]
}

Files installed (6)

  • src/app/components/blocks/stream-processing-flink-topology/stream-processing-flink-topology.component.ts21.6 kB
    // Apache Flink & RisingWave stateful streaming graph: real-time DAG topology, window
    // throughput, checkpointing duration, RocksDB state inspector, and backpressure heat
    // monitor. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      AlertTriangle,
      ArrowRight,
      Camera,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Cpu,
      Database,
      Loader2,
      LucideAngularModule,
      Radio,
      Square,
      Workflow,
    } 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 { UiStreamOperatorDetailComponent } from './stream-operator-detail.component'
    import { UiStreamTopologyTelemetryComponent } from './stream-topology-telemetry.component'
    import type { FlinkOperatorNode, FlinkSubtask } from './stream-topology-types'
    import { defaultFlinkOperators } from './stream-topology-data'
    
    export type { FlinkOperatorNode, FlinkSubtask }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-stream-processing-flink-topology, [ui-stream-processing-flink-topology]',
      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,
        UiStreamOperatorDetailComponent,
        UiStreamTopologyTelemetryComponent,
      ],
      template: `
        <div data-slot="stream-processing-flink-topology" [class]="rootClass">
          <!-- 1. 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]="Workflow" class="text-primary size-4.5" />
                </div>
                <div class="flex flex-wrap items-center gap-2">
                  <h1 class="text-foreground font-mono text-xl font-bold tracking-tight break-all sm:text-2xl">
                    realtime-fraud-scoring-pipeline
                  </h1>
                  <span ui-badge variant="outline" class="font-mono text-xs">Flink 1.19 on Kubernetes</span>
                </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>Job Running · Checkpointing Healthy</span>
                </span>
              </div>
              <p class="text-muted-foreground font-mono text-xs">
                Checkpoint Interval: Every 10s · RocksDB StateBackend · SLA Target: &lt; 50ms Lag · Job ID: flink_job_992014
              </p>
            </div>
    
            <!-- Action Buttons -->
            <div class="flex flex-wrap items-center gap-2">
              <button
                ui-button
                variant="outline"
                size="sm"
                class="gap-1.5 text-xs font-medium"
                [disabled]="isTriggeringSavepoint()"
                (click)="handleTriggerSavepoint()"
              >
                @if (isTriggeringSavepoint()) {
                  <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" />
                } @else {
                  <lucide-icon [img]="Camera" class="text-info size-3.5" />
                }
                <span>{{ isTriggeringSavepoint() ? 'Creating Savepoint...' : 'Trigger Savepoint' }}</span>
              </button>
    
              <button
                ui-button
                variant="outline"
                size="sm"
                class="text-destructive hover:border-destructive/40 hover:bg-destructive/10 gap-1.5 text-xs font-medium"
                [disabled]="isCanceling()"
                (click)="handleCancelJob()"
              >
                @if (isCanceling()) {
                  <lucide-icon [img]="Loader2" class="size-3.5 animate-spin" />
                } @else {
                  <lucide-icon [img]="Square" class="size-3.5 fill-current" />
                }
                <span>{{ isCanceling() ? 'Canceling with Savepoint...' : 'Cancel Job with Savepoint' }}</span>
              </button>
            </div>
          </div>
    
          <!-- Active Savepoint Notice Banner -->
          @if (savepointNotice(); as notice) {
            <div
              class="text-foreground border-info/30 bg-info/10 flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs"
              role="status"
            >
              <div class="flex items-center gap-2.5">
                <lucide-icon [img]="CheckCircle2" class="text-info size-4 shrink-0" />
                <div>
                  <span class="text-info font-semibold">Savepoint completed successfully:</span>
                  <span class="ml-1 font-mono text-xs">{{ notice }}</span>
                </div>
              </div>
              <button
                ui-button
                variant="ghost"
                size="xs"
                class="h-6 gap-1 px-2 text-xs"
                (click)="copyText('savepoint', notice)"
              >
                @if (copiedKey() === 'savepoint') {
                  <lucide-icon [img]="Check" class="text-success size-3" />
                } @else {
                  <lucide-icon [img]="Copy" class="size-3" />
                }
                <span>{{ copiedKey() === 'savepoint' ? 'Copied' : 'Copy URI' }}</span>
              </button>
            </div>
          }
    
          <!-- Active Cancellation Notice Banner -->
          @if (cancelConfirmed()) {
            <div
              class="text-foreground border-warning/30 bg-warning/10 flex items-center justify-between rounded-lg border p-3 text-xs shadow-xs"
              role="status"
            >
              <div class="flex items-center gap-2.5">
                <lucide-icon [img]="AlertTriangle" class="text-warning size-4 shrink-0" />
                <div>
                  <span class="text-warning font-semibold">Graceful Job Drain Initiated:</span>
                  <span class="ml-1">Stopping topology sources and committing final RocksDB savepoint to S3 storage.</span>
                </div>
              </div>
              <span ui-badge variant="outline" class="font-mono text-xs">Drain Status: 100%</span>
            </div>
          }
    
          <!-- 2. 4 Stream Telemetry KPI Cards -->
          <ui-stream-topology-telemetry />
    
          <!-- 3. Interactive 4-Operator Streaming Topology DAG -->
          <div ui-card class="shadow-xs">
            <div ui-card-header class="pb-3">
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <div class="flex items-center gap-2">
                    <h3 ui-card-title class="text-base font-semibold">Streaming Execution Topology DAG</h3>
                    <span ui-badge variant="outline" class="font-mono text-xs">4 Stateful Operators</span>
                  </div>
                  <p ui-card-description class="text-xs">
                    Live dataflow graph with event-time watermarking, state sizes, and backpressure telemetry. Click any
                    operator to inspect metrics and subtasks.
                  </p>
                </div>
                <div class="flex flex-wrap items-center gap-2">
                  <div
                    class="border-border bg-muted/40 text-muted-foreground flex items-center gap-1.5 rounded-md border px-2.5 py-1 font-mono text-xs"
                  >
                    <span class="bg-success size-2 rounded-full"></span>
                    <span>Backpressure Heat: 0.0% (Green)</span>
                  </div>
                  <div
                    class="border-border bg-muted/40 text-muted-foreground flex items-center gap-1.5 rounded-md border px-2.5 py-1 font-mono text-xs"
                  >
                    <lucide-icon [img]="Database" class="size-3" />
                    <span>Total Managed State: 57.0 MB</span>
                  </div>
                </div>
              </div>
            </div>
    
            <div ui-card-content class="p-4 pt-1 sm:p-6">
              <div class="border-border bg-muted/15 rounded-xl border p-4 sm:p-5">
                <!-- 4 Connected Nodes Grid -->
                <div class="grid grid-cols-1 gap-4 lg:grid-cols-4">
                  <!-- Node 1: Kafka Ingestion Source -->
                  <div
                    role="button"
                    tabindex="0"
                    [class]="nodeClass('op-kafka-source')"
                    (click)="selectOperator('op-kafka-source')"
                    (keydown.enter)="selectOperator('op-kafka-source')"
                    (keydown.space)="$event.preventDefault(); selectOperator('op-kafka-source')"
                  >
                    <div class="space-y-3">
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <div
                            class="border-info/30 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border"
                          >
                            <lucide-icon [img]="Radio" class="size-3.5" />
                          </div>
                          <span ui-badge variant="outline" class="border-info/30 text-info font-mono text-xs">SOURCE</span>
                        </div>
                        <span ui-badge variant="outline" class="font-mono text-xs">p=8</span>
                      </div>
    
                      <div>
                        <div class="text-foreground font-mono text-xs leading-snug font-bold">Kafka Ingestion Source</div>
                        <p class="text-muted-foreground mt-1 font-mono text-xs">42.5k rec/s · Backpressure: 0%</p>
                      </div>
    
                      <div class="border-border/60 space-y-1.5 border-t pt-2.5 font-mono text-xs">
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>Throughput:</span>
                          <span class="text-foreground font-semibold tabular-nums">8.4 MB/s</span>
                        </div>
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>State Size:</span>
                          <span class="text-foreground tabular-nums">0 MB (Stateless)</span>
                        </div>
                      </div>
                    </div>
    
                    <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2 text-xs">
                      <span class="text-muted-foreground font-mono">events.fraud.transactions</span>
                      <lucide-icon
                        [img]="ArrowRight"
                        class="text-muted-foreground group-hover:text-primary size-3.5 shrink-0 transition-colors"
                      />
                    </div>
                  </div>
    
                  <!-- Node 2: Keyed Sliding Window -->
                  <div
                    role="button"
                    tabindex="0"
                    [class]="nodeClass('op-keyed-window')"
                    (click)="selectOperator('op-keyed-window')"
                    (keydown.enter)="selectOperator('op-keyed-window')"
                    (keydown.space)="$event.preventDefault(); selectOperator('op-keyed-window')"
                  >
                    <div class="space-y-3">
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <div
                            class="border-chart-1/30 bg-chart-1/10 text-chart-1 flex size-7 items-center justify-center rounded-md border"
                          >
                            <lucide-icon [img]="Clock" class="size-3.5" />
                          </div>
                          <span ui-badge variant="outline" class="border-chart-1/30 text-chart-1 font-mono text-xs"
                            >WINDOW</span
                          >
                        </div>
                        <span ui-badge variant="outline" class="font-mono text-xs">p=16</span>
                      </div>
    
                      <div>
                        <div class="text-foreground font-mono text-xs leading-snug font-bold">Keyed Sliding Window</div>
                        <p class="text-muted-foreground mt-1 font-mono text-xs">5-min window, 10s slide · 12,400 keys</p>
                      </div>
    
                      <div class="border-border/60 space-y-1.5 border-t pt-2.5 font-mono text-xs">
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>Output Rate:</span>
                          <span class="text-foreground font-semibold tabular-nums">38.2k rec/s</span>
                        </div>
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>RocksDB State:</span>
                          <span class="text-foreground text-chart-1 font-semibold tabular-nums">18.4 MB</span>
                        </div>
                      </div>
                    </div>
    
                    <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2 text-xs">
                      <span class="text-muted-foreground font-mono">Cardholder Velocity Agg</span>
                      <lucide-icon
                        [img]="ArrowRight"
                        class="text-muted-foreground group-hover:text-primary size-3.5 shrink-0 transition-colors"
                      />
                    </div>
                  </div>
    
                  <!-- Node 3: ML Fraud Scoring Pattern Matcher -->
                  <div
                    role="button"
                    tabindex="0"
                    [class]="nodeClass('op-ml-scoring')"
                    (click)="selectOperator('op-ml-scoring')"
                    (keydown.enter)="selectOperator('op-ml-scoring')"
                    (keydown.space)="$event.preventDefault(); selectOperator('op-ml-scoring')"
                  >
                    <div class="space-y-3">
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <div
                            class="border-chart-2/30 bg-chart-2/10 text-chart-2 flex size-7 items-center justify-center rounded-md border"
                          >
                            <lucide-icon [img]="Cpu" class="size-3.5" />
                          </div>
                          <span ui-badge variant="outline" class="border-chart-2/30 text-chart-2 font-mono text-xs"
                            >CEP & ML</span
                          >
                        </div>
                        <span ui-badge variant="outline" class="font-mono text-xs">p=16</span>
                      </div>
    
                      <div>
                        <div class="text-foreground font-mono text-xs leading-snug font-bold">ML Fraud Scoring Matcher</div>
                        <p class="text-muted-foreground mt-1 font-mono text-xs">Inference: 1.2ms · RocksDB: 38.2 MB</p>
                      </div>
    
                      <div class="border-border/60 space-y-1.5 border-t pt-2.5 font-mono text-xs">
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>ONNX Inference:</span>
                          <span class="text-success text-success font-semibold tabular-nums">p99 &lt; 1.2ms</span>
                        </div>
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>Managed State:</span>
                          <span class="text-foreground text-chart-2 font-semibold tabular-nums">38.2 MB</span>
                        </div>
                      </div>
                    </div>
    
                    <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2 text-xs">
                      <span class="text-muted-foreground font-mono">xgboost_v4.onnx</span>
                      <lucide-icon
                        [img]="ArrowRight"
                        class="text-muted-foreground group-hover:text-primary size-3.5 shrink-0 transition-colors"
                      />
                    </div>
                  </div>
    
                  <!-- Node 4: Sink: Alert Dispatcher & Redis Cache -->
                  <div
                    role="button"
                    tabindex="0"
                    [class]="nodeClass('op-sink-dispatcher')"
                    (click)="selectOperator('op-sink-dispatcher')"
                    (keydown.enter)="selectOperator('op-sink-dispatcher')"
                    (keydown.space)="$event.preventDefault(); selectOperator('op-sink-dispatcher')"
                  >
                    <div class="space-y-3">
                      <div class="flex items-start justify-between gap-2">
                        <div class="flex items-center gap-2">
                          <div
                            class="border-success/30 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border"
                          >
                            <lucide-icon [img]="Database" class="size-3.5" />
                          </div>
                          <span ui-badge variant="outline" class="border-success/30 text-success font-mono text-xs"
                            >SINK</span
                          >
                        </div>
                        <span ui-badge variant="outline" class="font-mono text-xs">p=8</span>
                      </div>
    
                      <div>
                        <div class="text-foreground font-mono text-xs leading-snug font-bold">Sink: Alert Dispatcher</div>
                        <p class="text-muted-foreground mt-1 font-mono text-xs">Redis Cache & Webhooks · 42.5k rec/s</p>
                      </div>
    
                      <div class="border-border/60 space-y-1.5 border-t pt-2.5 font-mono text-xs">
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>Semantics:</span>
                          <span class="text-foreground font-semibold">2PC Exactly-Once</span>
                        </div>
                        <div class="text-muted-foreground flex items-center justify-between">
                          <span>Fan-Out:</span>
                          <span class="text-foreground font-semibold tabular-nums">42.5k rec/s</span>
                        </div>
                      </div>
                    </div>
    
                    <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2 text-xs">
                      <span class="text-muted-foreground font-mono">redis:6379 + alerts.queue</span>
                      <lucide-icon [img]="CheckCircle2" class="text-success size-3.5 shrink-0" />
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
    
          <!-- 4. Operator Detailed Performance & State Inspector Panel -->
          <ui-stream-operator-detail
            [operator]="selectedOperator()"
            [copiedKey]="copiedKey()"
            (copy)="copyText($event.key, $event.value)"
          />
        </div>
      `,
    })
    export class UiStreamProcessingFlinkTopologyComponent {
      protected readonly AlertTriangle = AlertTriangle
      protected readonly ArrowRight = ArrowRight
      protected readonly Camera = Camera
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly Cpu = Cpu
      protected readonly Database = Database
      protected readonly Loader2 = Loader2
      protected readonly Radio = Radio
      protected readonly Square = Square
      protected readonly Workflow = Workflow
    
      @Input() operators: FlinkOperatorNode[] = defaultFlinkOperators
      @Input('class') className?: string
    
      readonly selectedOperatorId = signal<string>('op-ml-scoring')
      readonly isTriggeringSavepoint = signal<boolean>(false)
      readonly savepointNotice = signal<string | null>(null)
      readonly isCanceling = signal<boolean>(false)
      readonly cancelConfirmed = signal<boolean>(false)
      readonly copiedKey = signal<string | null>(null)
    
      readonly selectedOperator = computed(() => {
        return this.operators.find((op) => op.id === this.selectedOperatorId()) ?? this.operators[0]!
      })
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      nodeClass(id: string): string {
        return cn(
          'group focus-visible:ring-ring relative flex cursor-pointer flex-col justify-between rounded-xl border p-4 transition-colors duration-200 focus-visible:ring-2 focus-visible:outline-none',
          this.selectedOperatorId() === id
            ? 'border-primary bg-primary/[0.04] ring-primary/30 shadow-xs ring-2'
            : 'border-border bg-card hover:border-primary/50 hover:bg-muted/30',
        )
      }
    
      selectOperator(id: string): void {
        this.selectedOperatorId.set(id)
      }
    
      handleTriggerSavepoint(): void {
        if (this.isTriggeringSavepoint()) return
        this.isTriggeringSavepoint.set(true)
        this.savepointNotice.set(null)
        setTimeout(() => {
          this.isTriggeringSavepoint.set(false)
          this.savepointNotice.set('s3://flink-savepoints/prod/savepoint-fraud-90412-20260821-143245/')
          setTimeout(() => {
            if (this.savepointNotice()?.includes('savepoint-fraud-90412')) this.savepointNotice.set(null)
          }, 6500)
        }, 1800)
      }
    
      handleCancelJob(): void {
        if (this.isCanceling()) return
        this.isCanceling.set(true)
        setTimeout(() => {
          this.isCanceling.set(false)
          this.cancelConfirmed.set(true)
          setTimeout(() => {
            this.cancelConfirmed.set(false)
          }, 4500)
        }, 2000)
      }
    
      copyText(key: string, value: string): void {
        try {
          if (typeof navigator !== 'undefined' && navigator.clipboard) {
            void navigator.clipboard.writeText(value)
            this.copiedKey.set(key)
            setTimeout(() => {
              if (this.copiedKey() === key) this.copiedKey.set(null)
            }, 2000)
          }
        } catch {
          // Clipboard fallback
        }
      }
    }
    
  • src/app/components/blocks/stream-processing-flink-topology/stream-operator-detail.component.ts18.2 kB
  • src/app/components/blocks/stream-processing-flink-topology/stream-topology-telemetry.component.ts7.6 kB
  • src/app/components/blocks/stream-processing-flink-topology/stream-topology-types.ts0.9 kB
  • src/app/components/blocks/stream-processing-flink-topology/stream-topology-data.ts19.2 kB
  • src/app/components/blocks/stream-processing-flink-topology/index.ts0.4 kB

Raw manifest:https://uipkge.dev/r/angular/stream-processing-flink-topology.json