Menu

Change language

GitHub repo

Rate Limiting Config

blockdevops

Cloudflare/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

$npx uipkge-ng@latest add rate-limiting-config
Manifest:https://uipkge.dev/r/angular/rate-limiting-config.jsonInstalls to:src/app/components/blocks/rate-limiting-config/

Variants

Loading interactive previews…

Schema

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

RateLimitRule
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
}
SimStats
interface SimStats {
  allowed: number
  dropped: number
  allowedPct: number
  droppedPct: number
  bucketFill: number
  status: 'optimal' | 'burst' | 'throttling'
  statusLabel: string
  latency: string
  description: string
}

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 &lt; 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

Raw manifest:https://uipkge.dev/r/angular/rate-limiting-config.json