Menu

Change language

GitHub repo

Threat Map Radar

blocksecurity

Live threat radar over a Mapbox world map. Attack arcs hit edge PoPs; click an origin to isolate its vector.

Installation

$npx uipkge-ng@latest add threat-map-radar
Manifest:https://uipkge.dev/r/angular/threat-map-radar.jsonInstalls to:src/app/components/blocks/threat-map-radar/

Variants

Loading interactive previews…

Schema

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

ThreatOrigin
interface ThreatOrigin {
  id: string
  country: string
  countryCode: string
  volume: string
  vectorType: AttackVectorType
  mitigation: MitigationAction
  coords: { x: number; y: number }
  targetEdgeId: string
}
EdgePoP
interface EdgePoP {
  id: string
  code: string
  city: string
  coords: { x: number; y: number }
}
AttackArc
interface AttackArc {
  id: string
  originId: string
  edgeId: string
  sourceCoords: { x: number; y: number }
  targetCoords: { x: number; y: number }
  controlOffset: number
  vectorType: AttackVectorType
  gradientId: string
}

npm dependencies

Includes

Files installed (2)

  • src/app/components/blocks/threat-map-radar/threat-map-radar.component.ts12.1 kB
    // Live threat radar over a Mapbox world map. Attack arcs hit edge PoPs; click an origin to
    // isolate its vector. Port of the Vue/React block 1:1 (Mapbox UiMapComponent, same lib as Vue).
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiMapComponent } from '@/ui/map/map.component'
    
    export type AttackVectorType = 'volumetric' | 'l7' | 'botnet'
    export type MitigationAction = 'DROP' | 'CHALLENGE' | 'RATE_LIMIT'
    
    export interface ThreatOrigin {
      id: string
      country: string
      countryCode: string
      volume: string
      vectorType: AttackVectorType
      mitigation: MitigationAction
      coords: { x: number; y: number }
      targetEdgeId: string
    }
    
    export interface EdgePoP {
      id: string
      code: string
      city: string
      coords: { x: number; y: number }
    }
    
    export interface AttackArc {
      id: string
      originId: string
      edgeId: string
      sourceCoords: { x: number; y: number }
      targetCoords: { x: number; y: number }
      controlOffset: number
      vectorType: AttackVectorType
      gradientId: string
    }
    
    export const defaultOrigins: ThreatOrigin[] = [
      {
        id: 'origin-1',
        country: 'China',
        countryCode: 'CN',
        volume: '642.5K/s',
        vectorType: 'l7',
        mitigation: 'DROP',
        coords: { x: 785, y: 185 },
        targetEdgeId: 'edge-nrt',
      },
      {
        id: 'origin-2',
        country: 'Russia',
        countryCode: 'RU',
        volume: '418.2K/s',
        vectorType: 'l7',
        mitigation: 'CHALLENGE',
        coords: { x: 595, y: 135 },
        targetEdgeId: 'edge-fra',
      },
      {
        id: 'origin-3',
        country: 'Brazil',
        countryCode: 'BR',
        volume: '285.4K/s',
        vectorType: 'botnet',
        mitigation: 'RATE_LIMIT',
        coords: { x: 310, y: 340 },
        targetEdgeId: 'edge-iad',
      },
      {
        id: 'origin-4',
        country: 'Vietnam',
        countryCode: 'VN',
        volume: '194.0K/s',
        vectorType: 'volumetric',
        mitigation: 'DROP',
        coords: { x: 725, y: 275 },
        targetEdgeId: 'edge-sin',
      },
    ]
    
    export const edgeNodes: EdgePoP[] = [
      { id: 'edge-iad', code: 'IAD', city: 'Ashburn', coords: { x: 270, y: 185 } },
      { id: 'edge-fra', code: 'FRA', city: 'Frankfurt', coords: { x: 515, y: 145 } },
      { id: 'edge-nrt', code: 'NRT', city: 'Tokyo', coords: { x: 840, y: 185 } },
      { id: 'edge-sin', code: 'SIN', city: 'Singapore', coords: { x: 755, y: 310 } },
      { id: 'edge-gru', code: 'GRU', city: 'São Paulo', coords: { x: 345, y: 385 } },
    ]
    
    export const attackArcs: AttackArc[] = [
      {
        id: 'arc-1',
        originId: 'origin-1',
        edgeId: 'edge-nrt',
        sourceCoords: { x: 785, y: 185 },
        targetCoords: { x: 840, y: 185 },
        controlOffset: -40,
        vectorType: 'l7',
        gradientId: 'attack-gradient-amber',
      },
      {
        id: 'arc-2',
        originId: 'origin-1',
        edgeId: 'edge-iad',
        sourceCoords: { x: 785, y: 185 },
        targetCoords: { x: 270, y: 185 },
        controlOffset: -160,
        vectorType: 'l7',
        gradientId: 'attack-gradient-amber',
      },
      {
        id: 'arc-3',
        originId: 'origin-2',
        edgeId: 'edge-fra',
        sourceCoords: { x: 595, y: 135 },
        targetCoords: { x: 515, y: 145 },
        controlOffset: -35,
        vectorType: 'l7',
        gradientId: 'attack-gradient-amber',
      },
      {
        id: 'arc-4',
        originId: 'origin-3',
        edgeId: 'edge-iad',
        sourceCoords: { x: 310, y: 340 },
        targetCoords: { x: 270, y: 185 },
        controlOffset: -45,
        vectorType: 'botnet',
        gradientId: 'attack-gradient-info',
      },
      {
        id: 'arc-5',
        originId: 'origin-4',
        edgeId: 'edge-sin',
        sourceCoords: { x: 725, y: 275 },
        targetCoords: { x: 755, y: 310 },
        controlOffset: 30,
        vectorType: 'volumetric',
        gradientId: 'attack-gradient-rose',
      },
    ]
    
    export function computeCurvePath(arc: AttackArc): string {
      const { sourceCoords, targetCoords, controlOffset } = arc
      const midX = (sourceCoords.x + targetCoords.x) / 2
      const midY = (sourceCoords.y + targetCoords.y) / 2 + controlOffset
      return `M ${sourceCoords.x} ${sourceCoords.y} Q ${midX} ${midY} ${targetCoords.x} ${targetCoords.y}`
    }
    
    export function originColor(type: AttackVectorType): string {
      if (type === 'volumetric') return '#f43f5e'
      if (type === 'l7') return '#f59e0b'
      return '#0ea5e9'
    }
    
    export function mitigationVariant(action: MitigationAction): 'destructive' | 'warning' | 'info' {
      if (action === 'DROP') return 'destructive'
      if (action === 'CHALLENGE') return 'warning'
      return 'info'
    }
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-threat-map-radar, [ui-threat-map-radar]',
      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: [
        UiBadgeComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiMapComponent,
      ],
      template: `
        <div data-slot="threat-map-radar" [class]="rootClass">
          <div class="flex flex-wrap items-center justify-between gap-3">
            <div class="flex flex-wrap items-center gap-2">
              <h2 class="text-foreground text-lg font-semibold tracking-tight">Threat Radar</h2>
              <span ui-badge variant="outline" class="border-success/30 bg-success/10 text-success text-xs font-medium">
                Live
              </span>
            </div>
            <p class="text-muted-foreground text-xs">{{ origins().length }} origins · {{ edges.length }} edge PoPs</p>
          </div>
    
          <div class="grid min-h-0 flex-1 grid-cols-1 gap-4 lg:grid-cols-12">
            <div ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-8">
              <div class="relative min-h-[360px] flex-1 overflow-hidden bg-zinc-950">
                <div
                  ui-map
                  variant="dark"
                  [accessToken]="accessToken() ?? ''"
                  [center]="[0, 18]"
                  [zoom]="1.05"
                  [navigation]="false"
                  class="pointer-events-none absolute inset-0 size-full"
                ></div>
                <svg
                  class="pointer-events-none absolute inset-0 size-full"
                  viewBox="0 0 1000 500"
                  fill="none"
                  preserveAspectRatio="xMidYMid slice"
                >
                  <defs>
                    <linearGradient id="attack-gradient-rose" x1="0%" y1="0%" x2="100%" y2="0%">
                      <stop offset="0%" stop-color="#f43f5e" stop-opacity="0.95" />
                      <stop offset="100%" stop-color="#10b981" stop-opacity="0.35" />
                    </linearGradient>
                    <linearGradient id="attack-gradient-amber" x1="0%" y1="0%" x2="100%" y2="0%">
                      <stop offset="0%" stop-color="#f59e0b" stop-opacity="0.95" />
                      <stop offset="100%" stop-color="#38bdf8" stop-opacity="0.35" />
                    </linearGradient>
                    <linearGradient id="attack-gradient-info" x1="0%" y1="0%" x2="100%" y2="0%">
                      <stop offset="0%" stop-color="#0ea5e9" stop-opacity="0.95" />
                      <stop offset="100%" stop-color="#38bdf8" stop-opacity="0.35" />
                    </linearGradient>
                  </defs>
    
                  @for (arc of filteredArcs(); track arc.id) {
                    <g>
                      <path
                        [attr.d]="curvePath(arc)"
                        fill="none"
                        [attr.stroke]="'url(#' + arc.gradientId + ')'"
                        stroke-width="2"
                        stroke-dasharray="6 4"
                        stroke-linecap="round"
                      >
                        <animate attributeName="stroke-dashoffset" from="100" to="0" dur="2.5s" repeatCount="indefinite" />
                      </path>
                      <circle r="3" fill="#ffffff">
                        <animateMotion [attr.path]="curvePath(arc)" dur="2.8s" repeatCount="indefinite" />
                      </circle>
                    </g>
                  }
    
                  @for (edge of edges; track edge.id) {
                    <g [attr.transform]="'translate(' + edge.coords.x + ', ' + edge.coords.y + ')'">
                      <polygon points="0,-5 5,0 0,5 -5,0" fill="#10b981" stroke="#ffffff" stroke-width="1.25" />
                      <text
                        y="16"
                        text-anchor="middle"
                        fill="rgba(255,255,255,0.85)"
                        font-size="11"
                        font-family="ui-monospace, monospace"
                        font-weight="700"
                      >
                        {{ edge.code }}
                      </text>
                    </g>
                  }
    
                  @for (origin of origins(); track origin.id) {
                    <g
                      [attr.transform]="'translate(' + origin.coords.x + ', ' + origin.coords.y + ')'"
                      class="pointer-events-auto cursor-pointer"
                      role="button"
                      tabindex="0"
                      [attr.aria-label]="origin.country"
                      (click)="selectOrigin(origin.id)"
                      (keydown.enter)="selectOrigin(origin.id)"
                    >
                      <circle r="6" [attr.fill]="colorFor(origin.vectorType)" stroke="#ffffff" stroke-width="1.5" />
                      <text
                        y="-10"
                        text-anchor="middle"
                        fill="#ffffff"
                        font-size="11"
                        font-family="ui-monospace, monospace"
                        font-weight="700"
                      >
                        {{ origin.countryCode }}
                      </text>
                    </g>
                  }
                </svg>
              </div>
            </div>
    
            <div ui-card class="border-border flex min-h-0 flex-col overflow-hidden shadow-xs lg:col-span-4">
              <div ui-card-header class="border-border border-b p-4">
                <h3 ui-card-title class="text-sm font-semibold">Origins</h3>
              </div>
              <div ui-card-content class="divide-border min-h-0 flex-1 divide-y overflow-auto p-0">
                @for (origin of origins(); track origin.id) {
                  <button type="button" [class]="originRowClass(origin.id)" (click)="selectOrigin(origin.id)">
                    <div class="min-w-0">
                      <p class="text-foreground truncate text-sm font-medium">
                        {{ origin.countryCode }} · {{ origin.country }}
                      </p>
                      <p class="text-muted-foreground font-mono text-xs tabular-nums">{{ origin.volume }}</p>
                    </div>
                    <span ui-badge [variant]="variantFor(origin.mitigation)" class="shrink-0 text-xs uppercase">
                      {{ origin.mitigation.replace('_', ' ') }}
                    </span>
                  </button>
                }
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiThreatMapRadarComponent {
      @Input('origins') set originsInput(v: ThreatOrigin[] | undefined) {
        this.origins.set(v ?? defaultOrigins.map((o) => ({ ...o, coords: { ...o.coords } })))
      }
      @Input('accessToken') set accessTokenInput(v: string | undefined) {
        this.accessToken.set(v)
      }
      @Input('class') className?: string
    
      protected readonly edges = edgeNodes
    
      readonly origins = signal<ThreatOrigin[]>(defaultOrigins.map((o) => ({ ...o, coords: { ...o.coords } })))
      readonly accessToken = signal<string | undefined>(undefined)
      readonly selectedId = signal<string | null>(null)
    
      readonly filteredArcs = computed(() => {
        const selected = this.selectedId()
        if (!selected) return attackArcs
        return attackArcs.filter((arc) => arc.originId === selected)
      })
    
      get rootClass(): string {
        return cn('flex h-full min-h-0 flex-col gap-4', this.className)
      }
    
      originRowClass(id: string): string {
        return cn(
          'hover:bg-accent/50 flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors',
          this.selectedId() === id && 'bg-accent/60',
        )
      }
    
      curvePath(arc: AttackArc): string {
        return computeCurvePath(arc)
      }
    
      colorFor(type: AttackVectorType): string {
        return originColor(type)
      }
    
      variantFor(action: MitigationAction): 'destructive' | 'warning' | 'info' {
        return mitigationVariant(action)
      }
    
      selectOrigin(id: string): void {
        this.selectedId.update((current) => (current === id ? null : id))
      }
    }
    
  • src/app/components/blocks/threat-map-radar/index.ts0.3 kB

Raw manifest:https://uipkge.dev/r/angular/threat-map-radar.json