
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
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.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/rate-limiting-config.json$npx shadcn@latest add https://uipkge.dev/r/react/rate-limiting-config.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/rate-limiting-config.json$bunx shadcn@latest add https://uipkge.dev/r/react/rate-limiting-config.jsonnpx shadcn@latest add @uipkge-react/rate-limiting-configInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
className | string | — | optional |
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
}'use client'
import * as React from 'react'
import {
Activity,
AlertTriangle,
CheckCircle2,
Copy,
Database,
Edit3,
MoreHorizontal,
Plus,
RefreshCw,
Search,
Shield,
ShieldAlert,
ShieldCheck,
Sliders,
Timer,
Trash2,
TrendingUp,
Zap,
ZapOff,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Progress } from '@/components/ui/progress'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Slider } from '@/components/ui/slider'
import { Switch } from '@/components/ui/switch'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
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 interface RateLimitingConfigProps {
className?: string
}
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 RateLimitingConfig({ className }: RateLimitingConfigProps) {
const [globalProtection, setGlobalProtection] = React.useState(true)
const [algorithm, setAlgorithm] = React.useState<'token-bucket' | 'sliding-window'>('token-bucket')
const [simVolume, setSimVolume] = React.useState(120)
const [searchQuery, setSearchQuery] = React.useState('')
const [rules, setRules] = React.useState<RateLimitRule[]>(INITIAL_RULES)
const toggleRule = React.useCallback((id: string) => {
setRules((prev) => prev.map((r) => (r.id === id ? { ...r, enabled: !r.enabled } : r)))
}, [])
const activeRulesCount = React.useMemo(() => rules.filter((r) => r.enabled).length, [rules])
const filteredRules = React.useMemo(() => {
if (!searchQuery.trim()) return rules
const q = searchQuery.toLowerCase()
return rules.filter(
(r) =>
r.name.toLowerCase().includes(q) ||
r.path.toLowerCase().includes(q) ||
r.scope.toLowerCase().includes(q) ||
r.mitigationAction.toLowerCase().includes(q),
)
}, [rules, searchQuery])
const simStats = React.useMemo(() => {
const vol = simVolume
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' as const,
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) {
const drop = 0
return {
allowed: vol,
dropped: drop,
allowedPct: 100,
droppedPct: 0,
bucketFill: Math.max(10, Math.round(100 - ((vol - refillRate) / (burstCapacity - refillRate)) * 80)),
status: 'burst' as const,
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)
const droppedPct = 100 - allowedPct
return {
allowed,
dropped,
allowedPct,
droppedPct,
bucketFill: 0,
status: 'throttling' as const,
statusLabel: `Rate Limiting Active · ${droppedPct}% Dropped`,
latency: '0.51ms',
description: `Burst reservoir exhausted. Token bucket limiter is actively enforcing HTTP 429 throttling on ${dropped} req/s excess traffic.`,
}
}
} else {
// Sliding window mode
const windowLimit = 120
if (vol <= windowLimit) {
const allowedPct = 100
return {
allowed: vol,
dropped: 0,
allowedPct,
droppedPct: 0,
bucketFill: Math.round((vol / windowLimit) * 100),
status: 'optimal' as const,
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.`,
}
} else {
const allowed = windowLimit
const dropped = vol - allowed
const allowedPct = Math.round((allowed / vol) * 100)
const droppedPct = 100 - allowedPct
return {
allowed,
dropped,
allowedPct,
droppedPct,
bucketFill: 100,
status: 'throttling' as const,
statusLabel: `Window Saturated · ${droppedPct}% Shed`,
latency: '0.48ms',
description: `Rolling time-window threshold exceeded (${vol} req/s > ${windowLimit} limit). Excess requests are rejected at edge proxy.`,
}
}
}
}, [simVolume, algorithm])
return (
<div data-slot="rate-limiting-config" className={cn('mx-auto w-full max-w-6xl space-y-6', className)}>
{/* Header Section */}
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1.5">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="outline" className="text-xs font-medium tracking-wide uppercase">
<ShieldCheck className="text-primary size-3.5" />
Edge Protection
</Badge>
<span className="text-muted-foreground text-xs">Cloudflare & Upstash compatible</span>
</div>
<h1 className="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">
Rate Limiting & Traffic Protection
</h1>
<p className="text-muted-foreground text-sm">
Configure DDoS mitigation, IP throttling, and per-tier request quotas.
</p>
</div>
{/* Header Controls */}
<div className="flex flex-wrap items-center gap-3">
<div className="bg-card border-border flex items-center gap-3 rounded-lg border px-3 py-2 shadow-xs">
<div className="flex flex-col">
<span className="text-foreground text-xs font-semibold">Global Protection</span>
<span className="text-muted-foreground text-xs tabular-nums">
{globalProtection ? `${activeRulesCount} Active Rules` : 'Disabled (Bypassed)'}
</span>
</div>
<Switch checked={globalProtection} onCheckedChange={setGlobalProtection} />
</div>
<Button className="gap-1.5 shadow-xs">
<Plus className="size-4" />
<span>Add Rate Limit Rule</span>
</Button>
</div>
</div>
{/* 4 Health Cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Card 1: Total Requests */}
<Card className="shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Total Requests / min</CardTitle>
<div className="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-md">
<Activity className="size-4" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">4,250 req/s</div>
<div className="flex items-center gap-1.5 text-xs">
<Badge variant="success" className="h-4.5 px-1 py-0 text-xs">
<TrendingUp className="size-3" />
+12.4%
</Badge>
<span className="text-muted-foreground tabular-nums">255.0k / min · 99.98% ok</span>
</div>
</CardContent>
</Card>
{/* Card 2: Blocked Attacks */}
<Card className="shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Blocked Attacks</CardTitle>
<div className="bg-destructive/10 text-destructive flex size-8 items-center justify-center rounded-md">
<ShieldAlert className="size-4" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">1,420 in 24h</div>
<div className="flex items-center gap-1.5 text-xs">
<Badge variant="outline" className="h-4.5 px-1.5 py-0 text-xs font-normal">
Tier-1 Edge
</Badge>
<span className="text-muted-foreground truncate tabular-nums">18 brute-force bursts</span>
</div>
</CardContent>
</Card>
{/* Card 3: Active Throttles */}
<Card className="shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Active Throttles</CardTitle>
<div className="bg-warning/10 text-warning flex size-8 items-center justify-center rounded-md">
<ZapOff className="size-4" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">18 IPs</div>
<div className="flex items-center gap-1.5 text-xs">
<span className="bg-warning inline-block size-1.5 rounded-full"></span>
<span className="text-muted-foreground truncate tabular-nums">4 edge clusters · Exp 15m</span>
</div>
</CardContent>
</Card>
{/* Card 4: Median Latency Overhead */}
<Card className="shadow-xs">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-muted-foreground text-xs font-medium">Median Latency Overhead</CardTitle>
<div className="bg-info/10 text-info flex size-8 items-center justify-center rounded-md">
<Timer className="size-4" />
</div>
</CardHeader>
<CardContent className="space-y-1">
<div className="text-foreground text-2xl font-bold tracking-tight tabular-nums">0.4ms</div>
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground tabular-nums">p99 < 1.2ms · Multi-Region Redis</span>
</div>
</CardContent>
</Card>
</div>
{/* Interactive Token Bucket Visualizer */}
<Card className="shadow-xs">
<CardHeader className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-lg font-semibold">Algorithm Simulation & Token Dynamics</CardTitle>
<Badge variant="outline" className="text-xs font-normal">
Interactive Sandbox
</Badge>
</div>
<CardDescription className="text-xs">
Test traffic volume against token reservoir dynamics and rolling time windows.
</CardDescription>
</div>
{/* Algorithm Selector */}
<div className="flex flex-wrap items-center gap-2">
<span className="text-muted-foreground text-xs font-medium">Algorithm:</span>
<Select value={algorithm} onValueChange={(val) => setAlgorithm(val as 'token-bucket' | 'sliding-window')}>
<SelectTrigger className="w-52 text-xs">
<SelectValue placeholder="Select algorithm" />
</SelectTrigger>
<SelectContent>
<SelectItem value="token-bucket">Token Bucket (Burst Tolerant)</SelectItem>
<SelectItem value="sliding-window">Sliding Window Counter</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Visual Algorithm Diagram / Pipeline */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
{/* Step 1 */}
<div className="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5">
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
{algorithm === 'token-bucket' ? '1. Refill Velocity' : '1. Time Window'}
</span>
<RefreshCw className="text-muted-foreground size-3.5" />
</div>
<div className="text-foreground text-lg font-semibold tabular-nums">
{algorithm === 'token-bucket' ? '50 tokens / s' : '60s Window'}
</div>
<p className="text-muted-foreground text-xs">
{algorithm === 'token-bucket'
? 'Deterministic continuous refill into pool'
: 'Rolling granular sub-window slices'}
</p>
</div>
<div className="mt-3">
<Progress value={100} className="h-1.5" />
</div>
</div>
{/* Step 2 */}
<div className="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5">
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
{algorithm === 'token-bucket' ? '2. Reservoir Capacity' : '2. Window Ceiling'}
</span>
<Database className="text-muted-foreground size-3.5" />
</div>
<div className="text-foreground text-lg font-semibold tabular-nums">
{algorithm === 'token-bucket' ? '100 tokens max' : '120 req / window'}
</div>
<p className="text-muted-foreground text-xs">
{algorithm === 'token-bucket'
? `Pool status: ${simStats.bucketFill}% available`
: `Window utilized: ${simStats.bucketFill}%`}
</p>
</div>
<div className="mt-3">
<Progress value={simStats.bucketFill} className="h-1.5" />
</div>
</div>
{/* Step 3 */}
<div className="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5">
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
3. Test Traffic Load
</span>
<Zap className="text-primary size-3.5" />
</div>
<div className="text-foreground text-lg font-semibold tabular-nums">{simVolume} req/s</div>
<p className="text-muted-foreground text-xs">Simulated rate generated from slider</p>
</div>
<div className="mt-3">
<Progress value={Math.min(100, Math.round((simVolume / 500) * 100))} className="h-1.5" />
</div>
</div>
{/* Step 4 */}
<div className="bg-muted/30 border-border relative flex flex-col justify-between rounded-lg border p-3.5">
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
4. Edge Gate Verdict
</span>
{simStats.status === 'optimal' ? (
<CheckCircle2 className="text-success size-3.5" />
) : simStats.status === 'burst' ? (
<AlertTriangle className="text-warning size-3.5" />
) : (
<ShieldAlert className="text-destructive size-3.5" />
)}
</div>
<div className="text-foreground text-lg font-semibold tabular-nums">
{simStats.allowed} Pass · {simStats.dropped} Drop
</div>
<p className="text-muted-foreground text-xs">Overhead: {simStats.latency}</p>
</div>
<div className="mt-3">
<div className="bg-secondary relative h-1.5 w-full overflow-hidden rounded-full">
<div
className="bg-success absolute top-0 bottom-0 left-0 transition-colors duration-150"
style={{ width: `${simStats.allowedPct}%` }}
/>
<div
className="bg-destructive absolute top-0 right-0 bottom-0 transition-colors duration-150"
style={{ width: `${simStats.droppedPct}%` }}
/>
</div>
</div>
</div>
</div>
{/* Interactive Simulator Slider Box */}
<div className="bg-muted/20 border-border space-y-4 rounded-lg border p-4">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-0.5">
<div className="flex flex-wrap items-center gap-2">
<Sliders className="text-primary size-4" />
<span className="text-foreground text-sm font-semibold">Simulate Incoming Request Volume</span>
</div>
<p className="text-muted-foreground text-xs">
Drag the slider to stress-test the algorithm against burst spikes and DDoS loads.
</p>
</div>
{/* Current Rate Pill */}
<div className="flex flex-wrap items-center gap-2">
<Badge
variant={
simStats.status === 'optimal' ? 'success' : simStats.status === 'burst' ? 'warning' : 'destructive'
}
className="text-xs font-medium"
>
{simStats.statusLabel}
</Badge>
<div className="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>
{/* Slider */}
<div className="space-y-2 pt-2">
<Slider
value={[simVolume]}
onValueChange={(val) => setSimVolume(val[0] ?? 120)}
min={10}
max={500}
step={10}
className="w-full"
/>
<div className="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>
{/* Simulation Visual Metrics & Split Bar */}
<div className="space-y-3 pt-2">
{/* Split Bar */}
<div className="space-y-1.5">
<div className="flex justify-between text-xs font-medium">
<span className="text-success flex items-center gap-1">
<span className="bg-success size-2 rounded-full"></span>
Allowed: {simStats.allowed} req/s ({simStats.allowedPct}%)
</span>
<span className="text-destructive flex items-center gap-1">
<span className="bg-destructive size-2 rounded-full"></span>
Rate Limited / 429: {simStats.dropped} req/s ({simStats.droppedPct}%)
</span>
</div>
<div className="bg-muted border-border/50 relative h-2.5 w-full overflow-hidden rounded-full border">
<div
className="bg-success absolute top-0 bottom-0 left-0 transition-colors duration-150"
style={{ width: `${simStats.allowedPct}%` }}
/>
<div
className="bg-destructive absolute top-0 right-0 bottom-0 transition-colors duration-150"
style={{ width: `${simStats.droppedPct}%` }}
/>
</div>
</div>
{/* Simulation Summary Description */}
<div className="bg-card border-border/80 flex items-start gap-2.5 rounded-md border p-3 text-xs">
<Shield className="text-primary mt-0.5 size-4 shrink-0" />
<p className="text-muted-foreground leading-relaxed">{simStats.description}</p>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Configured Rules Table Card */}
<Card className="shadow-xs">
<CardHeader className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-lg font-semibold">Configured Rate Limiting Rules</CardTitle>
<Badge variant="secondary" className="text-xs font-normal tabular-nums">
{filteredRules.length} Policies
</Badge>
</div>
<CardDescription className="text-xs">
Priority-ordered edge firewall rules evaluated sequentially for incoming traffic.
</CardDescription>
</div>
{/* Filter Search Bar */}
<div className="relative w-full sm:w-64">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2" />
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
type="text"
placeholder="Search rules, paths, actions…"
className="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>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[280px]">Rule Name & Scope</TableHead>
<TableHead className="w-[220px]">Path Matcher</TableHead>
<TableHead className="w-[180px]">Limit Threshold</TableHead>
<TableHead className="w-[200px]">Mitigation Action</TableHead>
<TableHead className="w-[90px] text-center">Status</TableHead>
<TableHead className="w-[60px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRules.map((rule) => (
<TableRow key={rule.id} className={cn(!rule.enabled && 'bg-muted/20 opacity-60')}>
{/* Rule Name & Scope */}
<TableCell>
<div className="space-y-0.5">
<div className="flex flex-wrap items-center gap-2">
<span className="text-foreground font-mono text-xs font-semibold">{rule.name}</span>
</div>
<div className="text-muted-foreground flex items-center gap-1.5 text-xs">
<span>{rule.scope}</span>
<span>·</span>
<span className="tabular-nums">{rule.blocked24h} in 24h</span>
</div>
</div>
</TableCell>
{/* Path Matcher */}
<TableCell>
<div className="flex items-center gap-1.5">
<Badge variant="outline" className="font-mono text-xs uppercase">
{rule.method}
</Badge>
<code className="bg-muted/60 border-border text-foreground rounded border px-1.5 py-0.5 font-mono text-xs">
{rule.path}
</code>
</div>
</TableCell>
{/* Limit Threshold */}
<TableCell>
<div className="space-y-0.5">
<div className="text-foreground font-mono text-xs font-medium tabular-nums">
{rule.limitThreshold}
</div>
<div className="text-muted-foreground text-xs">Triggered {rule.lastTriggered}</div>
</div>
</TableCell>
{/* Mitigation Action Badge */}
<TableCell>
<Badge variant={rule.actionVariant} className="text-xs font-medium">
<span
className={cn(
'mr-1 size-1.5 rounded-full',
rule.actionVariant === 'warning'
? 'bg-warning'
: rule.actionVariant === 'destructive'
? 'bg-destructive'
: 'bg-secondary-foreground',
)}
/>
{rule.mitigationAction}
</Badge>
</TableCell>
{/* Status Switch */}
<TableCell className="text-center">
<Switch checked={rule.enabled} onCheckedChange={() => toggleRule(rule.id)} />
</TableCell>
{/* Actions Dropdown */}
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="size-8">
<MoreHorizontal className="size-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel className="text-xs">Rule Configuration</DropdownMenuLabel>
<DropdownMenuItem className="gap-2 text-xs">
<Edit3 className="size-3.5" />
<span>Edit Rule</span>
</DropdownMenuItem>
<DropdownMenuItem className="gap-2 text-xs">
<Activity className="size-3.5" />
<span>View Metrics</span>
</DropdownMenuItem>
<DropdownMenuItem className="gap-2 text-xs">
<Copy className="size-3.5" />
<span>Duplicate Rule</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive focus:text-destructive gap-2 text-xs">
<Trash2 className="size-3.5" />
<span>Delete Rule</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
)
}
Raw manifest:https://uipkge.dev/r/react/rate-limiting-config.json