UIPackage
Menu

Framework

Change language

Boilerplate repo

Spark Job Stage Visualizer

blockdevops

Databricks and Apache Spark distributed job execution DAG visualizer with stage performance metrics, task skew distribution quantiles, and executor JVM GC telemetry.

Also available for React ->

Installation

$npx shadcn-vue@latest add https://uipkge.dev/r/vue/spark-job-stage-visualizer.json
Named registry:npx shadcn-vue@latest add @uipkge/spark-job-stage-visualizerInstalls to:app/components/blocks/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
initialStageIdnumber2optional
stagesSparkStage[]() => defaultStages,optional
classHTMLAttributes['class']optional

Schema

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

TaskQuantiles
interface TaskQuantiles {
  min: number
  p25: number
  median: number
  p75: number
  p95: number
  max: number
}
TaskBreakdownMetrics
interface TaskBreakdownMetrics {
  executorRunTime: string
  jvmGcTime: string
  shuffleFetchWait: string
  serializationTime: string
  deserializationTime: string
  peakMemory: string
}
SparkStage
interface SparkStage {
  id: number
  name: string
  callSite: string
  operator: string
  status: StageStatus
  tasksTotal: number
  tasksSucceeded: number
  tasksFailed: number
  duration: string
  durationSec: number
  inputBytes: string
  inputRecords: string
  outputBytes: string
  outputRecords: string
  shuffleReadBytes: string
  shuffleWriteBytes: string
  spillMemory: string
  spillDisk: string
  skewRatio: number
  quantiles: TaskQuantiles
  taskBreakdown: TaskBreakdownMetrics
  details: string
  upstreamStageIds: number[]
  downstreamStageIds: number[]
  branch: 'left' | 'right' | 'join'
}
SparkExecutor
interface SparkExecutor {
  id: string
  host: string
  role: 'driver' | 'executor'
  status: ExecutorStatus
  cores: number
  memoryUsedGB: number
  memoryTotalGB: number
  memoryPercent: number
  gcTimeSec: number
  gcPercent: number
  tasksCompleted: number
  tasksTotal: number
  tasksFailed: number
  shuffleReadGB: number
  shuffleWriteGB: number
  spillGB: number
}
AqeRule
interface AqeRule {
  id: string
  name: string
  status: 'applied' | 'skipped'
  description: string
  impact: string
}

