504 lines
14 KiB
Vue
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>
|