Threat Map Radar
blocksecurityLive threat radar over a Mapbox world map. Attack arcs hit edge PoPs; click an origin to isolate its vector.
Installation
$pnpm dlx uipkge-ng@latest add threat-map-radar$npx uipkge-ng@latest add threat-map-radar$yarn dlx uipkge-ng@latest add threat-map-radar$bunx uipkge-ng@latest add threat-map-radarManifest:
https://uipkge.dev/r/angular/threat-map-radar.jsonInstalls to:src/app/components/blocks/threat-map-radar/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ThreatOrigininterface ThreatOrigin {
id: string
country: string
countryCode: string
volume: string
vectorType: AttackVectorType
mitigation: MitigationAction
coords: { x: number; y: number }
targetEdgeId: string
}EdgePoPinterface EdgePoP {
id: string
code: string
city: string
coords: { x: number; y: number }
}AttackArcinterface 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
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
export { UiThreatMapRadarComponent, type AttackArc, type AttackVectorType, type EdgePoP, type MitigationAction, type ThreatOrigin, attackArcs, computeCurvePath, defaultOrigins, edgeNodes, mitigationVariant, originColor, } from './threat-map-radar.component'
Raw manifest:https://uipkge.dev/r/angular/threat-map-radar.json