Files installed (7)

  • app/components/blocks/SparkJobStageVisualizer.vue7.7 kB
    <script setup lang="ts">
    import { computed, ref, type HTMLAttributes } from 'vue'
    import { Check, CheckCircle2, Clock, Copy, Download, Flame, Server, ShieldAlert, X } from 'lucide-vue-next'
    import { cn } from '@/lib/utils'
    import { Badge } from '@/components/ui/badge'
    import { Button } from '@/components/ui/button'
    import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
    import SparkJobKpis from './SparkJobKpis.vue'
    import SparkStageDagTab from './SparkStageDagTab.vue'
    import SparkStageSkewTab from './SparkStageSkewTab.vue'
    import SparkExecutorsAqe from './SparkExecutorsAqe.vue'
    import type {
      StageStatus,
      ExecutorStatus,
      TaskQuantiles,
      TaskBreakdownMetrics,
      SparkStage,
      SparkExecutor,
      AqeRule,
    } from './spark-stage-types'
    import { defaultStages } from './spark-stage-data'
    
    export type { StageStatus, ExecutorStatus, TaskQuantiles, TaskBreakdownMetrics, SparkStage, SparkExecutor, AqeRule }
    
    interface Props {
      initialStageId?: number
      stages?: SparkStage[]
      class?: HTMLAttributes['class']
    }
    
    const props = withDefaults(defineProps<Props>(), {
      initialStageId: 2,
      stages: () => defaultStages,
    })
    
    const selectedStageId = ref<number>(props.initialStageId)
    const activeTab = ref<string>('dag')
    const copiedAppId = ref<boolean>(false)
    const showKillConfirm = ref<boolean>(false)
    const downloadStarted = ref<boolean>(false)
    
    const selectedStage = computed(() => {
      return props.stages.find((s) => s.id === selectedStageId.value) ?? props.stages[2] ?? props.stages[0]
    })
    
    function selectStage(id: number) {
      selectedStageId.value = id
    }
    
    function copyAppId() {
      navigator.clipboard?.writeText('app-20260821-142850-0042')
      copiedAppId.value = true
      setTimeout(() => {
        copiedAppId.value = false
      }, 2000)
    }
    
    function triggerDownload() {
      downloadStarted.value = true
      setTimeout(() => {
        downloadStarted.value = false
      }, 2500)
    }
    </script>
    
    <template>
      <div :class="cn('text-foreground w-full space-y-6 font-sans antialiased', props.class)">
        <!-- Header Section -->
        <div class="border-border bg-card rounded-xl border p-5 shadow-xs transition-colors">
          <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
            <!-- Title & Identifiers -->
            <div class="space-y-1.5">
              <div class="flex flex-wrap items-center gap-2.5">
                <div
                  class="flex size-9 items-center justify-center rounded-lg bg-orange-500/10 text-orange-600 dark:text-orange-400"
                >
                  <Flame class="size-5" />
                </div>
                <div>
                  <div class="flex flex-wrap items-center gap-2">
                    <h1 class="text-base font-semibold tracking-tight sm:text-lg">AggregatedCustomerLTVJob</h1>
                    <Badge wrap variant="outline" class="text-muted-foreground font-mono text-xs">
                      app-20260821-142850-0042
                    </Badge>
                    <Button
                      variant="ghost"
                      size="xs"
                      class="text-muted-foreground hover:text-foreground h-6 px-1.5 text-xs"
                      aria-label="Copy Spark Application ID"
                      @click="copyAppId"
                    >
                      <Check v-if="copiedAppId" class="text-success size-3" />
                      <Copy v-else class="size-3" />
                      <span class="sr-only">Copy Application ID</span>
                    </Button>
                  </div>
                  <p class="text-muted-foreground text-xs">Apache Spark 3.5.1 · Adaptive Query Execution (AQE) Enabled</p>
                </div>
              </div>
    
              <!-- Metadata Tags -->
              <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 pt-1 text-xs">
                <div class="flex items-center gap-1.5">
                  <span class="bg-success size-2 rounded-full" />
                  <span class="text-foreground font-medium">Job Succeeded</span>
                  <span>·</span>
                  <span class="font-mono tabular-nums">03m:42s</span>
                </div>
                <div class="flex items-center gap-1">
                  <Server class="size-3.5" />
                  <span
                    >Cluster: <strong class="text-foreground font-medium">c5d.4xlarge (4 nodes · 112 vCPUs)</strong></span
                  >
                </div>
                <div class="flex items-center gap-1">
                  <Clock class="size-3.5" />
                  <span>Started: <strong class="text-foreground font-medium">14:28:50 UTC</strong></span>
                </div>
              </div>
            </div>
    
            <!-- Action Buttons -->
            <div class="flex flex-wrap items-center gap-2">
              <Button
                aria-label="Download attachment"
                variant="outline"
                size="sm"
                class="h-8 gap-1.5 text-xs"
                :disabled="downloadStarted"
                @click="triggerDownload"
              >
                <CheckCircle2 v-if="downloadStarted" class="text-success size-3.5" />
                <Download v-else class="size-3.5" />
                <span>{{ downloadStarted ? 'Log Exported' : 'Download EventLog' }}</span>
              </Button>
    
              <Button
                variant="ghost"
                size="sm"
                class="text-destructive hover:bg-destructive/10 hover:text-destructive h-8 gap-1.5 text-xs"
                @click="showKillConfirm = !showKillConfirm"
              >
                <X class="size-3.5" />
                <span>Kill Application</span>
              </Button>
            </div>
          </div>
    
          <!-- Kill Confirmation Alert (Collapsible) -->
          <div
            v-if="showKillConfirm"
            class="border-destructive/30 bg-destructive/5 text-destructive mt-4 flex items-center justify-between rounded-lg border p-3 text-xs"
          >
            <div class="flex flex-wrap items-center gap-2">
              <ShieldAlert class="size-4 shrink-0" />
              <span
                >This application is in finished state (SUCCEEDED). Terminating an inactive application will release log
                retention locks.</span
              >
            </div>
            <div class="flex flex-wrap items-center gap-2">
              <Button size="xs" variant="destructive" @click="showKillConfirm = false"> Confirm Release </Button>
              <Button size="xs" variant="ghost" @click="showKillConfirm = false"> Cancel </Button>
            </div>
          </div>
        </div>
    
        <!-- 4 Spark Job Execution KPI Cards -->
        <SparkJobKpis />
    
        <!-- Main Content Tabs -->
        <Tabs v-model="activeTab" class="space-y-4">
          <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
            <TabsList class="grid w-full grid-cols-2 sm:w-auto sm:grid-cols-4">
              <TabsTrigger value="dag" class="text-xs"> Stages DAG Flow </TabsTrigger>
              <TabsTrigger value="skew" class="text-xs"> Task Skew & Quantiles </TabsTrigger>
              <TabsTrigger value="executors" class="text-xs"> Executors (4) </TabsTrigger>
              <TabsTrigger value="aqe" class="text-xs"> AQE Optimizer </TabsTrigger>
            </TabsList>
    
            <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
              <span class="bg-primary inline-block size-2 rounded-full" />
              <span>Active Selection:</span>
              <span class="text-foreground font-medium">Stage {{ selectedStage.id }} ({{ selectedStage.name }})</span>
            </div>
          </div>
    
          <!-- TAB 1: INTERACTIVE SPARK STAGES DAG FLOW -->
          <SparkStageDagTab
            :selected-stage="selectedStage"
            :selected-stage-id="selectedStageId"
            :stages="props.stages"
            @select-stage="selectStage"
          />
    
          <!-- TAB 2: TASK SKEW & QUANTILES DISTRIBUTION -->
          <SparkStageSkewTab
            :selected-stage="selectedStage"
            :stages="props.stages"
            :selected-stage-id="selectedStageId"
            @select-stage="selectStage"
          />
    
          <!-- TAB 3 & 4: Executors + AQE tabs -->
          <SparkExecutorsAqe />
        </Tabs>
      </div>
    </template>
    
  • app/components/blocks/SparkJobKpis.vue4 kB
  • app/components/blocks/SparkStageDagTab.vue19.9 kB
  • app/components/blocks/SparkStageSkewTab.vue12 kB
  • app/components/blocks/SparkExecutorsAqe.vue10 kB
  • app/components/blocks/spark-stage-data.ts7 kB
  • app/components/blocks/spark-stage-types.ts1.5 kB

Raw manifest:https://uipkge.dev/r/vue/spark-job-stage-visualizer.json