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