Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
339
pages/notification/queue/monitor.vue
Normal file
339
pages/notification/queue/monitor.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<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>
|
||||
<rs-button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@click="refreshData"
|
||||
:loading="isLoading"
|
||||
>
|
||||
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
|
||||
Refresh
|
||||
</rs-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<p class="text-gray-600">Monitor current notification queues and job statuses.</p>
|
||||
</template>
|
||||
</rs-card>
|
||||
|
||||
<!-- Error Alert -->
|
||||
<rs-alert v-if="error" variant="danger" class="mb-6">
|
||||
{{ error }}
|
||||
</rs-alert>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="isLoading" class="mb-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<rs-card v-for="i in 4" :key="i">
|
||||
<div class="p-4 text-center">
|
||||
<div class="h-8 bg-gray-200 rounded w-24 mx-auto mb-2 animate-pulse"></div>
|
||||
<div class="h-4 bg-gray-200 rounded w-32 mx-auto animate-pulse"></div>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Stats -->
|
||||
<div v-else class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<rs-card>
|
||||
<div class="p-4 text-center">
|
||||
<h3 class="text-2xl font-bold text-blue-600">{{ stats.pending }}</h3>
|
||||
<p class="text-sm text-gray-600">Pending</p>
|
||||
</div>
|
||||
</rs-card>
|
||||
<rs-card>
|
||||
<div class="p-4 text-center">
|
||||
<h3 class="text-2xl font-bold text-yellow-600">
|
||||
{{ stats.processing || 0 }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">Processing</p>
|
||||
</div>
|
||||
</rs-card>
|
||||
<rs-card>
|
||||
<div class="p-4 text-center">
|
||||
<h3 class="text-2xl font-bold text-green-600">
|
||||
{{ stats.completed }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600">Completed</p>
|
||||
</div>
|
||||
</rs-card>
|
||||
<rs-card>
|
||||
<div class="p-4 text-center">
|
||||
<h3 class="text-2xl font-bold text-red-600">{{ stats.failed }}</h3>
|
||||
<p class="text-sm text-gray-600">Failed</p>
|
||||
</div>
|
||||
</rs-card>
|
||||
</div>
|
||||
|
||||
<!-- Job List -->
|
||||
<rs-card>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold text-primary">Recent Jobs</h3>
|
||||
<div>
|
||||
<select
|
||||
v-model="statusFilter"
|
||||
class="px-2 py-1 border border-gray-300 rounded-md text-sm mr-2"
|
||||
@change="fetchJobs()"
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
<option value="queued">Pending</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body>
|
||||
<div v-if="isLoadingJobs" class="p-8 text-center">
|
||||
<div
|
||||
class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"
|
||||
></div>
|
||||
<p class="mt-2 text-gray-600">Loading jobs...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="jobs.length === 0" class="p-8 text-center">
|
||||
<Icon name="ic:outline-info" class="text-3xl text-gray-400 mb-2" />
|
||||
<p class="text-gray-600">No jobs found</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="(job, index) in jobs"
|
||||
: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="getStatusColor(job.status)"
|
||||
></div>
|
||||
<div>
|
||||
<p class="font-medium">{{ job.type }} - {{ truncateId(job.id) }}</p>
|
||||
<p class="text-sm text-gray-600">{{ job.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium capitalize">{{ job.status }}</p>
|
||||
<p class="text-xs text-gray-500">{{ formatTime(job.createdAt) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div v-if="pagination.totalPages > 1" class="flex justify-center mt-4">
|
||||
<button
|
||||
class="px-3 py-1 border border-gray-300 rounded-l-md disabled:opacity-50"
|
||||
:disabled="pagination.page === 1"
|
||||
@click="changePage(pagination.page - 1)"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<div class="px-3 py-1 border-t border-b border-gray-300 text-sm">
|
||||
{{ pagination.page }} / {{ pagination.totalPages }}
|
||||
</div>
|
||||
<button
|
||||
class="px-3 py-1 border border-gray-300 rounded-r-md disabled:opacity-50"
|
||||
:disabled="pagination.page === pagination.totalPages"
|
||||
@click="changePage(pagination.page + 1)"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
title: "Queue Monitor",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Notification",
|
||||
path: "/notification",
|
||||
},
|
||||
{
|
||||
name: "Queue",
|
||||
path: "/notification/queue",
|
||||
},
|
||||
{
|
||||
name: "Monitor",
|
||||
path: "/notification/queue/monitor",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Loading and error state
|
||||
const isLoading = ref(false);
|
||||
const isLoadingJobs = ref(false);
|
||||
const error = ref(null);
|
||||
const statusFilter = ref("");
|
||||
|
||||
// Stats data
|
||||
const stats = ref({
|
||||
pending: 0,
|
||||
processing: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
});
|
||||
|
||||
// Jobs data with pagination
|
||||
const jobs = ref([]);
|
||||
const pagination = ref({
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
totalItems: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
// Fetch stats from API
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
|
||||
const { data } = await useFetch("/api/notifications/queue/stats");
|
||||
|
||||
if (!data.value) throw new Error("Failed to fetch statistics");
|
||||
|
||||
if (data.value?.success) {
|
||||
stats.value = {
|
||||
pending: data.value.data.pending || 0,
|
||||
processing: data.value.data.processing || 0,
|
||||
completed: data.value.data.completed || 0,
|
||||
failed: data.value.data.failed || 0,
|
||||
};
|
||||
} else {
|
||||
throw new Error(data.value.statusMessage || "Failed to fetch statistics");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching stats:", error);
|
||||
error.value = `Failed to load statistics: ${error.message}`;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch jobs from API
|
||||
const fetchJobs = async () => {
|
||||
try {
|
||||
isLoadingJobs.value = true;
|
||||
|
||||
const params = {
|
||||
page: pagination.value.page,
|
||||
limit: 10,
|
||||
};
|
||||
|
||||
if (statusFilter.value) {
|
||||
params.status = statusFilter.value;
|
||||
}
|
||||
|
||||
const { data } = await useFetch("/api/notifications/queue/jobs", {
|
||||
query: params,
|
||||
});
|
||||
|
||||
if (!data.value) throw new Error("Failed to fetch jobs");
|
||||
|
||||
if (data.value?.success) {
|
||||
jobs.value = data.value.data.jobs;
|
||||
pagination.value = data.value.data.pagination;
|
||||
} else {
|
||||
throw new Error(data.value.statusMessage || "Failed to fetch jobs");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching jobs:", error);
|
||||
error.value = `Failed to load jobs: ${error.message}`;
|
||||
jobs.value = [];
|
||||
} finally {
|
||||
isLoadingJobs.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Change page
|
||||
const changePage = (page) => {
|
||||
pagination.value.page = page;
|
||||
fetchJobs();
|
||||
};
|
||||
|
||||
// Status color mapping
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
queued: "bg-blue-500",
|
||||
pending: "bg-blue-500",
|
||||
processing: "bg-yellow-500",
|
||||
completed: "bg-green-500",
|
||||
sent: "bg-green-500",
|
||||
failed: "bg-red-500",
|
||||
};
|
||||
return colors[status] || "bg-gray-500";
|
||||
};
|
||||
|
||||
// Format time for display
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return "Unknown";
|
||||
|
||||
try {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
|
||||
if (isNaN(diffMins)) return "Unknown";
|
||||
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins} min ago`;
|
||||
if (diffMins < 1440) {
|
||||
const hours = Math.floor(diffMins / 60);
|
||||
return `${hours} hour${hours > 1 ? "s" : ""} ago`;
|
||||
}
|
||||
|
||||
return date.toLocaleString();
|
||||
} catch (e) {
|
||||
console.error("Error formatting time:", e);
|
||||
return "Unknown";
|
||||
}
|
||||
};
|
||||
|
||||
// Truncate long IDs
|
||||
const truncateId = (id) => {
|
||||
if (!id) return "Unknown";
|
||||
if (id.length > 8) {
|
||||
return id.substring(0, 8) + "...";
|
||||
}
|
||||
return id;
|
||||
};
|
||||
|
||||
// Refresh data
|
||||
const refreshData = async () => {
|
||||
await Promise.all([fetchStats(), fetchJobs()]);
|
||||
};
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
let refreshInterval;
|
||||
onMounted(() => {
|
||||
refreshData();
|
||||
refreshInterval = setInterval(refreshData, 30000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
Reference in New Issue
Block a user