504 lines
14 KiB
Vue

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