768 lines
25 KiB
Vue

<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-speed"></Icon>
<h1 class="text-xl font-bold text-primary">Rate Limiting</h1>
</div>
</template>
<template #body>
<p class="text-gray-600">
Throttles how many messages/jobs can be processed per second/minute/hour.
Avoid hitting API limits (Twilio, SendGrid) and prevent spammy behavior that can trigger blacklisting.
</p>
</template>
</rs-card>
<!-- Rate Limit Statistics -->
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-4 gap-6 mb-6">
<rs-card
v-for="(stat, index) in rateLimitStats"
:key="index"
class="transition-all duration-300 hover:shadow-lg"
>
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
<div
class="p-4 flex justify-center items-center rounded-2xl"
:class="stat.bgColor"
>
<Icon class="text-2xl" :class="stat.iconColor" :name="stat.icon"></Icon>
</div>
<div class="flex-1 truncate">
<span class="block font-bold text-xl leading-tight" :class="stat.textColor">
{{ stat.value }}
</span>
<span class="text-sm font-medium text-gray-600">
{{ stat.title }}
</span>
</div>
</div>
</rs-card>
</div>
<!-- Current Usage Overview -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Current Usage Overview</h3>
<div class="flex items-center gap-2">
<div class="flex items-center">
<div class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div>
<span class="text-sm text-gray-600">Live Updates</span>
</div>
<rs-button variant="outline" size="sm" @click="refreshUsage">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div
v-for="(usage, index) in currentUsage"
:key="index"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center">
<Icon class="mr-2 text-primary" :name="usage.icon"></Icon>
<h4 class="font-semibold">{{ usage.service }}</h4>
</div>
<rs-badge :variant="getUsageVariant(usage.percentage)">
{{ usage.percentage }}%
</rs-badge>
</div>
<div class="space-y-2">
<div class="flex justify-between text-sm">
<span class="text-gray-600">Current Rate:</span>
<span class="font-medium">{{ usage.currentRate }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Limit:</span>
<span class="font-medium">{{ usage.limit }}</span>
</div>
<div class="flex justify-between text-sm">
<span class="text-gray-600">Window:</span>
<span class="font-medium">{{ usage.window }}</span>
</div>
</div>
<div class="mt-3">
<rs-progress-bar
:value="usage.percentage"
:variant="usage.percentage > 80 ? 'danger' : usage.percentage > 60 ? 'warning' : 'success'"
/>
</div>
<div class="mt-2 text-xs text-gray-500">
Resets in {{ usage.resetTime }}
</div>
</div>
</div>
</template>
</rs-card>
<!-- Rate Limit Configuration -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Rate Limit Configuration</h3>
<rs-button variant="outline" size="sm" @click="showConfigModal = true">
<Icon class="mr-1" name="ic:outline-settings"></Icon>
Configure Limits
</rs-button>
</div>
</template>
<template #body>
<rs-table
:field="configTableFields"
:data="rateLimitConfigs"
:options="{ striped: true, hover: true }"
:optionsAdvanced="{ sortable: true, filterable: false }"
advanced
/>
</template>
</rs-card>
<!-- Rate Limit Violations -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Recent Rate Limit Violations</h3>
<div class="flex items-center gap-2">
<select v-model="violationFilter" class="p-2 border border-gray-300 rounded-md text-sm">
<option value="">All Services</option>
<option value="email">Email</option>
<option value="sms">SMS</option>
<option value="push">Push</option>
<option value="webhook">Webhook</option>
</select>
<rs-button variant="outline" size="sm" @click="refreshViolations">
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</div>
</template>
<template #body>
<div v-if="filteredViolations.length === 0" class="text-center py-8 text-gray-500">
<Icon class="text-4xl mb-2" name="ic:outline-check-circle"></Icon>
<p>No rate limit violations in the selected timeframe</p>
</div>
<div v-else class="space-y-3">
<div
v-for="(violation, index) in filteredViolations"
:key="index"
class="border border-red-200 bg-red-50 rounded-lg p-4"
>
<div class="flex items-center justify-between mb-2">
<div class="flex items-center">
<Icon class="mr-2 text-red-600" name="ic:outline-warning"></Icon>
<span class="font-semibold text-red-800">{{ violation.service }} Rate Limit Exceeded</span>
</div>
<span class="text-sm text-red-600">{{ violation.timestamp }}</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span class="text-red-700">Attempted Rate:</span>
<span class="ml-1 font-medium">{{ violation.attemptedRate }}</span>
</div>
<div>
<span class="text-red-700">Limit:</span>
<span class="ml-1 font-medium">{{ violation.limit }}</span>
</div>
<div>
<span class="text-red-700">Messages Dropped:</span>
<span class="ml-1 font-medium">{{ violation.droppedMessages }}</span>
</div>
</div>
<p class="text-sm text-red-700 mt-2">{{ violation.description }}</p>
</div>
</div>
</template>
</rs-card>
<!-- Rate Limit Analytics -->
<rs-card class="mb-6">
<template #header>
<h3 class="text-lg font-semibold text-primary">Rate Limit Analytics</h3>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Usage Trends -->
<div>
<h4 class="font-semibold mb-4">Usage Trends (Last 24 Hours)</h4>
<div class="space-y-3">
<div
v-for="(trend, index) in usageTrends"
:key="index"
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
>
<div class="flex items-center">
<Icon class="mr-2 text-primary" :name="trend.icon"></Icon>
<div>
<p class="font-medium">{{ trend.service }}</p>
<p class="text-sm text-gray-600">Peak: {{ trend.peak }}</p>
</div>
</div>
<div class="text-right">
<p class="font-medium">{{ trend.average }}</p>
<p class="text-sm text-gray-600">Average</p>
</div>
</div>
</div>
</div>
<!-- Efficiency Metrics -->
<div>
<h4 class="font-semibold mb-4">Efficiency Metrics</h4>
<div class="space-y-4">
<div class="bg-green-50 border border-green-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-green-600" name="ic:outline-trending-up"></Icon>
<span class="font-medium text-green-800">Throughput Optimization</span>
</div>
<p class="text-sm text-green-700">
Current efficiency: {{ efficiencyMetrics.throughputOptimization }}%
</p>
</div>
<div class="bg-blue-50 border border-blue-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-blue-600" name="ic:outline-schedule"></Icon>
<span class="font-medium text-blue-800">Queue Utilization</span>
</div>
<p class="text-sm text-blue-700">
Average queue utilization: {{ efficiencyMetrics.queueUtilization }}%
</p>
</div>
<div class="bg-purple-50 border border-purple-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-purple-600" name="ic:outline-timer"></Icon>
<span class="font-medium text-purple-800">Response Time</span>
</div>
<p class="text-sm text-purple-700">
Average response time: {{ efficiencyMetrics.responseTime }}ms
</p>
</div>
<div class="bg-orange-50 border border-orange-200 rounded p-3">
<div class="flex items-center mb-2">
<Icon class="mr-2 text-orange-600" name="ic:outline-error"></Icon>
<span class="font-medium text-orange-800">Error Rate</span>
</div>
<p class="text-sm text-orange-700">
Rate limit errors: {{ efficiencyMetrics.errorRate }}%
</p>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Rate Limit Testing -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary">Rate Limit Testing</h3>
</template>
<template #body>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Service to Test</label>
<select v-model="testConfig.service" class="w-full p-2 border border-gray-300 rounded-md">
<option value="">Select service</option>
<option value="email">Email (SendGrid)</option>
<option value="sms">SMS (Twilio)</option>
<option value="push">Push (Firebase)</option>
<option value="webhook">Webhook</option>
</select>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Second</label>
<input
v-model.number="testConfig.rate"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="10"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Duration (seconds)</label>
<input
v-model.number="testConfig.duration"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
placeholder="60"
min="1"
max="300"
/>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Test Message</label>
<textarea
v-model="testConfig.message"
class="w-full p-2 border border-gray-300 rounded-md"
rows="3"
placeholder="Test message content"
></textarea>
</div>
<rs-button
@click="startRateLimitTest"
variant="primary"
class="w-full"
:disabled="testRunning"
>
<Icon class="mr-1" :name="testRunning ? 'ic:outline-stop' : 'ic:outline-play-arrow'"></Icon>
{{ testRunning ? 'Test Running...' : 'Start Rate Limit Test' }}
</rs-button>
</div>
<div class="space-y-4">
<h4 class="font-semibold">Test Results</h4>
<div v-if="testResults.length === 0" class="text-center text-gray-500 py-8">
<Icon class="text-4xl mb-2" name="ic:outline-science"></Icon>
<p>Run a test to see results</p>
</div>
<div v-else class="space-y-3 max-h-60 overflow-y-auto">
<div
v-for="(result, index) in testResults"
:key="index"
class="border border-gray-200 rounded p-3"
>
<div class="flex justify-between items-start mb-2">
<span class="font-medium">{{ result.service }}</span>
<span :class="{
'text-green-600': result.status === 'success',
'text-red-600': result.status === 'rate_limited',
'text-yellow-600': result.status === 'warning'
}" class="text-sm font-medium">{{ result.status }}</span>
</div>
<div class="text-sm space-y-1">
<div class="flex justify-between">
<span class="text-gray-600">Messages Sent:</span>
<span>{{ result.messagesSent }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Rate Achieved:</span>
<span>{{ result.rateAchieved }}/sec</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Errors:</span>
<span>{{ result.errors }}</span>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">{{ result.timestamp }}</p>
</div>
</div>
</div>
</div>
</template>
</rs-card>
<!-- Configuration Modal -->
<rs-modal v-model="showConfigModal" size="lg">
<template #header>
<h3 class="text-lg font-semibold">Rate Limit Configuration</h3>
</template>
<template #body>
<div class="space-y-6">
<div
v-for="(config, index) in editableConfigs"
:key="index"
class="border border-gray-200 rounded-lg p-4"
>
<div class="flex items-center mb-4">
<Icon class="mr-2 text-primary" :name="config.icon"></Icon>
<h4 class="font-semibold">{{ config.service }} Configuration</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Second</label>
<input
v-model.number="config.perSecond"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Minute</label>
<input
v-model.number="config.perMinute"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Messages per Hour</label>
<input
v-model.number="config.perHour"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Burst Limit</label>
<input
v-model.number="config.burstLimit"
type="number"
class="w-full p-2 border border-gray-300 rounded-md"
min="1"
/>
</div>
</div>
<div class="mt-4">
<label class="flex items-center">
<input
v-model="config.enabled"
type="checkbox"
class="mr-2"
/>
<span class="text-sm font-medium text-gray-700">Enable rate limiting for this service</span>
</label>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex justify-end gap-2">
<rs-button variant="outline" @click="showConfigModal = false">Cancel</rs-button>
<rs-button @click="saveRateLimitConfig" variant="primary">Save Configuration</rs-button>
</div>
</template>
</rs-modal>
</div>
</template>
<script setup>
definePageMeta({
title: "Rate Limiting",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue & Scheduler",
path: "/notification/queue-scheduler",
},
{
name: "Rate Limiting",
path: "/notification/queue-scheduler/rate-limit",
},
],
});
// Reactive data
const showConfigModal = ref(false);
const violationFilter = ref('');
const testRunning = ref(false);
const testResults = ref([]);
// Statistics
const rateLimitStats = ref([
{
title: "Active Limits",
value: "12",
icon: "ic:outline-speed",
bgColor: "bg-blue-100",
iconColor: "text-blue-600",
textColor: "text-blue-600"
},
{
title: "Messages/Hour",
value: "45.2K",
icon: "ic:outline-trending-up",
bgColor: "bg-green-100",
iconColor: "text-green-600",
textColor: "text-green-600"
},
{
title: "Violations Today",
value: "3",
icon: "ic:outline-warning",
bgColor: "bg-red-100",
iconColor: "text-red-600",
textColor: "text-red-600"
},
{
title: "Efficiency",
value: "96.8%",
icon: "ic:outline-check-circle",
bgColor: "bg-purple-100",
iconColor: "text-purple-600",
textColor: "text-purple-600"
}
]);
// Current usage
const currentUsage = ref([
{
service: 'Email (SendGrid)',
icon: 'ic:outline-email',
currentRate: '850/hour',
limit: '1000/hour',
percentage: 85,
window: '1 hour',
resetTime: '23 minutes'
},
{
service: 'SMS (Twilio)',
icon: 'ic:outline-sms',
currentRate: '45/minute',
limit: '100/minute',
percentage: 45,
window: '1 minute',
resetTime: '32 seconds'
},
{
service: 'Push (Firebase)',
icon: 'ic:outline-notifications',
currentRate: '1200/hour',
limit: '5000/hour',
percentage: 24,
window: '1 hour',
resetTime: '45 minutes'
},
{
service: 'Webhook',
icon: 'ic:outline-webhook',
currentRate: '15/second',
limit: '20/second',
percentage: 75,
window: '1 second',
resetTime: '0.5 seconds'
}
]);
// Configuration table fields
const configTableFields = ref([
{ key: 'service', label: 'Service', sortable: true },
{ key: 'perSecond', label: 'Per Second', sortable: true },
{ key: 'perMinute', label: 'Per Minute', sortable: true },
{ key: 'perHour', label: 'Per Hour', sortable: true },
{ key: 'burstLimit', label: 'Burst Limit', sortable: true },
{ key: 'status', label: 'Status', sortable: true },
{ key: 'actions', label: 'Actions', sortable: false }
]);
// Rate limit configurations
const rateLimitConfigs = ref([
{
service: 'Email (SendGrid)',
icon: 'ic:outline-email',
perSecond: 10,
perMinute: 600,
perHour: 1000,
burstLimit: 50,
enabled: true
},
{
service: 'SMS (Twilio)',
icon: 'ic:outline-sms',
perSecond: 5,
perMinute: 100,
perHour: 2000,
burstLimit: 20,
enabled: true
},
{
service: 'Push (Firebase)',
icon: 'ic:outline-notifications',
perSecond: 50,
perMinute: 1000,
perHour: 5000,
burstLimit: 200,
enabled: true
},
{
service: 'Webhook',
icon: 'ic:outline-webhook',
perSecond: 20,
perMinute: 500,
perHour: 10000,
burstLimit: 100,
enabled: true
}
].map(config => ({
...config,
status: h('span', {
class: `px-2 py-1 rounded text-xs font-medium ${
config.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
}`
}, config.enabled ? 'Active' : 'Disabled'),
actions: h('button', {
class: 'text-blue-600 hover:text-blue-800 text-sm',
onClick: () => editRateLimit(config)
}, 'Edit')
})));
// Editable configs for modal
const editableConfigs = ref(JSON.parse(JSON.stringify(rateLimitConfigs.value.map(c => ({
service: c.service,
icon: c.icon,
perSecond: c.perSecond,
perMinute: c.perMinute,
perHour: c.perHour,
burstLimit: c.burstLimit,
enabled: c.enabled
})))));
// Rate limit violations
const violations = ref([
{
service: 'Email',
timestamp: '2024-01-15 14:30:00',
attemptedRate: '1200/hour',
limit: '1000/hour',
droppedMessages: 45,
description: 'Newsletter campaign exceeded hourly limit during peak hours'
},
{
service: 'SMS',
timestamp: '2024-01-15 12:15:00',
attemptedRate: '150/minute',
limit: '100/minute',
droppedMessages: 23,
description: 'OTP verification burst exceeded per-minute limit'
},
{
service: 'Webhook',
timestamp: '2024-01-15 10:45:00',
attemptedRate: '25/second',
limit: '20/second',
droppedMessages: 12,
description: 'Order webhook notifications exceeded per-second limit'
}
]);
// Usage trends
const usageTrends = ref([
{
service: 'Email',
icon: 'ic:outline-email',
peak: '950/hour',
average: '650/hour'
},
{
service: 'SMS',
icon: 'ic:outline-sms',
peak: '85/minute',
average: '45/minute'
},
{
service: 'Push',
icon: 'ic:outline-notifications',
peak: '2100/hour',
average: '1200/hour'
},
{
service: 'Webhook',
icon: 'ic:outline-webhook',
peak: '18/second',
average: '12/second'
}
]);
// Efficiency metrics
const efficiencyMetrics = ref({
throughputOptimization: 96.8,
queueUtilization: 78.5,
responseTime: 245,
errorRate: 0.3
});
// Test configuration
const testConfig = ref({
service: '',
rate: 10,
duration: 60,
message: 'This is a rate limit test message'
});
// Computed filtered violations
const filteredViolations = computed(() => {
if (!violationFilter.value) {
return violations.value;
}
return violations.value.filter(v => v.service.toLowerCase() === violationFilter.value);
});
// Methods
function getUsageVariant(percentage) {
if (percentage > 80) return 'danger';
if (percentage > 60) return 'warning';
return 'success';
}
function refreshUsage() {
// Mock refresh
console.log('Refreshing usage data...');
}
function refreshViolations() {
// Mock refresh
console.log('Refreshing violations...');
}
function editRateLimit(config) {
// Find and update the editable config
const editableConfig = editableConfigs.value.find(c => c.service === config.service);
if (editableConfig) {
Object.assign(editableConfig, config);
}
showConfigModal.value = true;
}
function saveRateLimitConfig() {
// Mock save
console.log('Saving rate limit configuration...', editableConfigs.value);
showConfigModal.value = false;
}
function startRateLimitTest() {
if (!testConfig.value.service) {
return;
}
testRunning.value = true;
// Mock test execution
setTimeout(() => {
const result = {
service: testConfig.value.service,
messagesSent: Math.floor(testConfig.value.rate * testConfig.value.duration * 0.9),
rateAchieved: Math.floor(testConfig.value.rate * 0.9),
errors: Math.floor(Math.random() * 5),
status: Math.random() > 0.7 ? 'rate_limited' : 'success',
timestamp: new Date().toLocaleString()
};
testResults.value.unshift(result);
testRunning.value = false;
}, 3000);
}
</script>
<style lang="scss" scoped></style>