Leaflet Threat Map Radar
blocksecurityLive threat radar on free OpenStreetMap/Esri dark tiles — no API key. Attack arcs hit edge PoPs under a rotating sweep; click an origin to isolate its vector.
Installation
$pnpm dlx uipkge-ng@latest add leaflet-threat-map-radar$npx uipkge-ng@latest add leaflet-threat-map-radar$yarn dlx uipkge-ng@latest add leaflet-threat-map-radar$bunx uipkge-ng@latest add leaflet-threat-map-radarManifest:
https://uipkge.dev/r/angular/leaflet-threat-map-radar.jsonInstalls to:src/app/components/blocks/leaflet-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
/** [lng, lat] */
lngLat: [number, number]
targetEdgeId: string
}EdgePoPinterface EdgePoP {
id: string
code: string
city: string
/** [lng, lat] */
lngLat: [number, number]
}AttackArcinterface AttackArc {
id: string
originId: string
edgeId: string
vectorType: AttackVectorType
/** Sampled arc path as [lng, lat][]. */
path: [number, number][]
}Includes
Files installed (2)
src/app/components/blocks/leaflet-threat-map-radar/leaflet-threat-map-radar.component.ts11.4 kB
// Live threat radar on free OpenStreetMap/Esri dark tiles — no API key. Attack arcs hit // edge PoPs under a rotating sweep; click an origin to isolate its vector. Port of the React block 1:1. 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 { UiLeafletMapComponent, UiLeafletMarkerComponent, UiLeafletPolylineComponent, } from '@/ui/leaflet-map/leaflet-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 /** [lng, lat] */ lngLat: [number, number] targetEdgeId: string } export interface EdgePoP { id: string code: string city: string /** [lng, lat] */ lngLat: [number, number] } export interface AttackArc { id: string originId: string edgeId: string vectorType: AttackVectorType /** Sampled arc path as [lng, lat][]. */ path: [number, number][] } export const defaultOrigins: ThreatOrigin[] = [ { id: 'origin-1', country: 'China', countryCode: 'CN', volume: '642.5K/s', vectorType: 'l7', mitigation: 'DROP', lngLat: [104.2, 35.9], targetEdgeId: 'edge-nrt', }, { id: 'origin-2', country: 'Russia', countryCode: 'RU', volume: '418.2K/s', vectorType: 'l7', mitigation: 'CHALLENGE', lngLat: [37.6, 55.7], targetEdgeId: 'edge-fra', }, { id: 'origin-3', country: 'Brazil', countryCode: 'BR', volume: '285.4K/s', vectorType: 'botnet', mitigation: 'RATE_LIMIT', lngLat: [-51.9, -14.2], targetEdgeId: 'edge-iad', }, { id: 'origin-4', country: 'Vietnam', countryCode: 'VN', volume: '194.0K/s', vectorType: 'volumetric', mitigation: 'DROP', lngLat: [105.8, 21.0], targetEdgeId: 'edge-sin', }, ] export const edgeNodes: EdgePoP[] = [ { id: 'edge-iad', code: 'IAD', city: 'Ashburn', lngLat: [-77.45, 39.04] }, { id: 'edge-fra', code: 'FRA', city: 'Frankfurt', lngLat: [8.68, 50.11] }, { id: 'edge-nrt', code: 'NRT', city: 'Tokyo', lngLat: [139.69, 35.69] }, { id: 'edge-sin', code: 'SIN', city: 'Singapore', lngLat: [103.85, 1.29] }, { id: 'edge-gru', code: 'GRU', city: 'São Paulo', lngLat: [-46.63, -23.55] }, ] // Curved attack arc: sample a quadratic bezier whose control point bows // perpendicular to the route (biased northward) for a great-circle feel. export function arcPath(from: [number, number], to: [number, number], bend = 0.22): [number, number][] { const dx = to[0] - from[0] const dy = to[1] - from[1] const dist = Math.hypot(dx, dy) || 1 let nx = -dy / dist let ny = dx / dist if (ny < 0) { nx = -nx ny = -ny } const cx = (from[0] + to[0]) / 2 + nx * dist * bend const cy = (from[1] + to[1]) / 2 + ny * dist * bend const points: [number, number][] = [] const steps = 32 for (let i = 0; i <= steps; i++) { const t = i / steps const u = 1 - t points.push([u * u * from[0] + 2 * u * t * cx + t * t * to[0], u * u * from[1] + 2 * u * t * cy + t * t * to[1]]) } return points } export const attackArcs: AttackArc[] = [ { id: 'arc-1', originId: 'origin-1', edgeId: 'edge-nrt', vectorType: 'l7', path: arcPath([104.2, 35.9], [139.69, 35.69], 0.3), }, { id: 'arc-2', originId: 'origin-1', edgeId: 'edge-iad', vectorType: 'l7', path: arcPath([104.2, 35.9], [-77.45, 39.04], 0.22), }, { id: 'arc-3', originId: 'origin-2', edgeId: 'edge-fra', vectorType: 'l7', path: arcPath([37.6, 55.7], [8.68, 50.11], 0.35), }, { id: 'arc-4', originId: 'origin-3', edgeId: 'edge-iad', vectorType: 'botnet', path: arcPath([-51.9, -14.2], [-77.45, 39.04], 0.25), }, { id: 'arc-5', originId: 'origin-4', edgeId: 'edge-sin', vectorType: 'volumetric', path: arcPath([105.8, 21.0], [103.85, 1.29], 0.35), }, ] // `hex` feeds Leaflet path options; `dot`/`ping` feed the div-icon markers. export const VECTOR_STYLES: Record<AttackVectorType, { hex: string; dot: string; ping: string }> = { volumetric: { hex: '#f43f5e', dot: 'bg-destructive', ping: 'bg-destructive' }, l7: { hex: '#f59e0b', dot: 'bg-warning', ping: 'bg-warning' }, botnet: { hex: '#0ea5e9', dot: 'bg-info', ping: 'bg-info' }, } // Module-level map camera: stable reference so the map center input never re-fires. const MAP_CENTER: [number, number] = [10, 30] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-leaflet-threat-map-radar, [ui-leaflet-threat-map-radar]', standalone: true, host: { '[attr.class]': '"contents"' }, imports: [ UiBadgeComponent, UiCardComponent, UiCardContentComponent, UiCardHeaderComponent, UiCardTitleComponent, UiLeafletMapComponent, UiLeafletMarkerComponent, UiLeafletPolylineComponent, ], template: ` <div data-slot="leaflet-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 · {{ nodes.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"> <ui-leaflet-map variant="dark" [center]="mapCenter" [zoom]="2" [minZoom]="2" [navigation]="false" [scrollWheelZoom]="false" class="pointer-events-none absolute inset-0 size-full" > <!-- Radar sweep: a rotating conic-gradient wedge layered over the tiles, plus two static range rings. --> <div class="pointer-events-none absolute inset-0 z-[500] flex items-center justify-center"> <div class="border-success/15 absolute size-[340px] rounded-full border"></div> <div class="border-success/15 absolute size-[220px] rounded-full border"></div> <div class="size-[340px] animate-[spin_9s_linear_infinite] rounded-full bg-[conic-gradient(from_0deg,rgba(16,185,129,0.35),transparent_75deg)]" ></div> </div> @for (arc of filteredArcs(); track arc.id) { <ui-leaflet-polyline [lngLatPath]="arc.path" [color]="arcColor(arc)" [weight]="2" dashArray="6 4" lineCap="round" [opacity]="0.9" /> } @for (edge of nodes; track edge.id) { <ui-leaflet-marker [lngLat]="edge.lngLat" anchor="center"> <div class="flex flex-col items-center gap-1"> <span class="bg-success block size-2.5 rotate-45 border border-white/70 shadow-xs"></span> <span class="text-success font-mono text-xs font-bold">{{ edge.code }}</span> </div> </ui-leaflet-marker> } @for (origin of origins(); track origin.id) { <ui-leaflet-marker [lngLat]="origin.lngLat" anchor="center"> <button type="button" class="pointer-events-auto relative flex flex-col items-center gap-1" [attr.aria-label]="origin.country" (click)="selectOrigin(origin.id)" > <span class="font-mono text-xs font-bold text-white">{{ origin.countryCode }}</span> <span class="relative flex size-3 items-center justify-center"> <span [class]="pingClass(origin)"></span> <span [class]="dotClass(origin)"></span> </span> </button> </ui-leaflet-marker> } </ui-leaflet-map> </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]="rowClass(origin)" (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]="mitigationVariant(origin.mitigation)" class="shrink-0 text-xs uppercase"> {{ origin.mitigation.replace('_', ' ') }} </span> </button> } </div> </div> </div> </div> `, }) export class UiLeafletThreatMapRadarComponent { @Input('origins') set originsInput(v: ThreatOrigin[] | undefined) { this.origins.set(v ?? defaultOrigins.map((o) => ({ ...o, lngLat: [...o.lngLat] as [number, number] }))) } @Input('class') className?: string readonly origins = signal<ThreatOrigin[]>(defaultOrigins.map((o) => ({ ...o }))) readonly selectedId = signal<string | null>(null) protected readonly nodes = edgeNodes protected readonly mapCenter = MAP_CENTER readonly filteredArcs = computed(() => { const sel = this.selectedId() if (!sel) return attackArcs return attackArcs.filter((arc) => arc.originId === sel) }) get rootClass(): string { return cn('flex h-full min-h-0 flex-col gap-4', this.className) } selectOrigin(id: string): void { this.selectedId.update((cur) => (cur === id ? null : id)) } mitigationVariant(action: MitigationAction): 'destructive' | 'warning' | 'info' { if (action === 'DROP') return 'destructive' if (action === 'CHALLENGE') return 'warning' return 'info' } arcColor(arc: AttackArc): string { return VECTOR_STYLES[arc.vectorType].hex } pingClass(origin: ThreatOrigin): string { return cn('absolute size-3 animate-ping rounded-full opacity-70', VECTOR_STYLES[origin.vectorType].ping) } dotClass(origin: ThreatOrigin): string { return cn('relative size-3 rounded-full border-2 border-white/80', VECTOR_STYLES[origin.vectorType].dot) } rowClass(origin: ThreatOrigin): 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() === origin.id && 'bg-accent/60', ) } }src/app/components/blocks/leaflet-threat-map-radar/index.ts0.3 kB
export { UiLeafletThreatMapRadarComponent, defaultOrigins, edgeNodes, attackArcs, VECTOR_STYLES, arcPath, type AttackVectorType, type MitigationAction, type ThreatOrigin, type EdgePoP, type AttackArc, } from './leaflet-threat-map-radar.component'
Raw manifest:https://uipkge.dev/r/angular/leaflet-threat-map-radar.json