Rate Limiting Config
blockdevopsCloudflare/Upstash style API rate limiting rules builder and token bucket traffic monitor with 4 health metrics cards, interactive token bucket vs sliding window simulation slider, and configured mitigation rules table with status controls and contextual action menus.
Installation
$pnpm dlx uipkge-ng@latest add rate-limiting-config$npx uipkge-ng@latest add rate-limiting-config$yarn dlx uipkge-ng@latest add rate-limiting-config$bunx uipkge-ng@latest add rate-limiting-configManifest:
https://uipkge.dev/r/angular/rate-limiting-config.jsonInstalls to:src/app/components/blocks/rate-limiting-config/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.
RateLimitRuleinterface RateLimitRule {
id: string
name: string
scope: string
path: string
method: string
limitThreshold: string
mitigationAction: 'HTTP 429 Too Many Requests' | 'CAPTCHA Challenge' | 'Block IP'
actionVariant: 'warning' | 'secondary' | 'destructive'
enabled: boolean
lastTriggered: string
blocked24h: string
}SimStatsinterface SimStats {
allowed: number
dropped: number
allowedPct: number
droppedPct: number
bucketFill: number
status: 'optimal' | 'burst' | 'throttling'
statusLabel: string
latency: string
description: string
}npm dependencies
Files installed (2)
src/app/components/blocks/rate-limiting-config/rate-limiting-config.component.ts31.2 kB
// Cloudflare/Upstash style API rate limiting rules builder and token bucket traffic monitor // with 4 health metrics cards, an interactive token bucket vs sliding window simulation // slider, and a configured mitigation rules table. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core' import { Activity, AlertTriangle, CheckCircle2, Copy, Database, Edit3, LucideAngularModule, MoreHorizontal, Plus, RefreshCw, Search, Shield, ShieldAlert, ShieldCheck, Sliders, Timer, Trash2, TrendingUp, Zap, ZapOff, } 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, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, } from '@/ui/dropdown-menu/dropdown-menu.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSliderComponent } from '@/ui/slider/slider.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' export interface RateLimitRule { id: string name: string scope: string path: string method: string limitThreshold: string mitigationAction: 'HTTP 429 Too Many Requests' | 'CAPTCHA Challenge' | 'Block IP' actionVariant: 'warning' | 'secondary' | 'destructive' enabled: boolean lastTriggered: string blocked24h: string } export type RateLimitAlgorithm = 'token-bucket' | 'sliding-window' export interface SimStats { allowed: number dropped: number allowedPct: number droppedPct: number bucketFill: number status: 'optimal' | 'burst' | 'throttling' statusLabel: string latency: string description: string } export const INITIAL_RULES: RateLimitRule[] = [ { id: 'rule-auth-bruteforce', name: 'auth-endpoint-bruteforce', scope: 'Login & SSO token issuance', path: '/api/v1/auth/*', method: 'POST', limitThreshold: '100 req / 60s per IP', mitigationAction: 'HTTP 429 Too Many Requests', actionVariant: 'warning', enabled: true, lastTriggered: '2 mins ago', blocked24h: '842 blocks', }, { id: 'rule-public-unauth', name: 'public-api-v1-unauthenticated', scope: 'Anonymous REST endpoints', path: '/api/v1/public/*', method: 'GET, POST', limitThreshold: '300 req / 60s per IP', mitigationAction: 'CAPTCHA Challenge', actionVariant: 'secondary', enabled: true, lastTriggered: '14 mins ago', blocked24h: '412 challenges', }, { id: 'rule-ai-inference', name: 'ai-inference-tier-standard', scope: 'LLM completions & embeddings', path: '/api/v1/models/generate', method: 'POST', limitThreshold: '20 req / 60s per API Key', mitigationAction: 'HTTP 429 Too Many Requests', actionVariant: 'warning', enabled: true, lastTriggered: '1 hour ago', blocked24h: '148 throttled', }, { id: 'rule-search-cluster', name: 'search-endpoints', scope: 'Vector & fuzzy full-text query', path: '/api/v1/search/*', method: 'GET', limitThreshold: '60 req / 60s per IP', mitigationAction: 'Block IP', actionVariant: 'destructive', enabled: false, lastTriggered: '3 days ago', blocked24h: '18 blocks', }, ] export function computeSimStats(algorithm: RateLimitAlgorithm, vol: number): SimStats { if (algorithm === 'token-bucket') { const refillRate = 50 const burstCapacity = 100 if (vol <= refillRate) { return { allowed: vol, dropped: 0, allowedPct: 100, droppedPct: 0, bucketFill: Math.round(100 - (vol / refillRate) * 20), status: 'optimal', statusLabel: 'Optimal Throughput · No Throttling', latency: '0.35ms', description: `Traffic volume (${vol} req/s) is well within the 50 req/s continuous refill capacity. 100% of requests pass with zero drop rate.`, } } else if (vol <= burstCapacity) { return { allowed: vol, dropped: 0, allowedPct: 100, droppedPct: 0, bucketFill: Math.max(10, Math.round(100 - ((vol - refillRate) / (burstCapacity - refillRate)) * 80)), status: 'burst', statusLabel: 'Consuming Burst Pool · 0% Dropped', latency: '0.42ms', description: `Traffic exceeds the 50 req/s replenishment rate. Tokens are being drawn from the 100-token burst reservoir. Zero requests dropped so far.`, } } else { const allowed = burstCapacity const dropped = vol - allowed const allowedPct = Math.round((allowed / vol) * 100) return { allowed, dropped, allowedPct, droppedPct: 100 - allowedPct, bucketFill: 0, status: 'throttling', statusLabel: `Rate Limiting Active · ${100 - allowedPct}% Dropped`, latency: '0.51ms', description: `Burst reservoir exhausted. Token bucket limiter is actively enforcing HTTP 429 throttling on ${dropped} req/s excess traffic.`, } } } const windowLimit = 120 if (vol <= windowLimit) { return { allowed: vol, dropped: 0, allowedPct: 100, droppedPct: 0, bucketFill: Math.round((vol / windowLimit) * 100), status: 'optimal', statusLabel: 'Within Sliding Window Quota', latency: '0.38ms', description: `Current 60-second rolling window is ${Math.round((vol / windowLimit) * 100)}% utilized. No mitigation actions triggered.`, } } const allowed = windowLimit const dropped = vol - allowed const allowedPct = Math.round((allowed / vol) * 100) return { allowed, dropped, allowedPct, droppedPct: 100 - allowedPct, bucketFill: 100, status: 'throttling', statusLabel: `Window Saturated · ${100 - allowedPct}% Shed`, latency: '0.48ms', description: `Rolling time-window threshold exceeded (${vol} req/s > ${windowLimit} limit). Excess requests are rejected at edge proxy.`, } } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-rate-limiting-config, [ui-rate-limiting-config]', standalone: true, host: { '[attr.data-slot]': '"rate-limiting-config"', '[class]': 'hostClass', }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiDropdownMenuComponent, UiDropdownMenuContentComponent, UiDropdownMenuItemComponent, UiDropdownMenuLabelComponent, UiDropdownMenuSeparatorComponent, UiDropdownMenuTriggerComponent, UiProgressComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSliderComponent, UiSwitchComponent, UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, ], template: ` <!-- Header Section --> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1.5"> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="text-xs font-medium tracking-wide uppercase"> <lucide-icon [img]="ShieldCheck" class="text-primary size-3.5" /> Edge Protection </span> <span class="text-muted-foreground text-xs">Cloudflare & Upstash compatible</span> </div> <h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl"> Rate Limiting & Traffic Protection </h1> <p class="text-muted-foreground text-sm"> Configure DDoS mitigation, IP throttling, and per-tier request quotas. </p> </div> <div class="flex flex-wrap items-center gap-3"> <div class="bg-card border-border flex items-center gap-3 rounded-lg border px-3 py-2 shadow-xs"> <div class="flex flex-col"> <span class="text-foreground text-xs font-semibold">Global Protection</span> <span class="text-muted-foreground text-xs tabular-nums"> {{ globalProtection() ? activeRulesCount() + ' Active Rules' : 'Disabled (Bypassed)' }} </span> </div> <ui-switch [checked]="globalProtection()" (checkedChange)="globalProtection.set($event)" /> </div> <button ui-button class="gap-1.5 shadow-xs"> <lucide-icon [img]="Plus" class="size-4" /> <span>Add Rate Limit Rule</span> </button> </div> </div> <!-- 4 Health Cards --> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Total Requests / min</h3> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Activity" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">4,250 req/s</div> <div class="flex items-center gap-1.5 text-xs"> <span ui-badge variant="success" class="h-4.5 px-1 py-0 text-xs"> <lucide-icon [img]="TrendingUp" class="size-3" /> +12.4% </span> <span class="text-muted-foreground tabular-nums">255.0k / min · 99.98% ok</span> </div> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Blocked Attacks</h3> <div class="bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="ShieldAlert" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">1,420 in 24h</div> <div class="flex items-center gap-1.5 text-xs"> <span ui-badge variant="outline" class="h-4.5 px-1.5 py-0 text-xs font-normal">Tier-1 Edge</span> <span class="text-muted-foreground truncate tabular-nums">18 brute-force bursts</span> </div> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Active Throttles</h3> <div class="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="ZapOff" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">18 IPs</div> <div class="flex items-center gap-1.5 text-xs"> <span class="bg-warning inline-block size-1.5 rounded-full"></span> <span class="text-muted-foreground truncate tabular-nums">4 edge clusters · Exp 15m</span> </div> </div> </div> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-2"> <h3 ui-card-title class="text-muted-foreground text-xs font-medium">Median Latency Overhead</h3> <div class="bg-info/10 text-info flex size-8 items-center justify-center rounded-md"> <lucide-icon [img]="Timer" class="size-4" /> </div> </div> <div ui-card-content class="space-y-1"> <div class="text-foreground text-2xl font-bold tracking-tight tabular-nums">0.4ms</div> <div class="flex items-center gap-1.5 text-xs"> <span class="text-muted-foreground tabular-nums">p99 < 1.2ms · Multi-Region Redis</span> </div> </div> </div> </div> <!-- Interactive Token Bucket Visualizer --> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <h3 ui-card-title class="text-lg font-semibold">Algorithm Simulation & Token Dynamics</h3> <span ui-badge variant="outline" class="text-xs font-normal">Interactive Sandbox</span> </div> <p ui-card-description class="text-xs"> Test traffic volume against token reservoir dynamics and rolling time windows. </p> </div> <div class="flex flex-wrap items-center gap-2"> <span class="text-muted-foreground text-xs font-medium">Algorithm:</span> <ui-select [value]="algorithm()" (valueChange)="setAlgorithm($event)"> <button ui-select-trigger class="w-52 text-xs"> <ui-select-value placeholder="Select algorithm" /> </button> <ui-select-content> <ui-select-item value="token-bucket">Token Bucket (Burst Tolerant)</ui-select-item> <ui-select-item value="sliding-window">Sliding Window Counter</ui-select-item> </ui-select-content> </ui-select> </div> </div> <div ui-card-content class="space-y-6"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4"> <div class="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> {{ algorithm() === 'token-bucket' ? '1. Refill Velocity' : '1. Time Window' }} </span> <lucide-icon [img]="RefreshCw" class="text-muted-foreground size-3.5" /> </div> <div class="text-foreground text-lg font-semibold tabular-nums"> {{ algorithm() === 'token-bucket' ? '50 tokens / s' : '60s Window' }} </div> <p class="text-muted-foreground text-xs"> {{ algorithm() === 'token-bucket' ? 'Deterministic continuous refill into pool' : 'Rolling granular sub-window slices' }} </p> </div> <div class="mt-3"><ui-progress [value]="100" class="h-1.5" /></div> </div> <div class="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase"> {{ algorithm() === 'token-bucket' ? '2. Reservoir Capacity' : '2. Window Ceiling' }} </span> <lucide-icon [img]="Database" class="text-muted-foreground size-3.5" /> </div> <div class="text-foreground text-lg font-semibold tabular-nums"> {{ algorithm() === 'token-bucket' ? '100 tokens max' : '120 req / window' }} </div> <p class="text-muted-foreground text-xs"> {{ algorithm() === 'token-bucket' ? 'Pool status: ' + simStats().bucketFill + '% available' : 'Window utilized: ' + simStats().bucketFill + '%' }} </p> </div> <div class="mt-3"><ui-progress [value]="simStats().bucketFill" class="h-1.5" /></div> </div> <div class="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase" >3. Test Traffic Load</span > <lucide-icon [img]="Zap" class="text-primary size-3.5" /> </div> <div class="text-foreground text-lg font-semibold tabular-nums">{{ simVolume() }} req/s</div> <p class="text-muted-foreground text-xs">Simulated rate generated from slider</p> </div> <div class="mt-3"> <ui-progress [value]="sliderFillPct()" class="h-1.5" /> </div> </div> <div class="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5"> <div class="space-y-1.5"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs font-medium tracking-wider uppercase" >4. Edge Gate Verdict</span > @if (simStats().status === 'optimal') { <lucide-icon [img]="CheckCircle2" class="text-success size-3.5" /> } @else if (simStats().status === 'burst') { <lucide-icon [img]="AlertTriangle" class="text-warning size-3.5" /> } @else { <lucide-icon [img]="ShieldAlert" class="text-destructive size-3.5" /> } </div> <div class="text-foreground text-lg font-semibold tabular-nums"> {{ simStats().allowed }} Pass · {{ simStats().dropped }} Drop </div> <p class="text-muted-foreground text-xs">Overhead: {{ simStats().latency }}</p> </div> <div class="mt-3"> <div class="bg-secondary relative h-1.5 w-full overflow-hidden rounded-full"> <div class="bg-success absolute top-0 bottom-0 left-0 transition-colors duration-150" [style.width.%]="simStats().allowedPct" ></div> <div class="bg-destructive absolute top-0 right-0 bottom-0 transition-colors duration-150" [style.width.%]="simStats().droppedPct" ></div> </div> </div> </div> </div> <!-- Interactive Simulator Slider Box --> <div class="bg-muted/20 border-border space-y-4 rounded-lg border p-4"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-0.5"> <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="Sliders" class="text-primary size-4" /> <span class="text-foreground text-sm font-semibold">Simulate Incoming Request Volume</span> </div> <p class="text-muted-foreground text-xs"> Drag the slider to stress-test the algorithm against burst spikes and DDoS loads. </p> </div> <div class="flex flex-wrap items-center gap-2"> <span ui-badge [variant]=" simStats().status === 'optimal' ? 'success' : simStats().status === 'burst' ? 'warning' : 'destructive' " class="text-xs font-medium" > {{ simStats().statusLabel }} </span> <div class="bg-card border-border rounded border px-2.5 py-1 font-mono text-sm font-bold tabular-nums"> {{ simVolume() }} req/s </div> </div> </div> <div class="space-y-2 pt-2"> <ui-slider [value]="[simVolume()]" (valueChange)="onSimVolumeChange($event)" [min]="10" [max]="500" [step]="10" class="w-full" /> <div class="text-muted-foreground flex justify-between text-xs tabular-nums"> <span>10 req/s (Low)</span> <span>50 req/s (Refill Rate)</span> <span>100 req/s (Burst Limit)</span> <span>250 req/s (Heavy Spike)</span> <span>500 req/s (DDoS Attack)</span> </div> </div> <div class="space-y-3 pt-2"> <div class="space-y-1.5"> <div class="flex justify-between text-xs font-medium"> <span class="text-success flex items-center gap-1"> <span class="bg-success size-2 rounded-full"></span> Allowed: {{ simStats().allowed }} req/s ({{ simStats().allowedPct }}%) </span> <span class="text-destructive flex items-center gap-1"> <span class="bg-destructive size-2 rounded-full"></span> Rate Limited / 429: {{ simStats().dropped }} req/s ({{ simStats().droppedPct }}%) </span> </div> <div class="bg-muted border-border/50 relative h-2.5 w-full overflow-hidden rounded-full border"> <div class="bg-success absolute top-0 bottom-0 left-0 transition-colors duration-150" [style.width.%]="simStats().allowedPct" ></div> <div class="bg-destructive absolute top-0 right-0 bottom-0 transition-colors duration-150" [style.width.%]="simStats().droppedPct" ></div> </div> </div> <div class="bg-card border-border/80 flex items-start gap-2.5 rounded-md border p-3 text-xs"> <lucide-icon [img]="Shield" class="text-primary mt-0.5 size-4 shrink-0" /> <p class="text-muted-foreground leading-relaxed">{{ simStats().description }}</p> </div> </div> </div> </div> </div> <!-- Configured Rules Table Card --> <div ui-card class="shadow-xs"> <div ui-card-header class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <h3 ui-card-title class="text-lg font-semibold">Configured Rate Limiting Rules</h3> <span ui-badge variant="secondary" class="text-xs font-normal tabular-nums"> {{ filteredRules().length }} Policies </span> </div> <p ui-card-description class="text-xs"> Priority-ordered edge firewall rules evaluated sequentially for incoming traffic. </p> </div> <div class="relative w-full sm:w-64"> <lucide-icon [img]="Search" class="text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" /> <input type="text" placeholder="Search rules, paths, actions…" [value]="searchQuery()" (input)="searchQuery.set($any($event.target).value)" class="border-input bg-background placeholder:text-muted-foreground focus-visible:ring-ring/50 focus-visible:border-ring h-8.5 w-full rounded-md border pr-3 pl-8 text-xs shadow-xs outline-none focus-visible:ring-[3px]" /> </div> </div> <div ui-card-content class="p-0"> <div class="overflow-x-auto"> <table ui-table> <thead ui-table-header> <tr ui-table-row> <th ui-table-head class="w-[280px]">Rule Name & Scope</th> <th ui-table-head class="w-[220px]">Path Matcher</th> <th ui-table-head class="w-[180px]">Limit Threshold</th> <th ui-table-head class="w-[200px]">Mitigation Action</th> <th ui-table-head class="w-[90px] text-center">Status</th> <th ui-table-head class="w-[60px] text-right">Actions</th> </tr> </thead> <tbody ui-table-body> @for (rule of filteredRules(); track rule.id) { <tr ui-table-row [class]="rule.enabled ? '' : 'bg-muted/20 opacity-60'"> <td ui-table-cell> <div class="space-y-0.5"> <div class="flex flex-wrap items-center gap-2"> <span class="text-foreground font-mono text-xs font-semibold">{{ rule.name }}</span> </div> <div class="text-muted-foreground flex items-center gap-1.5 text-xs"> <span>{{ rule.scope }}</span> <span>·</span> <span class="tabular-nums">{{ rule.blocked24h }} in 24h</span> </div> </div> </td> <td ui-table-cell> <div class="flex items-center gap-1.5"> <span ui-badge variant="outline" class="font-mono text-xs uppercase">{{ rule.method }}</span> <code class="bg-muted/60 border-border text-foreground rounded border px-1.5 py-0.5 font-mono text-xs" > {{ rule.path }} </code> </div> </td> <td ui-table-cell> <div class="space-y-0.5"> <div class="text-foreground font-mono text-xs font-medium tabular-nums"> {{ rule.limitThreshold }} </div> <div class="text-muted-foreground text-xs">Triggered {{ rule.lastTriggered }}</div> </div> </td> <td ui-table-cell> <span ui-badge [variant]="rule.actionVariant" class="text-xs font-medium"> <span [class]=" 'mr-1 size-1.5 rounded-full ' + (rule.actionVariant === 'warning' ? 'bg-warning' : rule.actionVariant === 'destructive' ? 'bg-destructive' : 'bg-secondary-foreground') " ></span> {{ rule.mitigationAction }} </span> </td> <td ui-table-cell class="text-center"> <ui-switch [checked]="rule.enabled" [attr.aria-label]="'Toggle rule ' + rule.name" (checkedChange)="toggleRule(rule.id, $event)" /> </td> <td ui-table-cell class="text-right"> <ui-dropdown-menu> <button ui-button ui-dropdown-menu-trigger variant="ghost" size="icon" class="size-8"> <lucide-icon [img]="MoreHorizontal" class="size-4" /> <span class="sr-only">Open menu</span> </button> <ui-dropdown-menu-content align="end" class="w-48"> <ui-dropdown-menu-label class="text-xs">Rule Configuration</ui-dropdown-menu-label> <ui-dropdown-menu-item class="gap-2 text-xs"> <lucide-icon [img]="Edit3" class="size-3.5" /> <span>Edit Rule</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item class="gap-2 text-xs"> <lucide-icon [img]="Activity" class="size-3.5" /> <span>View Metrics</span> </ui-dropdown-menu-item> <ui-dropdown-menu-item class="gap-2 text-xs"> <lucide-icon [img]="Copy" class="size-3.5" /> <span>Duplicate Rule</span> </ui-dropdown-menu-item> <ui-dropdown-menu-separator /> <ui-dropdown-menu-item class="text-destructive focus:text-destructive gap-2 text-xs"> <lucide-icon [img]="Trash2" class="size-3.5" /> <span>Delete Rule</span> </ui-dropdown-menu-item> </ui-dropdown-menu-content> </ui-dropdown-menu> </td> </tr> } </tbody> </table> </div> </div> </div> `, }) export class UiRateLimitingConfigComponent { protected readonly Activity = Activity protected readonly AlertTriangle = AlertTriangle protected readonly CheckCircle2 = CheckCircle2 protected readonly Copy = Copy protected readonly Database = Database protected readonly Edit3 = Edit3 protected readonly MoreHorizontal = MoreHorizontal protected readonly Plus = Plus protected readonly RefreshCw = RefreshCw protected readonly Search = Search protected readonly Shield = Shield protected readonly ShieldAlert = ShieldAlert protected readonly ShieldCheck = ShieldCheck protected readonly Sliders = Sliders protected readonly Timer = Timer protected readonly Trash2 = Trash2 protected readonly TrendingUp = TrendingUp protected readonly Zap = Zap protected readonly ZapOff = ZapOff @Input('class') className?: string readonly globalProtection = signal(true) readonly algorithm = signal<RateLimitAlgorithm>('token-bucket') readonly simVolume = signal(120) readonly searchQuery = signal('') readonly rules = signal<RateLimitRule[]>(INITIAL_RULES.map((r) => ({ ...r }))) readonly activeRulesCount = computed(() => this.rules().filter((r) => r.enabled).length) readonly filteredRules = computed(() => { const q = this.searchQuery().trim().toLowerCase() if (!q) return this.rules() return this.rules().filter( (r) => r.name.toLowerCase().includes(q) || r.path.toLowerCase().includes(q) || r.scope.toLowerCase().includes(q) || r.mitigationAction.toLowerCase().includes(q), ) }) readonly simStats = computed<SimStats>(() => computeSimStats(this.algorithm(), this.simVolume())) readonly sliderFillPct = computed(() => Math.min(100, Math.round((this.simVolume() / 500) * 100))) get hostClass(): string { return cn('mx-auto w-full max-w-6xl space-y-6', this.className) } toggleRule(id: string, enabled?: boolean): void { this.rules.update((prev) => prev.map((r) => (r.id === id ? { ...r, enabled: enabled ?? !r.enabled } : r))) } setAlgorithm(value: string): void { this.algorithm.set(value as RateLimitAlgorithm) } onSimVolumeChange(value: number[]): void { if (value[0] !== undefined) this.simVolume.set(value[0]) } }src/app/components/blocks/rate-limiting-config/index.ts0.2 kB
export { UiRateLimitingConfigComponent, INITIAL_RULES, computeSimStats, type RateLimitRule, type RateLimitAlgorithm, type SimStats, } from './rate-limiting-config.component'
Raw manifest:https://uipkge.dev/r/angular/rate-limiting-config.json