Implement batch processing and queue management features, including new pages for batch scheduling, queue monitoring, rate limiting, and retry logic. Update existing components for improved camel case conversion and remove unused navigation items.
This commit is contained in:
parent
b181f99237
commit
ab9a8ae8c4
@ -116,11 +116,14 @@ const spacingCharactertoCamelCase = (array) => {
|
|||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
array.forEach((element) => {
|
array.forEach((element) => {
|
||||||
if (element.charAt(0) == element.charAt(0).toUpperCase()) {
|
// Handle both string elements and object elements with key property
|
||||||
|
const stringElement = typeof element === 'string' ? element : element.key;
|
||||||
|
|
||||||
|
if (stringElement && stringElement.charAt(0) == stringElement.charAt(0).toUpperCase()) {
|
||||||
// Camelcase the string and remove spacing
|
// Camelcase the string and remove spacing
|
||||||
// and if there is () in the string, do Uppercase inside the () and dont spacing it
|
// and if there is () in the string, do Uppercase inside the () and dont spacing it
|
||||||
|
|
||||||
let camelCase = element
|
let camelCase = stringElement
|
||||||
.replace(/([A-Z])/g, " $1")
|
.replace(/([A-Z])/g, " $1")
|
||||||
.replace(/^./, (str) => {
|
.replace(/^./, (str) => {
|
||||||
return str.toUpperCase();
|
return str.toUpperCase();
|
||||||
@ -133,7 +136,7 @@ const spacingCharactertoCamelCase = (array) => {
|
|||||||
|
|
||||||
result.push(resultCamelCase);
|
result.push(resultCamelCase);
|
||||||
} else {
|
} else {
|
||||||
result.push(element);
|
result.push(stringElement);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -10,18 +10,6 @@ export default [
|
|||||||
child: [],
|
child: [],
|
||||||
meta: {},
|
meta: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Notes",
|
|
||||||
path: "/notes",
|
|
||||||
icon: "",
|
|
||||||
child: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Metabase",
|
|
||||||
path: "/metabase",
|
|
||||||
icon: "",
|
|
||||||
child: [],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
meta: {},
|
meta: {},
|
||||||
},
|
},
|
||||||
|
706
pages/notification/queue-scheduler/batch.vue
Normal file
706
pages/notification/queue-scheduler/batch.vue
Normal file
@ -0,0 +1,706 @@
|
|||||||
|
<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-batch-prediction"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Batch Processing</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Schedule or trigger the processing of large groups of messages in one go.
|
||||||
|
Ideal for sending newsletters, campaigns, or system-wide alerts to thousands/millions of users.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||||
|
<rs-card class="cursor-pointer transition-all duration-300 hover:shadow-lg" @click="showCreateBatchModal = true">
|
||||||
|
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
||||||
|
<div class="p-4 flex justify-center items-center bg-blue-100 rounded-2xl">
|
||||||
|
<Icon class="text-blue-600 text-2xl" name="ic:outline-add"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="block font-bold text-lg text-blue-600">Create New Batch</span>
|
||||||
|
<span class="text-sm text-gray-600">Start a new batch processing job</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<rs-card class="cursor-pointer transition-all duration-300 hover:shadow-lg" @click="showScheduleModal = true">
|
||||||
|
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
||||||
|
<div class="p-4 flex justify-center items-center bg-green-100 rounded-2xl">
|
||||||
|
<Icon class="text-green-600 text-2xl" name="ic:outline-schedule"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="block font-bold text-lg text-green-600">Schedule Batch</span>
|
||||||
|
<span class="text-sm text-gray-600">Schedule for later execution</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<rs-card class="cursor-pointer transition-all duration-300 hover:shadow-lg" @click="showTemplatesModal = true">
|
||||||
|
<div class="pt-5 pb-3 px-5 flex items-center gap-4">
|
||||||
|
<div class="p-4 flex justify-center items-center bg-purple-100 rounded-2xl">
|
||||||
|
<Icon class="text-purple-600 text-2xl" name="ic:outline-library-books"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="block font-bold text-lg text-purple-600">Batch Templates</span>
|
||||||
|
<span class="text-sm text-gray-600">Use predefined batch configurations</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch 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 batchStats"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<!-- Active Batches -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Active Batches</h3>
|
||||||
|
<rs-button variant="outline" size="sm" @click="refreshBatches">
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="(batch, index) in activeBatches"
|
||||||
|
: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">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full mr-3"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-500': batch.status === 'pending',
|
||||||
|
'bg-yellow-500': batch.status === 'processing',
|
||||||
|
'bg-green-500': batch.status === 'completed',
|
||||||
|
'bg-red-500': batch.status === 'failed',
|
||||||
|
'bg-gray-500': batch.status === 'paused'
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold">{{ batch.name }}</h4>
|
||||||
|
<p class="text-sm text-gray-600">{{ batch.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<rs-badge :variant="getBatchStatusVariant(batch.status)">
|
||||||
|
{{ batch.status }}
|
||||||
|
</rs-badge>
|
||||||
|
<rs-dropdown>
|
||||||
|
<template #trigger>
|
||||||
|
<rs-button variant="outline" size="sm">
|
||||||
|
<Icon name="ic:outline-more-vert"></Icon>
|
||||||
|
</rs-button>
|
||||||
|
</template>
|
||||||
|
<rs-dropdown-item @click="viewBatchDetails(batch)">
|
||||||
|
<Icon class="mr-2" name="ic:outline-visibility"></Icon>
|
||||||
|
View Details
|
||||||
|
</rs-dropdown-item>
|
||||||
|
<rs-dropdown-item v-if="batch.status === 'processing'" @click="pauseBatch(batch)">
|
||||||
|
<Icon class="mr-2" name="ic:outline-pause"></Icon>
|
||||||
|
Pause
|
||||||
|
</rs-dropdown-item>
|
||||||
|
<rs-dropdown-item v-if="batch.status === 'paused'" @click="resumeBatch(batch)">
|
||||||
|
<Icon class="mr-2" name="ic:outline-play-arrow"></Icon>
|
||||||
|
Resume
|
||||||
|
</rs-dropdown-item>
|
||||||
|
<rs-dropdown-item v-if="['pending', 'paused'].includes(batch.status)" @click="cancelBatch(batch)" class="text-red-600">
|
||||||
|
<Icon class="mr-2" name="ic:outline-cancel"></Icon>
|
||||||
|
Cancel
|
||||||
|
</rs-dropdown-item>
|
||||||
|
</rs-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
||||||
|
<span>Progress: {{ batch.processed }}/{{ batch.total }}</span>
|
||||||
|
<span>{{ Math.round((batch.processed / batch.total) * 100) }}%</span>
|
||||||
|
</div>
|
||||||
|
<rs-progress-bar
|
||||||
|
:value="(batch.processed / batch.total) * 100"
|
||||||
|
:variant="batch.status === 'failed' ? 'danger' : 'primary'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Batch Info -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Type:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ batch.type }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Chunk Size:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ batch.chunkSize }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">Started:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ batch.startedAt }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600">ETA:</span>
|
||||||
|
<span class="ml-1 font-medium">{{ batch.eta }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Batch History -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Batch History</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<rs-table
|
||||||
|
:field="historyTableFields"
|
||||||
|
:data="batchHistory"
|
||||||
|
:options="{ striped: true, hover: true }"
|
||||||
|
:optionsAdvanced="{ sortable: true, filterable: true }"
|
||||||
|
advanced
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Create Batch Modal -->
|
||||||
|
<rs-modal v-model="showCreateBatchModal" size="lg">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Create New Batch</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<form @submit.prevent="createBatch" class="space-y-4">
|
||||||
|
<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">Batch Name</label>
|
||||||
|
<input
|
||||||
|
v-model="newBatch.name"
|
||||||
|
type="text"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
placeholder="Enter batch name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Message Type</label>
|
||||||
|
<select v-model="newBatch.type" class="w-full p-2 border border-gray-300 rounded-md" required>
|
||||||
|
<option value="">Select type</option>
|
||||||
|
<option value="email">Email</option>
|
||||||
|
<option value="sms">SMS</option>
|
||||||
|
<option value="push">Push Notification</option>
|
||||||
|
<option value="webhook">Webhook</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
||||||
|
<textarea
|
||||||
|
v-model="newBatch.description"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Describe this batch processing job"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Chunk Size</label>
|
||||||
|
<input
|
||||||
|
v-model.number="newBatch.chunkSize"
|
||||||
|
type="number"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
placeholder="500"
|
||||||
|
min="1"
|
||||||
|
max="10000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Priority</label>
|
||||||
|
<select v-model="newBatch.priority" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Delay Between Chunks (ms)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="newBatch.delay"
|
||||||
|
type="number"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
placeholder="1000"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Target Criteria</label>
|
||||||
|
<textarea
|
||||||
|
v-model="newBatch.criteria"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
rows="3"
|
||||||
|
placeholder="JSON criteria for selecting recipients (e.g., user type, timezone, preferences)"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<rs-button variant="outline" @click="showCreateBatchModal = false">Cancel</rs-button>
|
||||||
|
<rs-button @click="createBatch" variant="primary">Create Batch</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
|
||||||
|
<!-- Schedule Modal -->
|
||||||
|
<rs-modal v-model="showScheduleModal" size="md">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Schedule Batch</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Select Batch Template</label>
|
||||||
|
<select v-model="scheduleBatch.templateId" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="">Choose a template</option>
|
||||||
|
<option value="newsletter">Newsletter Campaign</option>
|
||||||
|
<option value="birthday">Birthday Reminders</option>
|
||||||
|
<option value="system">System Notifications</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">Schedule Date</label>
|
||||||
|
<input
|
||||||
|
v-model="scheduleBatch.date"
|
||||||
|
type="date"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Schedule Time</label>
|
||||||
|
<input
|
||||||
|
v-model="scheduleBatch.time"
|
||||||
|
type="time"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Timezone</label>
|
||||||
|
<select v-model="scheduleBatch.timezone" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
|
||||||
|
<option value="America/New_York">America/New_York</option>
|
||||||
|
<option value="Europe/London">Europe/London</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
<input
|
||||||
|
v-model="scheduleBatch.recurring"
|
||||||
|
type="checkbox"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
<label class="text-sm font-medium text-gray-700">Recurring batch</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="scheduleBatch.recurring">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Recurrence Pattern</label>
|
||||||
|
<select v-model="scheduleBatch.recurrencePattern" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<rs-button variant="outline" @click="showScheduleModal = false">Cancel</rs-button>
|
||||||
|
<rs-button @click="scheduleNewBatch" variant="primary">Schedule</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
|
||||||
|
<!-- Templates Modal -->
|
||||||
|
<rs-modal v-model="showTemplatesModal" size="lg">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Batch Templates</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(template, index) in batchTemplates"
|
||||||
|
:key="index"
|
||||||
|
class="border border-gray-200 rounded-lg p-4 cursor-pointer hover:border-primary transition-colors"
|
||||||
|
@click="useBatchTemplate(template)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-3">
|
||||||
|
<Icon class="mr-2 text-primary" :name="template.icon"></Icon>
|
||||||
|
<h4 class="font-semibold">{{ template.name }}</h4>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-600 mb-3">{{ template.description }}</p>
|
||||||
|
<div class="space-y-1 text-xs text-gray-500">
|
||||||
|
<div>Type: {{ template.type }}</div>
|
||||||
|
<div>Chunk Size: {{ template.chunkSize }}</div>
|
||||||
|
<div>Priority: {{ template.priority }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<rs-button variant="outline" @click="showTemplatesModal = false">Close</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Batch Processing",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue & Scheduler",
|
||||||
|
path: "/notification/queue-scheduler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Batch Processing",
|
||||||
|
path: "/notification/queue-scheduler/batch",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const showCreateBatchModal = ref(false);
|
||||||
|
const showScheduleModal = ref(false);
|
||||||
|
const showTemplatesModal = ref(false);
|
||||||
|
|
||||||
|
// New batch form
|
||||||
|
const newBatch = ref({
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
description: '',
|
||||||
|
chunkSize: 500,
|
||||||
|
priority: 'medium',
|
||||||
|
delay: 1000,
|
||||||
|
criteria: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schedule batch form
|
||||||
|
const scheduleBatch = ref({
|
||||||
|
templateId: '',
|
||||||
|
date: '',
|
||||||
|
time: '',
|
||||||
|
timezone: 'UTC',
|
||||||
|
recurring: false,
|
||||||
|
recurrencePattern: 'daily'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Batch statistics
|
||||||
|
const batchStats = ref([
|
||||||
|
{
|
||||||
|
title: "Active Batches",
|
||||||
|
value: "12",
|
||||||
|
icon: "ic:outline-play-circle",
|
||||||
|
bgColor: "bg-blue-100",
|
||||||
|
iconColor: "text-blue-600",
|
||||||
|
textColor: "text-blue-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Scheduled",
|
||||||
|
value: "8",
|
||||||
|
icon: "ic:outline-schedule",
|
||||||
|
bgColor: "bg-green-100",
|
||||||
|
iconColor: "text-green-600",
|
||||||
|
textColor: "text-green-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Completed Today",
|
||||||
|
value: "45",
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
bgColor: "bg-purple-100",
|
||||||
|
iconColor: "text-purple-600",
|
||||||
|
textColor: "text-purple-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Total Messages",
|
||||||
|
value: "2.3M",
|
||||||
|
icon: "ic:outline-email",
|
||||||
|
bgColor: "bg-orange-100",
|
||||||
|
iconColor: "text-orange-600",
|
||||||
|
textColor: "text-orange-600"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Active batches
|
||||||
|
const activeBatches = ref([
|
||||||
|
{
|
||||||
|
id: 'batch_001',
|
||||||
|
name: 'Newsletter Campaign Q1',
|
||||||
|
description: 'Quarterly newsletter to all subscribers',
|
||||||
|
status: 'processing',
|
||||||
|
type: 'email',
|
||||||
|
processed: 15000,
|
||||||
|
total: 50000,
|
||||||
|
chunkSize: 500,
|
||||||
|
startedAt: '2024-01-15 09:00:00',
|
||||||
|
eta: '2 hours'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'batch_002',
|
||||||
|
name: 'Birthday Reminders',
|
||||||
|
description: 'Daily birthday notifications',
|
||||||
|
status: 'completed',
|
||||||
|
type: 'push',
|
||||||
|
processed: 1200,
|
||||||
|
total: 1200,
|
||||||
|
chunkSize: 100,
|
||||||
|
startedAt: '2024-01-15 08:00:00',
|
||||||
|
eta: 'Completed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'batch_003',
|
||||||
|
name: 'SMS OTP Batch',
|
||||||
|
description: 'OTP messages for verification',
|
||||||
|
status: 'paused',
|
||||||
|
type: 'sms',
|
||||||
|
processed: 800,
|
||||||
|
total: 2000,
|
||||||
|
chunkSize: 200,
|
||||||
|
startedAt: '2024-01-15 10:30:00',
|
||||||
|
eta: 'Paused'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Batch history table fields
|
||||||
|
const historyTableFields = ref([
|
||||||
|
{ key: 'name', label: 'Batch Name', sortable: true },
|
||||||
|
{ key: 'type', label: 'Type', sortable: true },
|
||||||
|
{ key: 'status', label: 'Status', sortable: true },
|
||||||
|
{ key: 'total', label: 'Total Messages', sortable: true },
|
||||||
|
{ key: 'processed', label: 'Processed', sortable: true },
|
||||||
|
{ key: 'startedAt', label: 'Started', sortable: true },
|
||||||
|
{ key: 'completedAt', label: 'Completed', sortable: true },
|
||||||
|
{ key: 'duration', label: 'Duration', sortable: true }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Batch history data
|
||||||
|
const batchHistory = ref([
|
||||||
|
{
|
||||||
|
name: 'Welcome Email Series',
|
||||||
|
type: 'email',
|
||||||
|
status: 'completed',
|
||||||
|
total: 25000,
|
||||||
|
processed: 25000,
|
||||||
|
startedAt: '2024-01-14 14:00:00',
|
||||||
|
completedAt: '2024-01-14 16:30:00',
|
||||||
|
duration: '2h 30m'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Product Update Push',
|
||||||
|
type: 'push',
|
||||||
|
status: 'completed',
|
||||||
|
total: 100000,
|
||||||
|
processed: 98500,
|
||||||
|
startedAt: '2024-01-13 10:00:00',
|
||||||
|
completedAt: '2024-01-13 12:45:00',
|
||||||
|
duration: '2h 45m'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Security Alert SMS',
|
||||||
|
type: 'sms',
|
||||||
|
status: 'failed',
|
||||||
|
total: 5000,
|
||||||
|
processed: 2300,
|
||||||
|
startedAt: '2024-01-12 16:00:00',
|
||||||
|
completedAt: '2024-01-12 16:45:00',
|
||||||
|
duration: '45m'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Batch templates
|
||||||
|
const batchTemplates = ref([
|
||||||
|
{
|
||||||
|
name: 'Newsletter Campaign',
|
||||||
|
description: 'Standard newsletter template for marketing campaigns',
|
||||||
|
type: 'email',
|
||||||
|
chunkSize: 1000,
|
||||||
|
priority: 'medium',
|
||||||
|
icon: 'ic:outline-email'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Birthday Reminders',
|
||||||
|
description: 'Daily birthday notification template',
|
||||||
|
type: 'push',
|
||||||
|
chunkSize: 100,
|
||||||
|
priority: 'low',
|
||||||
|
icon: 'ic:outline-cake'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'System Notifications',
|
||||||
|
description: 'Critical system alerts and updates',
|
||||||
|
type: 'push',
|
||||||
|
chunkSize: 500,
|
||||||
|
priority: 'high',
|
||||||
|
icon: 'ic:outline-notification-important'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'SMS Verification',
|
||||||
|
description: 'OTP and verification SMS template',
|
||||||
|
type: 'sms',
|
||||||
|
chunkSize: 200,
|
||||||
|
priority: 'high',
|
||||||
|
icon: 'ic:outline-sms'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function getBatchStatusVariant(status) {
|
||||||
|
const variants = {
|
||||||
|
pending: 'info',
|
||||||
|
processing: 'warning',
|
||||||
|
completed: 'success',
|
||||||
|
failed: 'danger',
|
||||||
|
paused: 'secondary'
|
||||||
|
};
|
||||||
|
return variants[status] || 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewBatchDetails(batch) {
|
||||||
|
// Navigate to batch details page or show detailed modal
|
||||||
|
console.log('Viewing batch details:', batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseBatch(batch) {
|
||||||
|
batch.status = 'paused';
|
||||||
|
batch.eta = 'Paused';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeBatch(batch) {
|
||||||
|
batch.status = 'processing';
|
||||||
|
batch.eta = '1.5 hours';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelBatch(batch) {
|
||||||
|
batch.status = 'cancelled';
|
||||||
|
batch.eta = 'Cancelled';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBatch() {
|
||||||
|
// Mock batch creation
|
||||||
|
const batch = {
|
||||||
|
id: `batch_${Date.now()}`,
|
||||||
|
name: newBatch.value.name,
|
||||||
|
description: newBatch.value.description,
|
||||||
|
status: 'pending',
|
||||||
|
type: newBatch.value.type,
|
||||||
|
processed: 0,
|
||||||
|
total: 10000, // Mock total
|
||||||
|
chunkSize: newBatch.value.chunkSize,
|
||||||
|
startedAt: new Date().toLocaleString(),
|
||||||
|
eta: 'Pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
activeBatches.value.unshift(batch);
|
||||||
|
showCreateBatchModal.value = false;
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
newBatch.value = {
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
description: '',
|
||||||
|
chunkSize: 500,
|
||||||
|
priority: 'medium',
|
||||||
|
delay: 1000,
|
||||||
|
criteria: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNewBatch() {
|
||||||
|
// Mock batch scheduling
|
||||||
|
console.log('Scheduling batch:', scheduleBatch.value);
|
||||||
|
showScheduleModal.value = false;
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
scheduleBatch.value = {
|
||||||
|
templateId: '',
|
||||||
|
date: '',
|
||||||
|
time: '',
|
||||||
|
timezone: 'UTC',
|
||||||
|
recurring: false,
|
||||||
|
recurrencePattern: 'daily'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBatchTemplate(template) {
|
||||||
|
newBatch.value = {
|
||||||
|
name: template.name,
|
||||||
|
type: template.type,
|
||||||
|
description: template.description,
|
||||||
|
chunkSize: template.chunkSize,
|
||||||
|
priority: template.priority,
|
||||||
|
delay: 1000,
|
||||||
|
criteria: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
showTemplatesModal.value = false;
|
||||||
|
showCreateBatchModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshBatches() {
|
||||||
|
// Mock refresh
|
||||||
|
console.log('Refreshing batches...');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
@ -1,7 +1,278 @@
|
|||||||
<template>
|
<template>
|
||||||
<div></div>
|
<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-schedule"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Queue & Scheduler</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Controls how messages and background tasks are managed — crucial for
|
||||||
|
reliability, scalability, and timing precision in any notification or
|
||||||
|
event-driven system.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<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 quickStats"
|
||||||
|
: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-5 flex justify-center items-center bg-primary/20 rounded-2xl transition-all duration-300 hover:bg-primary/30"
|
||||||
|
>
|
||||||
|
<Icon class="text-primary text-3xl" :name="stat.icon"></Icon>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 truncate">
|
||||||
|
<span class="block font-bold text-2xl leading-tight text-primary">
|
||||||
|
{{ stat.value }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-medium text-gray-600">
|
||||||
|
{{ stat.title }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Feature Cards -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(feature, index) in features"
|
||||||
|
:key="index"
|
||||||
|
class="transition-all duration-300 hover:shadow-lg cursor-pointer"
|
||||||
|
@click="navigateTo(feature.path)"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" :name="feature.icon"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">{{ feature.title }}</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600 mb-4">{{ feature.description }}</p>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(useCase, idx) in feature.useCases"
|
||||||
|
:key="idx"
|
||||||
|
class="flex items-start"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
class="mr-2 mt-1 text-green-500 text-sm"
|
||||||
|
name="ic:outline-check-circle"
|
||||||
|
></Icon>
|
||||||
|
<span class="text-sm text-gray-700">{{ useCase }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<rs-button variant="outline" size="sm">
|
||||||
|
<Icon class="mr-1" name="ic:outline-arrow-forward"></Icon>
|
||||||
|
Open
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Activity -->
|
||||||
|
<rs-card class="mt-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-history"></Icon>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Recent Queue Activity</h3>
|
||||||
|
</div>
|
||||||
|
<rs-button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
@click="navigateTo('/notification/queue-scheduler/monitor')"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(activity, index) in recentActivity"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="w-3 h-3 rounded-full mr-3"
|
||||||
|
:class="{
|
||||||
|
'bg-green-500': activity.status === 'completed',
|
||||||
|
'bg-yellow-500': activity.status === 'processing',
|
||||||
|
'bg-red-500': activity.status === 'failed',
|
||||||
|
'bg-blue-500': activity.status === 'pending',
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ activity.job }}</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ activity.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm font-medium capitalize">{{ activity.status }}</p>
|
||||||
|
<p class="text-xs text-gray-500">{{ activity.time }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Queue & Scheduler",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue & Scheduler",
|
||||||
|
path: "/notification/queue-scheduler",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick stats data
|
||||||
|
const quickStats = ref([
|
||||||
|
{
|
||||||
|
title: "Active Jobs",
|
||||||
|
value: "1,247",
|
||||||
|
icon: "ic:outline-work",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Completed Today",
|
||||||
|
value: "8,932",
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Failed Jobs",
|
||||||
|
value: "23",
|
||||||
|
icon: "ic:outline-error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Avg Processing Time",
|
||||||
|
value: "2.3s",
|
||||||
|
icon: "ic:outline-timer",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Feature cards data
|
||||||
|
const features = ref([
|
||||||
|
{
|
||||||
|
title: "Queue Monitor",
|
||||||
|
description:
|
||||||
|
"Displays a live overview of all queued jobs — what's currently pending, processing, succeeded, or failed.",
|
||||||
|
icon: "ic:outline-monitor",
|
||||||
|
path: "/notification/queue-scheduler/monitor",
|
||||||
|
useCases: [
|
||||||
|
"Monitor load spikes and backlogs in real-time",
|
||||||
|
"Track job status with auto-refresh",
|
||||||
|
"Filter by job type, channel, and status",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Batch Processing",
|
||||||
|
description:
|
||||||
|
"Schedule or trigger the processing of large groups of messages in one go.",
|
||||||
|
icon: "ic:outline-batch-prediction",
|
||||||
|
path: "/notification/queue-scheduler/batch",
|
||||||
|
useCases: [
|
||||||
|
"Send newsletters and campaigns",
|
||||||
|
"Process messages in chunks (500 per batch)",
|
||||||
|
"Schedule batches for optimal timing",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Retry & Dead Letter",
|
||||||
|
description:
|
||||||
|
"Handles failed jobs with retry logic and dead letter queue for permanent failures.",
|
||||||
|
icon: "ic:outline-refresh",
|
||||||
|
path: "/notification/queue-scheduler/retry",
|
||||||
|
useCases: [
|
||||||
|
"Auto/manual retry of failed messages",
|
||||||
|
"Store permanently failed messages",
|
||||||
|
"Debug with detailed error reasons",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Timezone Handling",
|
||||||
|
description:
|
||||||
|
"Ensures messages are delivered at the right local time for each recipient.",
|
||||||
|
icon: "ic:outline-schedule",
|
||||||
|
path: "/notification/queue-scheduler/timezone",
|
||||||
|
useCases: [
|
||||||
|
"Schedule birthday messages at 9AM local time",
|
||||||
|
"Avoid 2AM push alerts across timezones",
|
||||||
|
"UTC + offset logic for global delivery",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Rate Limiting",
|
||||||
|
description:
|
||||||
|
"Throttles how many messages/jobs can be processed per second/minute/hour.",
|
||||||
|
icon: "ic:outline-speed",
|
||||||
|
path: "/notification/queue-scheduler/rate-limit",
|
||||||
|
useCases: [
|
||||||
|
"Avoid hitting API limits (Twilio, SendGrid)",
|
||||||
|
"Prevent spammy behavior and blacklisting",
|
||||||
|
"Global and per-channel rate controls",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Recent activity data
|
||||||
|
const recentActivity = ref([
|
||||||
|
{
|
||||||
|
job: "Email Campaign #1247",
|
||||||
|
description: "Newsletter to 50,000 subscribers",
|
||||||
|
status: "completed",
|
||||||
|
time: "2 minutes ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
job: "SMS Batch #892",
|
||||||
|
description: "OTP messages to 1,200 users",
|
||||||
|
status: "processing",
|
||||||
|
time: "5 minutes ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
job: "Push Notification #445",
|
||||||
|
description: "App update notification",
|
||||||
|
status: "failed",
|
||||||
|
time: "8 minutes ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
job: "Webhook Delivery #223",
|
||||||
|
description: "Order confirmation webhooks",
|
||||||
|
status: "pending",
|
||||||
|
time: "12 minutes ago",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
job: "Birthday Reminders",
|
||||||
|
description: "Daily birthday notifications",
|
||||||
|
status: "completed",
|
||||||
|
time: "1 hour ago",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
503
pages/notification/queue-scheduler/monitor.vue
Normal file
503
pages/notification/queue-scheduler/monitor.vue
Normal file
@ -0,0 +1,503 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<LayoutsBreadcrumb />
|
||||||
|
|
||||||
|
<!-- Header Section -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-monitor"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Queue Monitor</h1>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<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="toggleAutoRefresh">
|
||||||
|
<Icon
|
||||||
|
class="mr-1"
|
||||||
|
:name="autoRefresh ? 'ic:outline-pause' : 'ic:outline-play-arrow'"
|
||||||
|
></Icon>
|
||||||
|
{{ autoRefresh ? "Pause" : "Resume" }}
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Live overview of all queued jobs — monitor what's currently pending, processing,
|
||||||
|
succeeded, or failed.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Stats Overview -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-5 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(stat, index) in queueStats"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<!-- Filters and Controls -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
|
||||||
|
<select
|
||||||
|
v-model="filters.jobType"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="email">Email</option>
|
||||||
|
<option value="sms">SMS</option>
|
||||||
|
<option value="push">Push Notification</option>
|
||||||
|
<option value="webhook">Webhook</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Status</label>
|
||||||
|
<select
|
||||||
|
v-model="filters.status"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option value="">All Status</option>
|
||||||
|
<option value="pending">Pending</option>
|
||||||
|
<option value="processing">Processing</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
<option value="failed">Failed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Channel</label>
|
||||||
|
<select
|
||||||
|
v-model="filters.channel"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option value="">All Channels</option>
|
||||||
|
<option value="sendgrid">SendGrid</option>
|
||||||
|
<option value="twilio">Twilio</option>
|
||||||
|
<option value="firebase">Firebase</option>
|
||||||
|
<option value="slack">Slack</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<rs-button @click="clearFilters" variant="outline" class="w-full">
|
||||||
|
<Icon class="mr-1" name="ic:outline-clear"></Icon>
|
||||||
|
Clear Filters
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Queue Table -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Queue Jobs</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-gray-600">Last updated: {{ lastUpdated }}</span>
|
||||||
|
<rs-button variant="outline" size="sm" @click="refreshData">
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<rs-table
|
||||||
|
:field="tableFields"
|
||||||
|
:data="filteredJobs"
|
||||||
|
:options="{ striped: true, hover: true }"
|
||||||
|
:optionsAdvanced="{ sortable: true, filterable: false }"
|
||||||
|
advanced
|
||||||
|
>
|
||||||
|
<template #status="{ value }">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 rounded-full mr-2"
|
||||||
|
:class="getStatusColor(value.status)"
|
||||||
|
></div>
|
||||||
|
<span class="capitalize">{{ value.status }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ value }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
class="text-blue-600 hover:text-blue-800 text-sm"
|
||||||
|
@click="viewJobDetails(value)"
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="value.status === 'failed'"
|
||||||
|
class="text-green-600 hover:text-green-800 text-sm"
|
||||||
|
@click="retryJob(value)"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-table>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Job Details Modal -->
|
||||||
|
<rs-modal v-model="showJobModal" size="lg">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Job Details</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="selectedJob" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Job ID</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.id }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Status</label>
|
||||||
|
<rs-badge :variant="getStatusVariant(selectedJob.status)">
|
||||||
|
{{ selectedJob.status }}
|
||||||
|
</rs-badge>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Type</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.type }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Channel</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.channel }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Created</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.created }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700"
|
||||||
|
>Processing Time</label
|
||||||
|
>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.processingTime }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Payload</label>
|
||||||
|
<pre class="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-40">{{
|
||||||
|
JSON.stringify(selectedJob.payload, null, 2)
|
||||||
|
}}</pre>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedJob.error">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2"
|
||||||
|
>Error Details</label
|
||||||
|
>
|
||||||
|
<div class="bg-red-50 border border-red-200 rounded p-3">
|
||||||
|
<p class="text-sm text-red-800">{{ selectedJob.error }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<rs-button variant="outline" @click="showJobModal = false">Close</rs-button>
|
||||||
|
<rs-button
|
||||||
|
v-if="selectedJob?.status === 'failed'"
|
||||||
|
@click="retryJob"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Retry Job
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Queue Monitor",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue & Scheduler",
|
||||||
|
path: "/notification/queue-scheduler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Monitor",
|
||||||
|
path: "/notification/queue-scheduler/monitor",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const autoRefresh = ref(true);
|
||||||
|
const lastUpdated = ref(new Date().toLocaleTimeString());
|
||||||
|
const showJobModal = ref(false);
|
||||||
|
const selectedJob = ref(null);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const filters = ref({
|
||||||
|
jobType: "",
|
||||||
|
status: "",
|
||||||
|
channel: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Queue statistics
|
||||||
|
const queueStats = ref([
|
||||||
|
{
|
||||||
|
title: "Pending",
|
||||||
|
value: "1,247",
|
||||||
|
icon: "ic:outline-schedule",
|
||||||
|
bgColor: "bg-blue-100",
|
||||||
|
iconColor: "text-blue-600",
|
||||||
|
textColor: "text-blue-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Processing",
|
||||||
|
value: "89",
|
||||||
|
icon: "ic:outline-sync",
|
||||||
|
bgColor: "bg-yellow-100",
|
||||||
|
iconColor: "text-yellow-600",
|
||||||
|
textColor: "text-yellow-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Completed",
|
||||||
|
value: "8,932",
|
||||||
|
icon: "ic:outline-check-circle",
|
||||||
|
bgColor: "bg-green-100",
|
||||||
|
iconColor: "text-green-600",
|
||||||
|
textColor: "text-green-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Failed",
|
||||||
|
value: "23",
|
||||||
|
icon: "ic:outline-error",
|
||||||
|
bgColor: "bg-red-100",
|
||||||
|
iconColor: "text-red-600",
|
||||||
|
textColor: "text-red-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Retrying",
|
||||||
|
value: "7",
|
||||||
|
icon: "ic:outline-refresh",
|
||||||
|
bgColor: "bg-purple-100",
|
||||||
|
iconColor: "text-purple-600",
|
||||||
|
textColor: "text-purple-600",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Table fields
|
||||||
|
const tableFields = ref([
|
||||||
|
{ key: "id", label: "Job ID", sortable: true },
|
||||||
|
{ key: "type", label: "Type", sortable: true },
|
||||||
|
{ key: "channel", label: "Channel", sortable: true },
|
||||||
|
{ key: "status", label: "Status", sortable: true },
|
||||||
|
{ key: "priority", label: "Priority", sortable: true },
|
||||||
|
{ key: "created", label: "Created", sortable: true },
|
||||||
|
{ key: "processingTime", label: "Processing Time", sortable: true },
|
||||||
|
{ key: "actions", label: "Actions", sortable: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock job data
|
||||||
|
const jobs = ref([
|
||||||
|
{
|
||||||
|
id: "job_001",
|
||||||
|
type: "email",
|
||||||
|
channel: "sendgrid",
|
||||||
|
status: "completed",
|
||||||
|
priority: "high",
|
||||||
|
created: "2024-01-15 10:30:00",
|
||||||
|
processingTime: "2.3s",
|
||||||
|
payload: { to: "user@example.com", subject: "Welcome!", template: "welcome" },
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job_002",
|
||||||
|
type: "sms",
|
||||||
|
channel: "twilio",
|
||||||
|
status: "processing",
|
||||||
|
priority: "medium",
|
||||||
|
created: "2024-01-15 10:32:00",
|
||||||
|
processingTime: "1.8s",
|
||||||
|
payload: { to: "+1234567890", message: "Your OTP is 123456" },
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job_003",
|
||||||
|
type: "push",
|
||||||
|
channel: "firebase",
|
||||||
|
status: "failed",
|
||||||
|
priority: "low",
|
||||||
|
created: "2024-01-15 10:35:00",
|
||||||
|
processingTime: "0.5s",
|
||||||
|
payload: {
|
||||||
|
token: "device_token_123",
|
||||||
|
title: "New Message",
|
||||||
|
body: "You have a new message",
|
||||||
|
},
|
||||||
|
error: "Invalid device token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job_004",
|
||||||
|
type: "webhook",
|
||||||
|
channel: "slack",
|
||||||
|
status: "pending",
|
||||||
|
priority: "high",
|
||||||
|
created: "2024-01-15 10:40:00",
|
||||||
|
processingTime: "-",
|
||||||
|
payload: { url: "https://hooks.slack.com/...", data: { text: "Order completed" } },
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "job_005",
|
||||||
|
type: "email",
|
||||||
|
channel: "sendgrid",
|
||||||
|
status: "retrying",
|
||||||
|
priority: "medium",
|
||||||
|
created: "2024-01-15 10:25:00",
|
||||||
|
processingTime: "5.2s",
|
||||||
|
payload: {
|
||||||
|
to: "customer@example.com",
|
||||||
|
subject: "Order Confirmation",
|
||||||
|
template: "order",
|
||||||
|
},
|
||||||
|
error: "Rate limit exceeded",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Computed filtered jobs
|
||||||
|
const filteredJobs = computed(() => {
|
||||||
|
let filtered = jobs.value;
|
||||||
|
|
||||||
|
if (filters.value.jobType) {
|
||||||
|
filtered = filtered.filter((job) => job.type === filters.value.jobType);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.status) {
|
||||||
|
filtered = filtered.filter((job) => job.status === filters.value.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.channel) {
|
||||||
|
filtered = filtered.filter((job) => job.channel === filters.value.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return plain data without VNodes
|
||||||
|
return filtered;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function getStatusColor(status) {
|
||||||
|
const colors = {
|
||||||
|
pending: "bg-blue-500",
|
||||||
|
processing: "bg-yellow-500",
|
||||||
|
completed: "bg-green-500",
|
||||||
|
failed: "bg-red-500",
|
||||||
|
retrying: "bg-purple-500",
|
||||||
|
};
|
||||||
|
return colors[status] || "bg-gray-500";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusVariant(status) {
|
||||||
|
const variants = {
|
||||||
|
pending: "info",
|
||||||
|
processing: "warning",
|
||||||
|
completed: "success",
|
||||||
|
failed: "danger",
|
||||||
|
retrying: "secondary",
|
||||||
|
};
|
||||||
|
return variants[status] || "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewJobDetails(job) {
|
||||||
|
selectedJob.value = job;
|
||||||
|
showJobModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryJob(job) {
|
||||||
|
// Mock retry logic
|
||||||
|
if (job) {
|
||||||
|
job.status = "pending";
|
||||||
|
job.error = null;
|
||||||
|
}
|
||||||
|
showJobModal.value = false;
|
||||||
|
// In real app, would make API call to retry the job
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleAutoRefresh() {
|
||||||
|
autoRefresh.value = !autoRefresh.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshData() {
|
||||||
|
lastUpdated.value = new Date().toLocaleTimeString();
|
||||||
|
// In real app, would fetch fresh data from API
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
filters.value = {
|
||||||
|
jobType: "",
|
||||||
|
status: "",
|
||||||
|
channel: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto refresh functionality
|
||||||
|
let refreshInterval;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (autoRefresh.value) {
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
if (autoRefresh.value) {
|
||||||
|
refreshData();
|
||||||
|
}
|
||||||
|
}, 5000); // Refresh every 5 seconds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(autoRefresh, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
if (autoRefresh.value) {
|
||||||
|
refreshData();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
768
pages/notification/queue-scheduler/rate-limit.vue
Normal file
768
pages/notification/queue-scheduler/rate-limit.vue
Normal file
@ -0,0 +1,768 @@
|
|||||||
|
<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>
|
692
pages/notification/queue-scheduler/retry.vue
Normal file
692
pages/notification/queue-scheduler/retry.vue
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
<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-refresh"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Retry & Dead Letter Queue</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Handles failed jobs with retry logic and dead letter queue for permanent failures.
|
||||||
|
Critical for debugging and ensuring no message is lost without explanation.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- 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 retryStats"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<!-- Retry Configuration -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Retry Configuration</h3>
|
||||||
|
<rs-button variant="outline" size="sm" @click="showRetryConfigModal = true">
|
||||||
|
<Icon class="mr-1" name="ic:outline-settings"></Icon>
|
||||||
|
Configure
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="(config, index) in retryConfigs"
|
||||||
|
:key="index"
|
||||||
|
class="border border-gray-200 rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-3">
|
||||||
|
<Icon class="mr-2 text-primary" :name="config.icon"></Icon>
|
||||||
|
<h4 class="font-semibold">{{ config.type }}</h4>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Max Attempts:</span>
|
||||||
|
<span class="font-medium">{{ config.maxAttempts }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Backoff Strategy:</span>
|
||||||
|
<span class="font-medium">{{ config.backoffStrategy }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Initial Delay:</span>
|
||||||
|
<span class="font-medium">{{ config.initialDelay }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Max Delay:</span>
|
||||||
|
<span class="font-medium">{{ config.maxDelay }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Tabs for Failed Jobs and Dead Letter Queue -->
|
||||||
|
<rs-tab v-model="activeTab" class="mb-6">
|
||||||
|
<rs-tab-item name="failed" label="Failed Jobs">
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Failed Jobs</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<rs-button variant="outline" size="sm" @click="retryAllFailed">
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Retry All
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="outline" size="sm" @click="refreshFailedJobs">
|
||||||
|
<Icon class="mr-1" name="ic:outline-sync"></Icon>
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
|
||||||
|
<select v-model="failedFilters.type" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="email">Email</option>
|
||||||
|
<option value="sms">SMS</option>
|
||||||
|
<option value="push">Push Notification</option>
|
||||||
|
<option value="webhook">Webhook</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Error Type</label>
|
||||||
|
<select v-model="failedFilters.errorType" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="">All Errors</option>
|
||||||
|
<option value="network">Network Error</option>
|
||||||
|
<option value="rate_limit">Rate Limit</option>
|
||||||
|
<option value="invalid_token">Invalid Token</option>
|
||||||
|
<option value="timeout">Timeout</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<rs-button @click="clearFailedFilters" variant="outline" class="w-full">
|
||||||
|
<Icon class="mr-1" name="ic:outline-clear"></Icon>
|
||||||
|
Clear Filters
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<rs-table
|
||||||
|
:field="failedJobsFields"
|
||||||
|
:data="filteredFailedJobs"
|
||||||
|
:options="{ striped: true, hover: true }"
|
||||||
|
:optionsAdvanced="{ sortable: true, filterable: false }"
|
||||||
|
advanced
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</rs-tab-item>
|
||||||
|
|
||||||
|
<rs-tab-item name="dead-letter" label="Dead Letter Queue">
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Dead Letter Queue</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<rs-button variant="outline" size="sm" @click="exportDeadLetters">
|
||||||
|
<Icon class="mr-1" name="ic:outline-download"></Icon>
|
||||||
|
Export
|
||||||
|
</rs-button>
|
||||||
|
<rs-button variant="outline" size="sm" @click="refreshDeadLetters">
|
||||||
|
<Icon class="mr-1" name="ic:outline-sync"></Icon>
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Job Type</label>
|
||||||
|
<select v-model="deadLetterFilters.type" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="">All Types</option>
|
||||||
|
<option value="email">Email</option>
|
||||||
|
<option value="sms">SMS</option>
|
||||||
|
<option value="push">Push Notification</option>
|
||||||
|
<option value="webhook">Webhook</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Date Range</label>
|
||||||
|
<select v-model="deadLetterFilters.dateRange" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="">All Time</option>
|
||||||
|
<option value="today">Today</option>
|
||||||
|
<option value="week">This Week</option>
|
||||||
|
<option value="month">This Month</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-end">
|
||||||
|
<rs-button @click="clearDeadLetterFilters" variant="outline" class="w-full">
|
||||||
|
<Icon class="mr-1" name="ic:outline-clear"></Icon>
|
||||||
|
Clear Filters
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<rs-table
|
||||||
|
:field="deadLetterFields"
|
||||||
|
:data="filteredDeadLetters"
|
||||||
|
:options="{ striped: true, hover: true }"
|
||||||
|
:optionsAdvanced="{ sortable: true, filterable: false }"
|
||||||
|
advanced
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
</rs-tab-item>
|
||||||
|
</rs-tab>
|
||||||
|
|
||||||
|
<!-- Job Details Modal -->
|
||||||
|
<rs-modal v-model="showJobModal" size="lg">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Job Details</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="selectedJob" class="space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Job ID</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.id }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Type</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.type }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Attempts</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.attempts }}/{{ selectedJob.maxAttempts }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700">Last Attempt</label>
|
||||||
|
<p class="text-sm text-gray-900">{{ selectedJob.lastAttempt }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error History -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Error History</label>
|
||||||
|
<div class="space-y-2 max-h-40 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="(error, index) in selectedJob.errorHistory"
|
||||||
|
:key="index"
|
||||||
|
class="bg-red-50 border border-red-200 rounded p-3"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-start mb-1">
|
||||||
|
<span class="text-sm font-medium text-red-800">Attempt {{ error.attempt }}</span>
|
||||||
|
<span class="text-xs text-red-600">{{ error.timestamp }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-red-700">{{ error.message }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payload -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Payload</label>
|
||||||
|
<pre class="bg-gray-100 p-3 rounded text-xs overflow-auto max-h-40">{{ JSON.stringify(selectedJob.payload, null, 2) }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<rs-button variant="outline" @click="showJobModal = false">Close</rs-button>
|
||||||
|
<rs-button v-if="selectedJob?.canRetry" @click="retryJob(selectedJob)" variant="primary">
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Retry Job
|
||||||
|
</rs-button>
|
||||||
|
<rs-button v-if="!selectedJob?.isDead" @click="moveToDeadLetter(selectedJob)" variant="danger">
|
||||||
|
<Icon class="mr-1" name="ic:outline-delete"></Icon>
|
||||||
|
Move to DLQ
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
|
||||||
|
<!-- Retry Configuration Modal -->
|
||||||
|
<rs-modal v-model="showRetryConfigModal" size="lg">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Retry Configuration</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div
|
||||||
|
v-for="(config, index) in retryConfigs"
|
||||||
|
: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.type }} 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">Max Attempts</label>
|
||||||
|
<input
|
||||||
|
v-model.number="config.maxAttempts"
|
||||||
|
type="number"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Backoff Strategy</label>
|
||||||
|
<select v-model="config.backoffStrategy" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="fixed">Fixed</option>
|
||||||
|
<option value="exponential">Exponential</option>
|
||||||
|
<option value="linear">Linear</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Initial Delay (seconds)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="config.initialDelay"
|
||||||
|
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">Max Delay (seconds)</label>
|
||||||
|
<input
|
||||||
|
v-model.number="config.maxDelay"
|
||||||
|
type="number"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
min="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<rs-button variant="outline" @click="showRetryConfigModal = false">Cancel</rs-button>
|
||||||
|
<rs-button @click="saveRetryConfig" variant="primary">Save Configuration</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Retry & Dead Letter Queue",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue & Scheduler",
|
||||||
|
path: "/notification/queue-scheduler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Retry & Dead Letter",
|
||||||
|
path: "/notification/queue-scheduler/retry",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const activeTab = ref('failed');
|
||||||
|
const showJobModal = ref(false);
|
||||||
|
const showRetryConfigModal = ref(false);
|
||||||
|
const selectedJob = ref(null);
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const failedFilters = ref({
|
||||||
|
type: '',
|
||||||
|
errorType: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const deadLetterFilters = ref({
|
||||||
|
type: '',
|
||||||
|
dateRange: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
const retryStats = ref([
|
||||||
|
{
|
||||||
|
title: "Failed Jobs",
|
||||||
|
value: "156",
|
||||||
|
icon: "ic:outline-error",
|
||||||
|
bgColor: "bg-red-100",
|
||||||
|
iconColor: "text-red-600",
|
||||||
|
textColor: "text-red-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Retrying",
|
||||||
|
value: "23",
|
||||||
|
icon: "ic:outline-refresh",
|
||||||
|
bgColor: "bg-yellow-100",
|
||||||
|
iconColor: "text-yellow-600",
|
||||||
|
textColor: "text-yellow-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Dead Letters",
|
||||||
|
value: "45",
|
||||||
|
icon: "ic:outline-delete",
|
||||||
|
bgColor: "bg-gray-100",
|
||||||
|
iconColor: "text-gray-600",
|
||||||
|
textColor: "text-gray-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Success Rate",
|
||||||
|
value: "94.2%",
|
||||||
|
icon: "ic:outline-trending-up",
|
||||||
|
bgColor: "bg-green-100",
|
||||||
|
iconColor: "text-green-600",
|
||||||
|
textColor: "text-green-600"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Retry configurations
|
||||||
|
const retryConfigs = ref([
|
||||||
|
{
|
||||||
|
type: 'Email',
|
||||||
|
icon: 'ic:outline-email',
|
||||||
|
maxAttempts: 3,
|
||||||
|
backoffStrategy: 'exponential',
|
||||||
|
initialDelay: 30,
|
||||||
|
maxDelay: 300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'SMS',
|
||||||
|
icon: 'ic:outline-sms',
|
||||||
|
maxAttempts: 5,
|
||||||
|
backoffStrategy: 'linear',
|
||||||
|
initialDelay: 10,
|
||||||
|
maxDelay: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Push',
|
||||||
|
icon: 'ic:outline-notifications',
|
||||||
|
maxAttempts: 2,
|
||||||
|
backoffStrategy: 'fixed',
|
||||||
|
initialDelay: 60,
|
||||||
|
maxDelay: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'Webhook',
|
||||||
|
icon: 'ic:outline-webhook',
|
||||||
|
maxAttempts: 4,
|
||||||
|
backoffStrategy: 'exponential',
|
||||||
|
initialDelay: 15,
|
||||||
|
maxDelay: 600
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Table fields for failed jobs
|
||||||
|
const failedJobsFields = ref([
|
||||||
|
{ key: 'id', label: 'Job ID', sortable: true },
|
||||||
|
{ key: 'type', label: 'Type', sortable: true },
|
||||||
|
{ key: 'attempts', label: 'Attempts', sortable: true },
|
||||||
|
{ key: 'lastError', label: 'Last Error', sortable: false },
|
||||||
|
{ key: 'lastAttempt', label: 'Last Attempt', sortable: true },
|
||||||
|
{ key: 'nextRetry', label: 'Next Retry', sortable: true },
|
||||||
|
{ key: 'actions', label: 'Actions', sortable: false }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Table fields for dead letter queue
|
||||||
|
const deadLetterFields = ref([
|
||||||
|
{ key: 'id', label: 'Job ID', sortable: true },
|
||||||
|
{ key: 'type', label: 'Type', sortable: true },
|
||||||
|
{ key: 'totalAttempts', label: 'Total Attempts', sortable: true },
|
||||||
|
{ key: 'finalError', label: 'Final Error', sortable: false },
|
||||||
|
{ key: 'failedAt', label: 'Failed At', sortable: true },
|
||||||
|
{ key: 'actions', label: 'Actions', sortable: false }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock failed jobs data
|
||||||
|
const failedJobs = ref([
|
||||||
|
{
|
||||||
|
id: 'job_fail_001',
|
||||||
|
type: 'email',
|
||||||
|
attempts: 2,
|
||||||
|
maxAttempts: 3,
|
||||||
|
lastError: 'SMTP connection timeout',
|
||||||
|
lastAttempt: '2024-01-15 10:30:00',
|
||||||
|
nextRetry: '2024-01-15 11:00:00',
|
||||||
|
canRetry: true,
|
||||||
|
isDead: false,
|
||||||
|
payload: { to: 'user@example.com', subject: 'Welcome!' },
|
||||||
|
errorHistory: [
|
||||||
|
{ attempt: 1, message: 'SMTP server unavailable', timestamp: '2024-01-15 10:00:00' },
|
||||||
|
{ attempt: 2, message: 'SMTP connection timeout', timestamp: '2024-01-15 10:30:00' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'job_fail_002',
|
||||||
|
type: 'sms',
|
||||||
|
attempts: 3,
|
||||||
|
maxAttempts: 5,
|
||||||
|
lastError: 'Invalid phone number format',
|
||||||
|
lastAttempt: '2024-01-15 10:25:00',
|
||||||
|
nextRetry: '2024-01-15 10:45:00',
|
||||||
|
canRetry: true,
|
||||||
|
isDead: false,
|
||||||
|
payload: { to: '+invalid', message: 'Your OTP is 123456' },
|
||||||
|
errorHistory: [
|
||||||
|
{ attempt: 1, message: 'Invalid phone number format', timestamp: '2024-01-15 10:00:00' },
|
||||||
|
{ attempt: 2, message: 'Invalid phone number format', timestamp: '2024-01-15 10:15:00' },
|
||||||
|
{ attempt: 3, message: 'Invalid phone number format', timestamp: '2024-01-15 10:25:00' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock dead letter queue data
|
||||||
|
const deadLetters = ref([
|
||||||
|
{
|
||||||
|
id: 'job_dead_001',
|
||||||
|
type: 'push',
|
||||||
|
totalAttempts: 2,
|
||||||
|
finalError: 'Invalid device token - token expired',
|
||||||
|
failedAt: '2024-01-14 15:30:00',
|
||||||
|
canRetry: false,
|
||||||
|
isDead: true,
|
||||||
|
payload: { token: 'expired_token_123', title: 'New Message' },
|
||||||
|
errorHistory: [
|
||||||
|
{ attempt: 1, message: 'Invalid device token', timestamp: '2024-01-14 15:00:00' },
|
||||||
|
{ attempt: 2, message: 'Invalid device token - token expired', timestamp: '2024-01-14 15:30:00' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'job_dead_002',
|
||||||
|
type: 'webhook',
|
||||||
|
totalAttempts: 4,
|
||||||
|
finalError: 'Endpoint not found (404)',
|
||||||
|
failedAt: '2024-01-13 12:45:00',
|
||||||
|
canRetry: false,
|
||||||
|
isDead: true,
|
||||||
|
payload: { url: 'https://api.example.com/webhook', data: { event: 'order.completed' } },
|
||||||
|
errorHistory: [
|
||||||
|
{ attempt: 1, message: 'Connection timeout', timestamp: '2024-01-13 12:00:00' },
|
||||||
|
{ attempt: 2, message: 'Connection timeout', timestamp: '2024-01-13 12:15:00' },
|
||||||
|
{ attempt: 3, message: 'Endpoint not found (404)', timestamp: '2024-01-13 12:30:00' },
|
||||||
|
{ attempt: 4, message: 'Endpoint not found (404)', timestamp: '2024-01-13 12:45:00' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Computed filtered data
|
||||||
|
const filteredFailedJobs = computed(() => {
|
||||||
|
let filtered = failedJobs.value;
|
||||||
|
|
||||||
|
if (failedFilters.value.type) {
|
||||||
|
filtered = filtered.filter(job => job.type === failedFilters.value.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add actions column
|
||||||
|
return filtered.map(job => ({
|
||||||
|
...job,
|
||||||
|
attempts: `${job.attempts}/${job.maxAttempts}`,
|
||||||
|
actions: h('div', { class: 'flex gap-2' }, [
|
||||||
|
h('button', {
|
||||||
|
class: 'text-blue-600 hover:text-blue-800 text-sm',
|
||||||
|
onClick: () => viewJobDetails(job)
|
||||||
|
}, 'View'),
|
||||||
|
h('button', {
|
||||||
|
class: 'text-green-600 hover:text-green-800 text-sm',
|
||||||
|
onClick: () => retryJob(job)
|
||||||
|
}, 'Retry'),
|
||||||
|
h('button', {
|
||||||
|
class: 'text-red-600 hover:text-red-800 text-sm',
|
||||||
|
onClick: () => moveToDeadLetter(job)
|
||||||
|
}, 'Move to DLQ')
|
||||||
|
])
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredDeadLetters = computed(() => {
|
||||||
|
let filtered = deadLetters.value;
|
||||||
|
|
||||||
|
if (deadLetterFilters.value.type) {
|
||||||
|
filtered = filtered.filter(job => job.type === deadLetterFilters.value.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add actions column
|
||||||
|
return filtered.map(job => ({
|
||||||
|
...job,
|
||||||
|
actions: h('div', { class: 'flex gap-2' }, [
|
||||||
|
h('button', {
|
||||||
|
class: 'text-blue-600 hover:text-blue-800 text-sm',
|
||||||
|
onClick: () => viewJobDetails(job)
|
||||||
|
}, 'View'),
|
||||||
|
h('button', {
|
||||||
|
class: 'text-red-600 hover:text-red-800 text-sm',
|
||||||
|
onClick: () => deleteDeadLetter(job)
|
||||||
|
}, 'Delete')
|
||||||
|
])
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function viewJobDetails(job) {
|
||||||
|
selectedJob.value = job;
|
||||||
|
showJobModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryJob(job) {
|
||||||
|
// Mock retry logic
|
||||||
|
job.attempts += 1;
|
||||||
|
job.lastAttempt = new Date().toLocaleString();
|
||||||
|
job.nextRetry = new Date(Date.now() + 30 * 60000).toLocaleString(); // 30 minutes from now
|
||||||
|
|
||||||
|
if (job.attempts >= job.maxAttempts) {
|
||||||
|
moveToDeadLetter(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
showJobModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function retryAllFailed() {
|
||||||
|
failedJobs.value.forEach(job => {
|
||||||
|
if (job.canRetry) {
|
||||||
|
retryJob(job);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveToDeadLetter(job) {
|
||||||
|
// Move job to dead letter queue
|
||||||
|
const deadJob = {
|
||||||
|
...job,
|
||||||
|
totalAttempts: job.attempts,
|
||||||
|
finalError: job.lastError,
|
||||||
|
failedAt: new Date().toLocaleString(),
|
||||||
|
canRetry: false,
|
||||||
|
isDead: true
|
||||||
|
};
|
||||||
|
|
||||||
|
deadLetters.value.unshift(deadJob);
|
||||||
|
|
||||||
|
// Remove from failed jobs
|
||||||
|
const index = failedJobs.value.findIndex(j => j.id === job.id);
|
||||||
|
if (index > -1) {
|
||||||
|
failedJobs.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
showJobModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteDeadLetter(job) {
|
||||||
|
const index = deadLetters.value.findIndex(j => j.id === job.id);
|
||||||
|
if (index > -1) {
|
||||||
|
deadLetters.value.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFailedFilters() {
|
||||||
|
failedFilters.value = {
|
||||||
|
type: '',
|
||||||
|
errorType: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDeadLetterFilters() {
|
||||||
|
deadLetterFilters.value = {
|
||||||
|
type: '',
|
||||||
|
dateRange: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshFailedJobs() {
|
||||||
|
// Mock refresh
|
||||||
|
console.log('Refreshing failed jobs...');
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDeadLetters() {
|
||||||
|
// Mock refresh
|
||||||
|
console.log('Refreshing dead letters...');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportDeadLetters() {
|
||||||
|
// Mock export
|
||||||
|
console.log('Exporting dead letters...');
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRetryConfig() {
|
||||||
|
// Mock save
|
||||||
|
console.log('Saving retry configuration...');
|
||||||
|
showRetryConfigModal.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
707
pages/notification/queue-scheduler/timezone.vue
Normal file
707
pages/notification/queue-scheduler/timezone.vue
Normal file
@ -0,0 +1,707 @@
|
|||||||
|
<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-schedule"></Icon>
|
||||||
|
<h1 class="text-xl font-bold text-primary">Timezone Handling</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Ensures messages are delivered at the right local time for each recipient.
|
||||||
|
Schedule birthday messages at 9AM local time and avoid 2AM push alerts across timezones.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Current Time Display -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||||
|
<rs-card
|
||||||
|
v-for="(timezone, index) in majorTimezones"
|
||||||
|
:key="index"
|
||||||
|
class="transition-all duration-300 hover:shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="pt-5 pb-3 px-5 text-center">
|
||||||
|
<div class="mb-2">
|
||||||
|
<Icon class="text-primary text-2xl" name="ic:outline-access-time"></Icon>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-lg">{{ timezone.name }}</h3>
|
||||||
|
<p class="text-2xl font-bold text-primary">{{ timezone.time }}</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ timezone.zone }}</p>
|
||||||
|
</div>
|
||||||
|
</rs-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timezone 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 timezoneStats"
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<!-- Timezone Configuration -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Timezone Configuration</h3>
|
||||||
|
<rs-button variant="outline" size="sm" @click="showConfigModal = true">
|
||||||
|
<Icon class="mr-1" name="ic:outline-settings"></Icon>
|
||||||
|
Configure
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 class="font-semibold mb-3 flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-schedule"></Icon>
|
||||||
|
Default Delivery Times
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Morning Messages:</span>
|
||||||
|
<span class="font-medium">{{ config.morningTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Afternoon Messages:</span>
|
||||||
|
<span class="font-medium">{{ config.afternoonTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Evening Messages:</span>
|
||||||
|
<span class="font-medium">{{ config.eveningTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 class="font-semibold mb-3 flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-block"></Icon>
|
||||||
|
Quiet Hours
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Start Time:</span>
|
||||||
|
<span class="font-medium">{{ config.quietHours.start }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">End Time:</span>
|
||||||
|
<span class="font-medium">{{ config.quietHours.end }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Emergency Override:</span>
|
||||||
|
<span class="font-medium">{{ config.quietHours.allowEmergency ? 'Enabled' : 'Disabled' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 class="font-semibold mb-3 flex items-center">
|
||||||
|
<Icon class="mr-2 text-primary" name="ic:outline-public"></Icon>
|
||||||
|
Timezone Detection
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-2 text-sm">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Auto-detect:</span>
|
||||||
|
<span class="font-medium">{{ config.autoDetect ? 'Enabled' : 'Disabled' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Fallback Timezone:</span>
|
||||||
|
<span class="font-medium">{{ config.fallbackTimezone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<span class="text-gray-600">Update Frequency:</span>
|
||||||
|
<span class="font-medium">{{ config.updateFrequency }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Scheduled Messages by Timezone -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Scheduled Messages by Timezone</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<select v-model="selectedTimezone" class="p-2 border border-gray-300 rounded-md text-sm">
|
||||||
|
<option value="">All Timezones</option>
|
||||||
|
<option v-for="tz in availableTimezones" :key="tz.value" :value="tz.value">
|
||||||
|
{{ tz.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<rs-button variant="outline" size="sm" @click="refreshScheduledMessages">
|
||||||
|
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||||
|
Refresh
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<rs-table
|
||||||
|
:field="scheduledMessagesFields"
|
||||||
|
:data="filteredScheduledMessages"
|
||||||
|
:options="{ striped: true, hover: true }"
|
||||||
|
:optionsAdvanced="{ sortable: true, filterable: false }"
|
||||||
|
advanced
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Timezone Distribution Chart -->
|
||||||
|
<rs-card class="mb-6">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">User Distribution by Timezone</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<!-- Chart would go here in a real implementation -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="(distribution, index) in timezoneDistribution"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-4 h-4 rounded mr-3" :style="{ backgroundColor: distribution.color }"></div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">{{ distribution.timezone }}</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ distribution.users }} users</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="font-medium">{{ distribution.percentage }}%</p>
|
||||||
|
<p class="text-sm text-gray-600">{{ distribution.currentTime }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h4 class="font-semibold">Delivery Optimization</h4>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<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-check-circle"></Icon>
|
||||||
|
<span class="font-medium text-green-800">Optimal Delivery Windows</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-green-700">
|
||||||
|
{{ optimizationStats.optimalWindows }} messages scheduled during optimal hours
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-yellow-50 border border-yellow-200 rounded p-3">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<Icon class="mr-2 text-yellow-600" name="ic:outline-warning"></Icon>
|
||||||
|
<span class="font-medium text-yellow-800">Quiet Hours Conflicts</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-yellow-700">
|
||||||
|
{{ optimizationStats.quietHoursConflicts }} messages would be sent during quiet hours
|
||||||
|
</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-info"></Icon>
|
||||||
|
<span class="font-medium text-blue-800">Timezone Coverage</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-blue-700">
|
||||||
|
Messages will be delivered across {{ optimizationStats.timezoneCoverage }} timezones
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Timezone Testing Tool -->
|
||||||
|
<rs-card>
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold text-primary">Timezone Testing Tool</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">Test Message</label>
|
||||||
|
<textarea
|
||||||
|
v-model="testMessage.content"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Enter test message content"
|
||||||
|
></textarea>
|
||||||
|
</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">Scheduled Time (UTC)</label>
|
||||||
|
<input
|
||||||
|
v-model="testMessage.scheduledTime"
|
||||||
|
type="datetime-local"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Message Type</label>
|
||||||
|
<select v-model="testMessage.type" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="email">Email</option>
|
||||||
|
<option value="sms">SMS</option>
|
||||||
|
<option value="push">Push Notification</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<rs-button @click="testTimezoneDelivery" variant="primary" class="w-full">
|
||||||
|
<Icon class="mr-1" name="ic:outline-play-arrow"></Icon>
|
||||||
|
Test Timezone Delivery
|
||||||
|
</rs-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<h4 class="font-semibold">Delivery Preview</h4>
|
||||||
|
<div v-if="deliveryPreview.length > 0" class="space-y-2 max-h-60 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="(preview, index) in deliveryPreview"
|
||||||
|
:key="index"
|
||||||
|
class="border border-gray-200 rounded p-3"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-start mb-1">
|
||||||
|
<span class="font-medium">{{ preview.timezone }}</span>
|
||||||
|
<span class="text-sm text-gray-600">{{ preview.users }} users</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
<p class="text-gray-600">Local delivery time: <span class="font-medium">{{ preview.localTime }}</span></p>
|
||||||
|
<p class="text-gray-600">Status:
|
||||||
|
<span :class="{
|
||||||
|
'text-green-600': preview.status === 'optimal',
|
||||||
|
'text-yellow-600': preview.status === 'suboptimal',
|
||||||
|
'text-red-600': preview.status === 'blocked'
|
||||||
|
}" class="font-medium">{{ preview.status }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-center text-gray-500 py-8">
|
||||||
|
<Icon class="text-4xl mb-2" name="ic:outline-schedule"></Icon>
|
||||||
|
<p>Run a test to see delivery preview</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-card>
|
||||||
|
|
||||||
|
<!-- Configuration Modal -->
|
||||||
|
<rs-modal v-model="showConfigModal" size="lg">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-lg font-semibold">Timezone Configuration</h3>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-3">Default Delivery Times</h4>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Morning Messages</label>
|
||||||
|
<input
|
||||||
|
v-model="config.morningTime"
|
||||||
|
type="time"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Afternoon Messages</label>
|
||||||
|
<input
|
||||||
|
v-model="config.afternoonTime"
|
||||||
|
type="time"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Evening Messages</label>
|
||||||
|
<input
|
||||||
|
v-model="config.eveningTime"
|
||||||
|
type="time"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-3">Quiet Hours</h4>
|
||||||
|
<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">Start Time</label>
|
||||||
|
<input
|
||||||
|
v-model="config.quietHours.start"
|
||||||
|
type="time"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">End Time</label>
|
||||||
|
<input
|
||||||
|
v-model="config.quietHours.end"
|
||||||
|
type="time"
|
||||||
|
class="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input
|
||||||
|
v-model="config.quietHours.allowEmergency"
|
||||||
|
type="checkbox"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700">Allow emergency messages during quiet hours</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-3">Timezone Detection</h4>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input
|
||||||
|
v-model="config.autoDetect"
|
||||||
|
type="checkbox"
|
||||||
|
class="mr-2"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700">Auto-detect user timezones</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<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">Fallback Timezone</label>
|
||||||
|
<select v-model="config.fallbackTimezone" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="UTC">UTC</option>
|
||||||
|
<option value="America/New_York">America/New_York</option>
|
||||||
|
<option value="Europe/London">Europe/London</option>
|
||||||
|
<option value="Asia/Tokyo">Asia/Tokyo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-2">Update Frequency</label>
|
||||||
|
<select v-model="config.updateFrequency" class="w-full p-2 border border-gray-300 rounded-md">
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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="saveConfiguration" variant="primary">Save Configuration</rs-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</rs-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
definePageMeta({
|
||||||
|
title: "Timezone Handling",
|
||||||
|
middleware: ["auth"],
|
||||||
|
requiresAuth: true,
|
||||||
|
breadcrumb: [
|
||||||
|
{
|
||||||
|
name: "Notification",
|
||||||
|
path: "/notification",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Queue & Scheduler",
|
||||||
|
path: "/notification/queue-scheduler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Timezone Handling",
|
||||||
|
path: "/notification/queue-scheduler/timezone",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const showConfigModal = ref(false);
|
||||||
|
const selectedTimezone = ref('');
|
||||||
|
const deliveryPreview = ref([]);
|
||||||
|
|
||||||
|
// Current time for major timezones
|
||||||
|
const majorTimezones = ref([
|
||||||
|
{
|
||||||
|
name: 'New York',
|
||||||
|
zone: 'America/New_York',
|
||||||
|
time: new Date().toLocaleTimeString('en-US', { timeZone: 'America/New_York' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'London',
|
||||||
|
zone: 'Europe/London',
|
||||||
|
time: new Date().toLocaleTimeString('en-US', { timeZone: 'Europe/London' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tokyo',
|
||||||
|
zone: 'Asia/Tokyo',
|
||||||
|
time: new Date().toLocaleTimeString('en-US', { timeZone: 'Asia/Tokyo' })
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Update times every second
|
||||||
|
setInterval(() => {
|
||||||
|
majorTimezones.value.forEach(tz => {
|
||||||
|
tz.time = new Date().toLocaleTimeString('en-US', { timeZone: tz.zone });
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
const timezoneStats = ref([
|
||||||
|
{
|
||||||
|
title: "Active Timezones",
|
||||||
|
value: "24",
|
||||||
|
icon: "ic:outline-public",
|
||||||
|
bgColor: "bg-blue-100",
|
||||||
|
iconColor: "text-blue-600",
|
||||||
|
textColor: "text-blue-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Scheduled Messages",
|
||||||
|
value: "1,847",
|
||||||
|
icon: "ic:outline-schedule",
|
||||||
|
bgColor: "bg-green-100",
|
||||||
|
iconColor: "text-green-600",
|
||||||
|
textColor: "text-green-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Optimal Deliveries",
|
||||||
|
value: "94.2%",
|
||||||
|
icon: "ic:outline-trending-up",
|
||||||
|
bgColor: "bg-purple-100",
|
||||||
|
iconColor: "text-purple-600",
|
||||||
|
textColor: "text-purple-600"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Quiet Hours Respected",
|
||||||
|
value: "99.8%",
|
||||||
|
icon: "ic:outline-nights-stay",
|
||||||
|
bgColor: "bg-orange-100",
|
||||||
|
iconColor: "text-orange-600",
|
||||||
|
textColor: "text-orange-600"
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const config = ref({
|
||||||
|
morningTime: '09:00',
|
||||||
|
afternoonTime: '14:00',
|
||||||
|
eveningTime: '18:00',
|
||||||
|
quietHours: {
|
||||||
|
start: '22:00',
|
||||||
|
end: '07:00',
|
||||||
|
allowEmergency: true
|
||||||
|
},
|
||||||
|
autoDetect: true,
|
||||||
|
fallbackTimezone: 'UTC',
|
||||||
|
updateFrequency: 'daily'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Available timezones
|
||||||
|
const availableTimezones = ref([
|
||||||
|
{ value: 'America/New_York', label: 'America/New_York (EST/EDT)' },
|
||||||
|
{ value: 'America/Los_Angeles', label: 'America/Los_Angeles (PST/PDT)' },
|
||||||
|
{ value: 'Europe/London', label: 'Europe/London (GMT/BST)' },
|
||||||
|
{ value: 'Europe/Paris', label: 'Europe/Paris (CET/CEST)' },
|
||||||
|
{ value: 'Asia/Tokyo', label: 'Asia/Tokyo (JST)' },
|
||||||
|
{ value: 'Asia/Shanghai', label: 'Asia/Shanghai (CST)' },
|
||||||
|
{ value: 'Asia/Kolkata', label: 'Asia/Kolkata (IST)' },
|
||||||
|
{ value: 'Australia/Sydney', label: 'Australia/Sydney (AEST/AEDT)' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Table fields for scheduled messages
|
||||||
|
const scheduledMessagesFields = ref([
|
||||||
|
{ key: 'id', label: 'Message ID', sortable: true },
|
||||||
|
{ key: 'type', label: 'Type', sortable: true },
|
||||||
|
{ key: 'timezone', label: 'Timezone', sortable: true },
|
||||||
|
{ key: 'scheduledUTC', label: 'Scheduled (UTC)', sortable: true },
|
||||||
|
{ key: 'localTime', label: 'Local Time', sortable: true },
|
||||||
|
{ key: 'recipients', label: 'Recipients', sortable: true },
|
||||||
|
{ key: 'status', label: 'Status', sortable: true }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Mock scheduled messages data
|
||||||
|
const scheduledMessages = ref([
|
||||||
|
{
|
||||||
|
id: 'msg_001',
|
||||||
|
type: 'email',
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
scheduledUTC: '2024-01-15 14:00:00',
|
||||||
|
localTime: '2024-01-15 09:00:00',
|
||||||
|
recipients: 1250,
|
||||||
|
status: 'scheduled'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg_002',
|
||||||
|
type: 'push',
|
||||||
|
timezone: 'Europe/London',
|
||||||
|
scheduledUTC: '2024-01-15 09:00:00',
|
||||||
|
localTime: '2024-01-15 09:00:00',
|
||||||
|
recipients: 890,
|
||||||
|
status: 'scheduled'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'msg_003',
|
||||||
|
type: 'sms',
|
||||||
|
timezone: 'Asia/Tokyo',
|
||||||
|
scheduledUTC: '2024-01-15 00:00:00',
|
||||||
|
localTime: '2024-01-15 09:00:00',
|
||||||
|
recipients: 2100,
|
||||||
|
status: 'scheduled'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Timezone distribution
|
||||||
|
const timezoneDistribution = ref([
|
||||||
|
{
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
users: 15420,
|
||||||
|
percentage: 32.5,
|
||||||
|
color: '#3B82F6',
|
||||||
|
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'America/New_York' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timezone: 'Europe/London',
|
||||||
|
users: 12890,
|
||||||
|
percentage: 27.2,
|
||||||
|
color: '#10B981',
|
||||||
|
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'Europe/London' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timezone: 'Asia/Tokyo',
|
||||||
|
users: 9650,
|
||||||
|
percentage: 20.3,
|
||||||
|
color: '#F59E0B',
|
||||||
|
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'Asia/Tokyo' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timezone: 'Australia/Sydney',
|
||||||
|
users: 5840,
|
||||||
|
percentage: 12.3,
|
||||||
|
color: '#EF4444',
|
||||||
|
currentTime: new Date().toLocaleTimeString('en-US', { timeZone: 'Australia/Sydney' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timezone: 'Others',
|
||||||
|
users: 3700,
|
||||||
|
percentage: 7.7,
|
||||||
|
color: '#8B5CF6',
|
||||||
|
currentTime: '-'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Optimization stats
|
||||||
|
const optimizationStats = ref({
|
||||||
|
optimalWindows: 1654,
|
||||||
|
quietHoursConflicts: 23,
|
||||||
|
timezoneCoverage: 18
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test message
|
||||||
|
const testMessage = ref({
|
||||||
|
content: '',
|
||||||
|
scheduledTime: '',
|
||||||
|
type: 'email'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed filtered scheduled messages
|
||||||
|
const filteredScheduledMessages = computed(() => {
|
||||||
|
let filtered = scheduledMessages.value;
|
||||||
|
|
||||||
|
if (selectedTimezone.value) {
|
||||||
|
filtered = filtered.filter(msg => msg.timezone === selectedTimezone.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.map(msg => ({
|
||||||
|
...msg,
|
||||||
|
status: h('span', {
|
||||||
|
class: `px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
msg.status === 'scheduled' ? 'bg-blue-100 text-blue-800' :
|
||||||
|
msg.status === 'sent' ? 'bg-green-100 text-green-800' :
|
||||||
|
'bg-gray-100 text-gray-800'
|
||||||
|
}`
|
||||||
|
}, msg.status)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
function refreshScheduledMessages() {
|
||||||
|
// Mock refresh
|
||||||
|
console.log('Refreshing scheduled messages...');
|
||||||
|
}
|
||||||
|
|
||||||
|
function testTimezoneDelivery() {
|
||||||
|
if (!testMessage.value.content || !testMessage.value.scheduledTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock delivery preview generation
|
||||||
|
deliveryPreview.value = [
|
||||||
|
{
|
||||||
|
timezone: 'America/New_York',
|
||||||
|
users: 1250,
|
||||||
|
localTime: '09:00 AM',
|
||||||
|
status: 'optimal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timezone: 'Europe/London',
|
||||||
|
localTime: '02:00 AM',
|
||||||
|
users: 890,
|
||||||
|
status: 'blocked'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timezone: 'Asia/Tokyo',
|
||||||
|
localTime: '11:00 AM',
|
||||||
|
users: 2100,
|
||||||
|
status: 'optimal'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timezone: 'Australia/Sydney',
|
||||||
|
localTime: '01:00 AM',
|
||||||
|
users: 580,
|
||||||
|
status: 'blocked'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveConfiguration() {
|
||||||
|
// Mock save
|
||||||
|
console.log('Saving timezone configuration...');
|
||||||
|
showConfigModal.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
Loading…
x
Reference in New Issue
Block a user