first commit
This commit is contained in:
parent
bcbf2f0958
commit
5275289942
1
.node-version
Normal file
1
.node-version
Normal file
@ -0,0 +1 @@
|
||||
20.11.1
|
183
pages/BF-PRF/AS/QUEUE/index.vue
Normal file
183
pages/BF-PRF/AS/QUEUE/index.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useToast } from 'vue-toastification';
|
||||
|
||||
const toast = useToast();
|
||||
const loading = ref(true);
|
||||
const refreshInterval = ref(null);
|
||||
|
||||
// Use useFetch for queue data
|
||||
const { data: queueData, refresh: refreshQueueData } = await useFetch('/api/queue/asnaf-analysis', {
|
||||
onResponse({ response }) {
|
||||
loading.value = false;
|
||||
},
|
||||
onResponseError({ error }) {
|
||||
console.error('Error fetching queue data:', error);
|
||||
toast.error('Gagal memuat data queue');
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Start auto-refresh
|
||||
onMounted(() => {
|
||||
// Refresh every 30 seconds
|
||||
refreshInterval.value = setInterval(() => {
|
||||
refreshQueueData();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
// Clean up interval on component unmount
|
||||
onUnmounted(() => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value);
|
||||
}
|
||||
});
|
||||
|
||||
// Retry failed job
|
||||
async function retryJob(jobId) {
|
||||
try {
|
||||
await useFetch(`/api/queue/asnaf-analysis/${jobId}/retry`, {
|
||||
method: 'POST',
|
||||
onResponse() {
|
||||
toast.success('Job akan diproses semula');
|
||||
refreshQueueData();
|
||||
},
|
||||
onResponseError({ error }) {
|
||||
console.error('Error retrying job:', error);
|
||||
toast.error('Gagal memproses semula job');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error retrying job:', error);
|
||||
toast.error('Gagal memproses semula job');
|
||||
}
|
||||
}
|
||||
|
||||
// Remove job
|
||||
async function removeJob(jobId) {
|
||||
try {
|
||||
await useFetch(`/api/queue/asnaf-analysis/${jobId}`, {
|
||||
method: 'DELETE',
|
||||
onResponse() {
|
||||
toast.success('Job telah dipadam');
|
||||
refreshQueueData();
|
||||
},
|
||||
onResponseError({ error }) {
|
||||
console.error('Error removing job:', error);
|
||||
toast.error('Gagal memadam job');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error removing job:', error);
|
||||
toast.error('Gagal memadam job');
|
||||
}
|
||||
}
|
||||
|
||||
// Page metadata
|
||||
definePageMeta({
|
||||
title: "Queue Analisis Asnaf",
|
||||
middleware: ["auth"],
|
||||
requiresAuth: true,
|
||||
breadcrumb: [
|
||||
{
|
||||
name: "Dashboard",
|
||||
path: "/",
|
||||
},
|
||||
{
|
||||
name: "BF-PRF",
|
||||
path: "/BF-PRF",
|
||||
},
|
||||
{
|
||||
name: "Asnaf",
|
||||
path: "/BF-PRF/AS",
|
||||
},
|
||||
{
|
||||
name: "Queue",
|
||||
path: "/BF-PRF/AS/QUEUE",
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<LayoutsBreadcrumb />
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-primary">Queue Analisis Asnaf</h1>
|
||||
<rs-button variant="primary" @click="refreshQueueData">
|
||||
<Icon name="mdi:refresh" size="18" class="mr-1" />
|
||||
Refresh
|
||||
</rs-button>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="flex justify-center items-center py-20">
|
||||
<div class="text-center">
|
||||
<Loading />
|
||||
<p class="mt-4 text-gray-600">Memuat data queue...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<rs-card>
|
||||
<template #body>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Asnaf ID</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Updated</th>
|
||||
<th class="px-6 py-3 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tr v-for="job in queueData" :key="job.id" class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ job.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<rs-badge
|
||||
:variant="job.status === 'completed' ? 'success' :
|
||||
job.status === 'failed' ? 'danger' :
|
||||
job.status === 'active' ? 'primary' :
|
||||
'warning'">
|
||||
{{ job.status }}
|
||||
</rs-badge>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ job.data.asnafId }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ new Date(job.timestamp).toLocaleString('ms-MY') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ new Date(job.processedOn || job.finishedOn).toLocaleString('ms-MY') }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<div class="flex space-x-2">
|
||||
<rs-button
|
||||
v-if="job.status === 'failed'"
|
||||
variant="warning"
|
||||
size="sm"
|
||||
@click="retryJob(job.id)">
|
||||
<Icon name="mdi:refresh" size="16" class="mr-1" />
|
||||
Retry
|
||||
</rs-button>
|
||||
<rs-button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
@click="removeJob(job.id)">
|
||||
<Icon name="mdi:delete" size="16" class="mr-1" />
|
||||
Remove
|
||||
</rs-button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</rs-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
45
server/api/queue/asnaf-analysis/index.get.js
Normal file
45
server/api/queue/asnaf-analysis/index.get.js
Normal file
@ -0,0 +1,45 @@
|
||||
import Queue from 'bull';
|
||||
|
||||
// Create a Bull queue instance
|
||||
const asnafAnalysisQueue = new Queue('asnaf-analysis', {
|
||||
redis: {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD
|
||||
}
|
||||
});
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// Get all jobs from different states
|
||||
const [waiting, active, completed, failed] = await Promise.all([
|
||||
asnafAnalysisQueue.getWaiting(),
|
||||
asnafAnalysisQueue.getActive(),
|
||||
asnafAnalysisQueue.getCompleted(),
|
||||
asnafAnalysisQueue.getFailed()
|
||||
]);
|
||||
|
||||
// Combine all jobs and format them
|
||||
const allJobs = [...waiting, ...active, ...completed, ...failed]
|
||||
.map(job => ({
|
||||
id: job.id,
|
||||
status: job.finishedOn ? 'completed' :
|
||||
job.failedReason ? 'failed' :
|
||||
job.processedOn ? 'active' : 'waiting',
|
||||
data: job.data,
|
||||
timestamp: job.timestamp,
|
||||
processedOn: job.processedOn,
|
||||
finishedOn: job.finishedOn,
|
||||
failedReason: job.failedReason
|
||||
}))
|
||||
.sort((a, b) => b.timestamp - a.timestamp); // Sort by timestamp, newest first
|
||||
|
||||
return allJobs;
|
||||
} catch (error) {
|
||||
console.error('Error fetching queue data:', error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: 'Failed to fetch queue data'
|
||||
});
|
||||
}
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user