UIPackage
Menu

Framework

Change language

Boilerplate 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.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/stream-processing-flink-topology.json
Named registry:npx shadcn-vue@latest add @uipkge/stream-processing-flink-topologyInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
operatorsFlinkOperatorNode[]() => defaultFlinkOperators,optional
classHTMLAttributes['class']optional

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 (5)

  • app/components/blocks/StreamProcessingFlinkTopology.vue19.7 kB
    <script setup lang="ts">
    import { computed, ref, type HTMLAttributes } from 'vue'
    import {
      AlertTriangle,
      ArrowRight,
      Camera,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Cpu,
      Database,
      Loader2,
      Radio,
      Square,
      Workflow,
    } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
    import StreamOperatorDetail from './StreamOperatorDetail.vue'
    import StreamTopologyTelemetry from './StreamTopologyTelemetry.vue'
    import type { FlinkOperatorNode, FlinkSubtask } from './stream-topology-types'
    import { defaultFlinkOperators } from './stream-topology-data'
    
    export type { FlinkOperatorNode, FlinkSubtask }
    
    interface Props {
      operators?: FlinkOperatorNode[]
      class?: HTMLAttributes['class']
    }
    
    const props = withDefaults(defineProps<Props>(), {
      operators: () => defaultFlinkOperators,
    })
    
    const selectedOperatorId = ref<string>('op-ml-scoring')
    const isTriggeringSavepoint = ref<boolean>(false)
    const savepointNotice = ref<string | null>(null)
    const isCanceling = ref<boolean>(false)
    const cancelConfirmed = ref<boolean>(false)
    const copiedKey = ref<string | null>(null)
    
    const selectedOperator = computed(() => {
      return props.operators.find((op) => op.id === selectedOperatorId.value) ?? props.operators[0]
    })
    
    function selectOperator(id: string) {
      selectedOperatorId.value = id
    }
    
    function handleTriggerSavepoint() {
      if (isTriggeringSavepoint.value) return
      isTriggeringSavepoint.value = true
      savepointNotice.value = null
    
      setTimeout(() => {
        isTriggeringSavepoint.value = false
        savepointNotice.value = 's3://flink-savepoints/prod/savepoint-fraud-90412-20260821-143245/'
        setTimeout(() => {
          if (savepointNotice.value?.includes('savepoint-fraud-90412')) {
            savepointNotice.value = null
          }
        }, 6500)
      }, 1800)
    }
    
    function handleCancelJob() {
      if (isCanceling.value) return
      isCanceling.value = true
      setTimeout(() => {
        isCanceling.value = false
        cancelConfirmed.value = true
        setTimeout(() => {
          cancelConfirmed.value = false
        }, 4500)
      }, 2000)
    }
    
    function copyText(key: string, value: string) {
      if (typeof navigator !== 'undefined' && navigator.clipboard) {
        navigator.clipboard.writeText(value)
        copiedKey.value = key
        setTimeout(() => {
          if (copiedKey.value === key) {
            copiedKey.value = null
          }
        }, 2000)
      }
    }
    </script>
    
    <template>
      <div data-slot="stream-processing-flink-topology" :class="cn('w-full space-y-6', props.class)">
        <!-- 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"
              >
                <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>
                <Badge variant="outline" class="font-mono text-xs"> Flink 1.19 on Kubernetes </Badge>
              </div>
              <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 class="bg-success relative inline-flex size-2 rounded-full" />
                </span>
                <span>Job Running · Checkpointing Healthy</span>
              </Badge>
            </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
              variant="outline"
              size="sm"
              class="gap-1.5 text-xs font-medium"
              :disabled="isTriggeringSavepoint"
              @click="handleTriggerSavepoint"
            >
              <Loader2 v-if="isTriggeringSavepoint" class="size-3.5 animate-spin" aria-hidden="true" />
              <Camera v-else class="text-info size-3.5" aria-hidden="true" />
              <span>{{ isTriggeringSavepoint ? 'Creating Savepoint...' : 'Trigger Savepoint' }}</span>
            </Button>
    
            <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"
            >
              <Loader2 v-if="isCanceling" class="size-3.5 animate-spin" aria-hidden="true" />
              <Square v-else class="size-3.5 fill-current" aria-hidden="true" />
              <span>{{ isCanceling ? 'Canceling with Savepoint...' : 'Cancel Job with Savepoint' }}</span>
            </Button>
          </div>
        </div>
    
        <!-- Active Savepoint Notice Banner -->
        <div
          v-if="savepointNotice"
          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">
            <CheckCircle2 class="text-info size-4 shrink-0" aria-hidden="true" />
            <div>
              <span class="text-info font-semibold">Savepoint completed successfully:</span>
              <span class="ml-1 font-mono text-xs">{{ savepointNotice }}</span>
            </div>
          </div>
          <Button variant="ghost" size="xs" class="h-6 gap-1 px-2 text-xs" @click="copyText('savepoint', savepointNotice)">
            <Check v-if="copiedKey === 'savepoint'" class="text-success size-3" />
            <Copy v-else class="size-3" />
            <span>{{ copiedKey === 'savepoint' ? 'Copied' : 'Copy URI' }}</span>
          </Button>
        </div>
    
        <!-- Active Cancellation Notice Banner -->
        <div
          v-if="cancelConfirmed"
          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">
            <AlertTriangle class="text-warning size-4 shrink-0" aria-hidden="true" />
            <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>
          <Badge variant="outline" class="font-mono text-xs">Drain Status: 100%</Badge>
        </div>
    
        <!-- 2. 4 Stream Telemetry KPI Cards -->
        <StreamTopologyTelemetry />
    
        <!-- 3. Interactive 4-Operator Streaming Topology DAG -->
        <Card class="shadow-xs">
          <CardHeader 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">
                  <CardTitle class="text-base font-semibold">Streaming Execution Topology DAG</CardTitle>
                  <Badge variant="outline" class="font-mono text-xs">4 Stateful Operators</Badge>
                </div>
                <CardDescription class="text-xs">
                  Live dataflow graph with event-time watermarking, state sizes, and backpressure telemetry. Click any
                  operator to inspect metrics and subtasks.
                </CardDescription>
              </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>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"
                >
                  <Database class="size-3" />
                  <span>Total Managed State: 57.0 MB</span>
                </div>
              </div>
            </div>
          </CardHeader>
    
          <CardContent 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="
                    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',
                      selectedOperatorId === 'op-kafka-source'
                        ? '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',
                    )
                  "
                  @click="selectOperator('op-kafka-source')"
                  @keydown.enter="selectOperator('op-kafka-source')"
                  @keydown.space.prevent="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"
                        >
                          <Radio class="size-3.5" />
                        </div>
                        <Badge variant="outline" class="border-info/30 text-info font-mono text-xs"> SOURCE </Badge>
                      </div>
                      <Badge variant="outline" class="font-mono text-xs">p=8</Badge>
                    </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>
                    <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="
                    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',
                      selectedOperatorId === 'op-keyed-window'
                        ? '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',
                    )
                  "
                  @click="selectOperator('op-keyed-window')"
                  @keydown.enter="selectOperator('op-keyed-window')"
                  @keydown.space.prevent="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"
                        >
                          <Clock class="size-3.5" />
                        </div>
                        <Badge variant="outline" class="border-chart-1/30 text-chart-1 font-mono text-xs"> WINDOW </Badge>
                      </div>
                      <Badge variant="outline" class="font-mono text-xs">p=16</Badge>
                    </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>
                    <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="
                    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',
                      selectedOperatorId === 'op-ml-scoring'
                        ? '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',
                    )
                  "
                  @click="selectOperator('op-ml-scoring')"
                  @keydown.enter="selectOperator('op-ml-scoring')"
                  @keydown.space.prevent="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"
                        >
                          <Cpu class="size-3.5" />
                        </div>
                        <Badge variant="outline" class="border-chart-2/30 text-chart-2 font-mono text-xs"> CEP & ML </Badge>
                      </div>
                      <Badge variant="outline" class="font-mono text-xs">p=16</Badge>
                    </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>
                    <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="
                    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',
                      selectedOperatorId === 'op-sink-dispatcher'
                        ? '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',
                    )
                  "
                  @click="selectOperator('op-sink-dispatcher')"
                  @keydown.enter="selectOperator('op-sink-dispatcher')"
                  @keydown.space.prevent="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"
                        >
                          <Database class="size-3.5" />
                        </div>
                        <Badge variant="outline" class="border-success/30 text-success font-mono text-xs"> SINK </Badge>
                      </div>
                      <Badge variant="outline" class="font-mono text-xs">p=8</Badge>
                    </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>
                    <CheckCircle2 class="text-success size-3.5 shrink-0" />
                  </div>
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
    
        <!-- 4. Operator Detailed Performance & State Inspector Panel -->
        <StreamOperatorDetail :operator="selectedOperator" :copied-key="copiedKey" @copy="copyText" />
      </div>
    </template>
    
  • app/components/blocks/StreamOperatorDetail.vue16.5 kB
  • app/components/blocks/StreamTopologyTelemetry.vue6.3 kB
  • app/components/blocks/stream-topology-types.ts0.9 kB
  • app/components/blocks/stream-topology-data.ts19.2 kB

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