Menu

Change language

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

Installation

$npx uipkge-ng@latest add spark-job-stage-visualizer
Manifest:https://uipkge.dev/r/angular/spark-job-stage-visualizer.jsonInstalls to:src/app/components/blocks/spark-job-stage-visualizer/

Variants

Loading interactive previews…

Schema

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

SparkStageSummary
interface SparkStageSummary {
  id: number
  name: string
  status: 'succeeded' | 'running' | 'pending'
  tasksTotal: number
  tasksSucceeded: number
  duration: string
  skewRatio: number
  quantiles: { min: number; p25: number; median: number; p75: number; p95: number; max: number }
}
SparkExecutorSummary
interface SparkExecutorSummary {
  id: string
  host: string
  role: 'driver' | 'executor'
  status: string
  cores: number
  memoryUsedGB: number
  memoryTotalGB: number
  memoryPercent: number
  gcTimeSec: number
  gcPercent: number
  tasksCompleted: number
  spillGB: number
}
AqeRuleSummary
interface AqeRuleSummary {
  id: string
  name: string
  status: 'applied' | 'skipped'
  description: string
  impact: string
}

Files installed (2)

  • src/app/components/blocks/spark-job-stage-visualizer/spark-job-stage-visualizer.component.ts26 kB
    // Databricks and Apache Spark distributed job execution DAG visualizer with stage
    // performance metrics, task skew distribution quantiles, and executor JVM GC telemetry.
    // Port of the Vue block 1:1 (multi-file Vue block consolidated into one standalone component).
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      ArrowRightLeft,
      Check,
      CheckCircle2,
      Clock,
      Copy,
      Cpu,
      Download,
      Flame,
      Gauge,
      LucideAngularModule,
      Server,
      ShieldAlert,
      X,
      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,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component'
    
    export type SparkTab = 'dag' | 'skew' | 'executors' | 'aqe'
    
    interface SparkStageSummary {
      id: number
      name: string
      status: 'succeeded' | 'running' | 'pending'
      tasksTotal: number
      tasksSucceeded: number
      duration: string
      skewRatio: number
      quantiles: { min: number; p25: number; median: number; p75: number; p95: number; max: number }
    }
    
    interface SparkExecutorSummary {
      id: string
      host: string
      role: 'driver' | 'executor'
      status: string
      cores: number
      memoryUsedGB: number
      memoryTotalGB: number
      memoryPercent: number
      gcTimeSec: number
      gcPercent: number
      tasksCompleted: number
      spillGB: number
    }
    
    interface AqeRuleSummary {
      id: string
      name: string
      status: 'applied' | 'skipped'
      description: string
      impact: string
    }
    
    const defaultStages: SparkStageSummary[] = [
      {
        id: 0,
        name: 'FileScan parquet',
        status: 'succeeded',
        tasksTotal: 540,
        tasksSucceeded: 540,
        duration: '18s',
        skewRatio: 1.12,
        quantiles: { min: 0.8, p25: 1.2, median: 1.6, p75: 1.8, p95: 2.0, max: 2.1 },
      },
      {
        id: 1,
        name: 'Exchange hashpartitioning',
        status: 'succeeded',
        tasksTotal: 200,
        tasksSucceeded: 200,
        duration: '42s',
        skewRatio: 1.15,
        quantiles: { min: 1.8, p25: 2.4, median: 2.9, p75: 3.2, p95: 3.4, max: 3.6 },
      },
      {
        id: 2,
        name: 'HashAggregate & Filter',
        status: 'succeeded',
        tasksTotal: 200,
        tasksSucceeded: 200,
        duration: '1m 12s',
        skewRatio: 1.18,
        quantiles: { min: 2.1, p25: 2.8, median: 3.5, p75: 3.9, p95: 4.1, max: 4.2 },
      },
      {
        id: 3,
        name: 'SortMergeJoin',
        status: 'succeeded',
        tasksTotal: 200,
        tasksSucceeded: 200,
        duration: '56s',
        skewRatio: 1.09,
        quantiles: { min: 1.9, p25: 2.5, median: 3.1, p75: 3.4, p95: 3.7, max: 3.8 },
      },
    ]
    
    const defaultExecutors: SparkExecutorSummary[] = [
      {
        id: 'driver',
        host: '10.0.12.4',
        role: 'driver',
        status: 'active',
        cores: 16,
        memoryUsedGB: 24.2,
        memoryTotalGB: 32,
        memoryPercent: 75.6,
        gcTimeSec: 4.2,
        gcPercent: 1.8,
        tasksCompleted: 285,
        spillGB: 0.36,
      },
      {
        id: 'exec-1',
        host: '10.0.12.18',
        role: 'executor',
        status: 'active',
        cores: 32,
        memoryUsedGB: 28.8,
        memoryTotalGB: 32,
        memoryPercent: 90.0,
        gcTimeSec: 5.1,
        gcPercent: 2.2,
        tasksCompleted: 284,
        spillGB: 0.42,
      },
      {
        id: 'exec-2',
        host: '10.0.12.29',
        role: 'executor',
        status: 'active',
        cores: 32,
        memoryUsedGB: 21.4,
        memoryTotalGB: 32,
        memoryPercent: 66.8,
        gcTimeSec: 4.8,
        gcPercent: 2.3,
        tasksCompleted: 286,
        spillGB: 0.34,
      },
      {
        id: 'exec-3',
        host: '10.0.12.44',
        role: 'executor',
        status: 'active',
        cores: 32,
        memoryUsedGB: 18.6,
        memoryTotalGB: 32,
        memoryPercent: 58.1,
        gcTimeSec: 4.3,
        gcPercent: 2.1,
        tasksCompleted: 285,
        spillGB: 0.36,
      },
    ]
    
    const defaultAqeRules: AqeRuleSummary[] = [
      {
        id: 'aqe-1',
        name: 'CoalesceShufflePartitions',
        status: 'applied',
        description:
          'Coalesced 540 initial map partitions down to 200 target shuffle partitions based on target partition size (64 MB).',
        impact: 'Reduced network RPC overhead by 63% and avoided 340 small shuffle partition files.',
      },
      {
        id: 'aqe-2',
        name: 'OptimizeSkewedJoin',
        status: 'applied',
        description: 'Dynamic skew detection evaluated all 200 partition sizes against the 5x median threshold.',
        impact: 'Max partition skew ratio remained well balanced at 1.18x. No partition splitting required.',
      },
      {
        id: 'aqe-3',
        name: 'DemoteBroadcastHashJoin',
        status: 'skipped',
        description: 'Evaluated right dimension relation size (3.1 GB) against auto-broadcast threshold (10 MB).',
        impact: 'Relation exceeded broadcast threshold; preserved robust SortMergeJoin strategy.',
      },
      {
        id: 'aqe-4',
        name: 'EliminateSubqueryAliases',
        status: 'applied',
        description: 'Inlined and pruned unused redundant subquery attribute references from execution DAG.',
        impact: 'Streamlined Catalyst physical plan tree depth from 14 nodes down to 8 nodes.',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-spark-job-stage-visualizer, [ui-spark-job-stage-visualizer]',
      standalone: true,
      // Vue 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,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiProgressComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
        UiTabsComponent,
        UiTabsListComponent,
        UiTabsTriggerComponent,
      ],
      template: `
        <div data-slot="spark-job-stage-visualizer" [class]="rootClass">
          <!-- 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"
                  >
                    <lucide-icon [img]="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>
                      <span ui-badge wrap variant="outline" class="text-muted-foreground font-mono text-xs">
                        app-20260821-142850-0042
                      </span>
                      <button
                        ui-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()"
                      >
                        @if (copiedAppId()) {
                          <lucide-icon [img]="Check" class="text-success size-3" />
                        } @else {
                          <lucide-icon [img]="Copy" 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>
                    <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">
                    <lucide-icon [img]="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">
                    <lucide-icon [img]="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
                  ui-button
                  aria-label="Download attachment"
                  variant="outline"
                  size="sm"
                  class="h-8 gap-1.5 text-xs"
                  [disabled]="downloadStarted()"
                  (click)="triggerDownload()"
                >
                  @if (downloadStarted()) {
                    <lucide-icon [img]="CheckCircle2" class="text-success size-3.5" />
                  } @else {
                    <lucide-icon [img]="Download" class="size-3.5" />
                  }
                  <span>{{ downloadStarted() ? 'Log Exported' : 'Download EventLog' }}</span>
                </button>
    
                <button
                  ui-button
                  variant="ghost"
                  size="sm"
                  class="text-destructive hover:bg-destructive/10 hover:text-destructive h-8 gap-1.5 text-xs"
                  (click)="showKillConfirm.set(!showKillConfirm())"
                >
                  <lucide-icon [img]="X" class="size-3.5" />
                  <span>Kill Application</span>
                </button>
              </div>
            </div>
    
            <!-- Kill Confirmation Alert (Collapsible) -->
            @if (showKillConfirm()) {
              <div
                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">
                  <lucide-icon [img]="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 ui-button size="xs" variant="destructive" (click)="showKillConfirm.set(false)">
                    Confirm Release
                  </button>
                  <button ui-button size="xs" variant="ghost" (click)="showKillConfirm.set(false)">Cancel</button>
                </div>
              </div>
            }
          </div>
    
          <!-- 4 Spark Job Execution KPI Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Total Stages Completed</h3>
                <div class="bg-success/10 text-success flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="CheckCircle2" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="text-foreground font-mono text-2xl font-bold tracking-tight">6 / 6</div>
                <div class="flex flex-wrap items-center gap-2">
                  <span ui-badge wrap variant="success" class="text-xs">6 / 6 Stages Succeeded</span>
                </div>
                <p class="text-muted-foreground text-xs">0 failed · 0 skipped · 4 stages active in DAG</p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Shuffle Read / Write Volume</h3>
                <div class="bg-info/10 text-info flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="ArrowRightLeft" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">42.8 GB</div>
                <div class="text-muted-foreground flex items-center gap-1.5 text-xs">
                  <span class="text-foreground font-medium">42.8 GB Shuffle Write</span>
                  <span>·</span>
                  <span class="text-warning font-mono">1.48 GB Spill (Memory)</span>
                </div>
                <p class="text-muted-foreground text-xs">38.6 GB shuffle read · 0 B disk spill</p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Task Skew Max/Median Ratio</h3>
                <div class="bg-success/10 text-success flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="Gauge" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">1.18x</div>
                <div class="flex flex-wrap items-center gap-2">
                  <span ui-badge wrap variant="success" class="text-xs">1.18x Ratio · Well Balanced</span>
                </div>
                <p class="text-muted-foreground text-xs">
                  Max task: <span class="text-foreground font-mono font-medium">4.2s</span> · Median:
                  <span class="text-foreground font-mono font-medium">3.5s</span>
                </p>
              </div>
            </div>
    
            <div ui-card class="border-border shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Executor JVM GC Time</h3>
                <div class="bg-chart-1/10 text-chart-1 flex size-7 items-center justify-center rounded-md">
                  <lucide-icon [img]="Cpu" class="size-4" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums">2.1%</div>
                <div class="flex flex-wrap items-center gap-2">
                  <span ui-badge wrap variant="success" class="text-xs">2.1% of Total CPU Time · Healthy</span>
                </div>
                <p class="text-muted-foreground text-xs">18.4s GC time across 880s CPU total</p>
              </div>
            </div>
          </div>
    
          <!-- Main Content Tabs -->
          <ui-tabs [value]="activeTab()" (valueChange)="setActiveTab($event)" class="space-y-4">
            <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
              <ui-tabs-list class="grid w-full grid-cols-2 sm:w-auto sm:grid-cols-4">
                <button ui-tabs-trigger value="dag" class="text-xs">Stages DAG Flow</button>
                <button ui-tabs-trigger value="skew" class="text-xs">Task Skew & Quantiles</button>
                <button ui-tabs-trigger value="executors" class="text-xs">Executors (4)</button>
                <button ui-tabs-trigger value="aqe" class="text-xs">AQE Optimizer</button>
              </ui-tabs-list>
    
              <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>
                <span>Active Selection:</span>
                <span class="text-foreground font-medium">Stage {{ selectedStage().id }} ({{ selectedStage().name }})</span>
              </div>
            </div>
    
            <!-- TAB 1: STAGE DAG FLOW -->
            @if (activeTab() === 'dag') {
              <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
                @for (stage of allStages(); track stage.id) {
                  <button type="button" [class]="stageCardClass(stage.id)" (click)="selectStage(stage.id)">
                    <div class="flex items-center justify-between">
                      <span class="font-mono text-xs font-semibold">Stage {{ stage.id }}</span>
                      <span ui-badge [variant]="stage.status === 'succeeded' ? 'success' : 'secondary'" class="text-xs">
                        {{ stage.status }}
                      </span>
                    </div>
                    <p class="text-foreground mt-1.5 text-left text-sm font-semibold">{{ stage.name }}</p>
                    <p class="text-muted-foreground mt-1 font-mono text-xs tabular-nums">
                      {{ stage.tasksSucceeded }}/{{ stage.tasksTotal }} tasks · {{ stage.duration }}
                    </p>
                    <div ui-progress [value]="(stage.tasksSucceeded / stage.tasksTotal) * 100" class="mt-2 h-1.5"></div>
                  </button>
                }
              </div>
            }
    
            <!-- TAB 2: TASK SKEW & QUANTILES -->
            @if (activeTab() === 'skew') {
              <div ui-card class="border-border shadow-xs">
                <div ui-card-header class="pb-2">
                  <div class="flex flex-wrap items-center justify-between gap-2">
                    <h3 ui-card-title class="text-sm font-semibold">
                      Task Duration Quantiles — Stage {{ selectedStage().id }} ({{ selectedStage().name }})
                    </h3>
                    <span ui-badge variant="outline" class="font-mono text-xs">
                      Skew {{ selectedStage().skewRatio.toFixed(2) }}x
                    </span>
                  </div>
                </div>
                <div ui-card-content class="space-y-3">
                  @for (q of quantileRows(); track q.label) {
                    <div class="flex items-center gap-3 text-xs">
                      <span class="text-muted-foreground w-14 shrink-0 font-mono">{{ q.label }}</span>
                      <div class="bg-muted/60 relative h-2.5 flex-1 overflow-hidden rounded-full">
                        <div class="bg-primary absolute top-0 bottom-0 left-0 rounded-full" [style.width.%]="q.pct"></div>
                      </div>
                      <span class="text-foreground w-12 shrink-0 text-right font-mono tabular-nums">{{ q.value }}s</span>
                    </div>
                  }
                  <div class="flex flex-wrap gap-2 pt-1">
                    @for (stage of allStages(); track stage.id) {
                      <button
                        ui-button
                        [variant]="selectedStageId() === stage.id ? 'default' : 'outline'"
                        size="xs"
                        class="font-mono text-xs"
                        (click)="selectStage(stage.id)"
                      >
                        Stage {{ stage.id }}
                      </button>
                    }
                  </div>
                </div>
              </div>
            }
    
            <!-- TAB 3: EXECUTORS -->
            @if (activeTab() === 'executors') {
              <div ui-card class="border-border shadow-xs">
                <div ui-card-header class="pb-2">
                  <div class="flex flex-wrap items-center justify-between gap-2">
                    <h3 ui-card-title class="text-sm font-semibold">
                      Active Cluster Executors (4 Nodes · 112 Cores Total)
                    </h3>
                    <span ui-badge variant="success" class="font-mono text-xs">All Healthy</span>
                  </div>
                </div>
                <div ui-card-content class="overflow-x-auto p-0">
                  <table ui-table>
                    <thead ui-table-header>
                      <tr ui-table-row class="bg-muted/40 hover:bg-muted/40">
                        <th ui-table-head class="text-xs font-semibold">Executor</th>
                        <th ui-table-head class="text-xs font-semibold">Host</th>
                        <th ui-table-head class="text-right text-xs font-semibold">Cores</th>
                        <th ui-table-head class="text-right text-xs font-semibold">Memory</th>
                        <th ui-table-head class="text-right text-xs font-semibold">GC Pause Time</th>
                        <th ui-table-head class="text-right text-xs font-semibold">Tasks</th>
                      </tr>
                    </thead>
                    <tbody ui-table-body>
                      @for (exec of executors(); track exec.id) {
                        <tr ui-table-row class="text-xs">
                          <td ui-table-cell class="font-mono font-semibold">{{ exec.id }}</td>
                          <td ui-table-cell class="font-mono">{{ exec.host }}</td>
                          <td ui-table-cell class="text-right font-mono tabular-nums">{{ exec.cores }}</td>
                          <td ui-table-cell class="text-right font-mono tabular-nums">
                            {{ exec.memoryUsedGB.toFixed(1) }}/{{ exec.memoryTotalGB }} GB
                          </td>
                          <td ui-table-cell class="text-right font-mono tabular-nums">
                            {{ exec.gcTimeSec.toFixed(1) }}s ({{ exec.gcPercent.toFixed(1) }}%)
                          </td>
                          <td ui-table-cell class="text-right font-mono tabular-nums">{{ exec.tasksCompleted }}</td>
                        </tr>
                      }
                    </tbody>
                  </table>
                </div>
              </div>
            }
    
            <!-- TAB 4: AQE OPTIMIZER -->
            @if (activeTab() === 'aqe') {
              <div class="space-y-3">
                <div ui-card class="border-border shadow-xs">
                  <div ui-card-header class="pb-2">
                    <div class="flex flex-wrap items-center justify-between gap-2">
                      <h3 ui-card-title class="text-base font-semibold">Adaptive Query Execution (AQE) Rules Engine</h3>
                      <span ui-badge variant="success" class="font-mono text-xs">AQE Status: Active (3 Rules Applied)</span>
                    </div>
                    <p class="text-muted-foreground text-xs">
                      Catalyst optimizer rewrites evaluated at runtime against live shuffle statistics.
                    </p>
                  </div>
                </div>
                @for (rule of aqeRules(); track rule.id) {
                  <div class="border-border bg-card space-y-3 rounded-xl border p-4">
                    <div class="flex flex-wrap items-center justify-between gap-2">
                      <span class="text-foreground font-mono text-sm font-semibold">{{ rule.name }}</span>
                      <span
                        ui-badge
                        [variant]="rule.status === 'applied' ? 'success' : 'outline'"
                        class="font-mono text-xs"
                      >
                        {{ rule.status }}
                      </span>
                    </div>
                    <p class="text-muted-foreground text-xs">{{ rule.description }}</p>
                    <p class="text-xs">
                      <lucide-icon [img]="Zap" class="text-warning mr-1 inline size-3.5" />
                      <span class="text-muted-foreground">{{ rule.impact }}</span>
                    </p>
                  </div>
                }
              </div>
            }
          </ui-tabs>
        </div>
      `,
    })
    export class UiSparkJobStageVisualizerComponent {
      protected readonly ArrowRightLeft = ArrowRightLeft
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Clock = Clock
      protected readonly Copy = Copy
      protected readonly Cpu = Cpu
      protected readonly Download = Download
      protected readonly Flame = Flame
      protected readonly Gauge = Gauge
      protected readonly Server = Server
      protected readonly ShieldAlert = ShieldAlert
      protected readonly X = X
      protected readonly Zap = Zap
    
      @Input() set initialStageId(v: number) {
        if (v !== undefined && v !== null) this.selectedStageId.set(v)
      }
      @Input() set stages(v: SparkStageSummary[] | undefined) {
        if (v) this.stageList.set(v)
      }
      @Input('class') className?: string
    
      private readonly stageList = signal<SparkStageSummary[]>(defaultStages)
      readonly executors = signal<SparkExecutorSummary[]>(defaultExecutors)
      readonly aqeRules = signal<AqeRuleSummary[]>(defaultAqeRules)
    
      readonly selectedStageId = signal(2)
      readonly activeTab = signal<SparkTab>('dag')
      readonly copiedAppId = signal(false)
      readonly showKillConfirm = signal(false)
      readonly downloadStarted = signal(false)
    
      readonly allStages = computed(() => this.stageList())
    
      readonly selectedStage = computed<SparkStageSummary>(() => {
        const list = this.stageList()
        return list.find((s) => s.id === this.selectedStageId()) ?? list[2] ?? list[0]!
      })
    
      readonly quantileRows = computed(() => {
        const q = this.selectedStage().quantiles
        const max = q.max || 1
        const rows = [
          { label: 'min', value: q.min },
          { label: 'p25', value: q.p25 },
          { label: 'median', value: q.median },
          { label: 'p75', value: q.p75 },
          { label: 'p95', value: q.p95 },
          { label: 'max', value: q.max },
        ]
        return rows.map((r) => ({ ...r, value: r.value.toFixed(1), pct: Math.min(100, (r.value / max) * 100) }))
      })
    
      get rootClass(): string {
        return cn('text-foreground w-full space-y-6 font-sans antialiased', this.className)
      }
    
      stageCardClass(id: number): string {
        return cn(
          'rounded-xl border p-4 text-left transition-colors cursor-pointer',
          this.selectedStageId() === id
            ? 'border-primary bg-primary/5 ring-primary shadow-xs ring-1'
            : 'border-border bg-card hover:border-primary/40',
        )
      }
    
      setActiveTab(value: string): void {
        this.activeTab.set(value as SparkTab)
      }
    
      selectStage(id: number): void {
        this.selectedStageId.set(id)
      }
    
      async copyAppId(): Promise<void> {
        try {
          await navigator.clipboard.writeText('app-20260821-142850-0042')
          this.copiedAppId.set(true)
          setTimeout(() => this.copiedAppId.set(false), 2000)
        } catch {
          // Clipboard unavailable
        }
      }
    
      triggerDownload(): void {
        this.downloadStarted.set(true)
        setTimeout(() => this.downloadStarted.set(false), 2500)
      }
    }
    
  • src/app/components/blocks/spark-job-stage-visualizer/index.ts0.1 kB

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