180 lines
6.6 KiB
Vue
180 lines
6.6 KiB
Vue
<script setup>
|
|
import { defineProps, defineEmits, computed } from 'vue';
|
|
|
|
const props = defineProps({
|
|
job: {
|
|
type: Object,
|
|
required: true,
|
|
},
|
|
show: {
|
|
type: Boolean,
|
|
required: true,
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits(['close']);
|
|
|
|
function formatDate(timestamp) {
|
|
if (!timestamp) return 'N/A';
|
|
const date = new Date(Number(timestamp));
|
|
if (isNaN(date.getTime())) return 'Invalid Date';
|
|
return date.toLocaleString();
|
|
}
|
|
|
|
const jobDuration = computed(() => {
|
|
if (props.job.duration) {
|
|
return (props.job.duration / 1000).toFixed(2) + 's';
|
|
}
|
|
if (props.job.processedOn && props.job.finishedOn) {
|
|
return ((props.job.finishedOn - props.job.processedOn) / 1000).toFixed(2) + 's';
|
|
}
|
|
return 'N/A';
|
|
});
|
|
|
|
const dataEntries = computed(() => {
|
|
if (props.job.data && typeof props.job.data === 'object') {
|
|
return Object.entries(props.job.data);
|
|
}
|
|
return [];
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<transition name="modal-fade">
|
|
<div v-if="show"
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-60 backdrop-blur-sm p-4"
|
|
@click.self="emit('close')">
|
|
<rs-card class="w-full max-w-2xl bg-white shadow-xl rounded-lg max-h-[90vh] flex flex-col">
|
|
<template #header>
|
|
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-xl font-semibold text-gray-800">
|
|
Job Details: <span class="text-primary">#{{ job.id }}</span>
|
|
</h3>
|
|
<button @click="emit('close')" class="text-gray-400 hover:text-gray-600 transition-colors">
|
|
<Icon name="mdi:close" size="24" />
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<template #body>
|
|
<div class="p-6 space-y-5 overflow-y-auto flex-grow">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4 text-sm">
|
|
<div>
|
|
<p class="text-gray-500">Name</p>
|
|
<p class="font-medium text-gray-800">{{ job.name || 'N/A' }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Queue</p>
|
|
<p class="font-medium text-gray-800">{{ job.queue }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Status</p>
|
|
<rs-badge
|
|
:variant="job.state === 'completed' ? 'success' :
|
|
job.state === 'failed' ? 'danger' :
|
|
job.state === 'active' ? 'primary' : 'info'"
|
|
class="text-xs font-semibold"
|
|
>
|
|
{{ job.state }}
|
|
</rs-badge>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Priority</p>
|
|
<p class="font-medium text-gray-800">{{ job.priority }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Attempts Made</p>
|
|
<p class="font-medium text-gray-800">{{ job.attemptsMade }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Duration</p>
|
|
<p class="font-medium text-gray-800">{{ jobDuration }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border-t border-gray-200 pt-4">
|
|
<h4 class="text-md font-semibold text-gray-700 mb-2">Timestamps</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2 text-sm">
|
|
<div>
|
|
<p class="text-gray-500">Created At</p>
|
|
<p class="font-medium text-gray-800">{{ formatDate(job.timestamp) }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Processed At</p>
|
|
<p class="font-medium text-gray-800">{{ formatDate(job.processedOn) }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-gray-500">Finished At</p>
|
|
<p class="font-medium text-gray-800">{{ formatDate(job.finishedOn) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="job.state === 'active' && job.progress > 0" class="border-t border-gray-200 pt-4">
|
|
<h4 class="text-md font-semibold text-gray-700 mb-1">Progress</h4>
|
|
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
|
<div class="bg-blue-600 h-2.5 rounded-full transition-all duration-300 ease-out"
|
|
:style="{ width: job.progress + '%' }">
|
|
</div>
|
|
</div>
|
|
<p class="text-xs text-gray-600 mt-1">{{ job.progress }}% complete</p>
|
|
</div>
|
|
|
|
<div v-if="job.state === 'failed' && job.failedReason" class="border-t border-gray-200 pt-4">
|
|
<h4 class="text-md font-semibold text-red-600 mb-1">Failure Reason</h4>
|
|
<div class="p-3 bg-red-50 border border-red-200 rounded-md text-sm text-red-700 whitespace-pre-wrap">
|
|
{{ job.failedReason }}
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="dataEntries.length > 0" class="border-t border-gray-200 pt-4">
|
|
<h4 class="text-md font-semibold text-gray-700 mb-2">Job Data</h4>
|
|
<div class="p-3 bg-gray-50 border border-gray-200 rounded-md max-h-60 overflow-y-auto text-xs">
|
|
<pre class="whitespace-pre-wrap break-all">{{ JSON.stringify(job.data, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="job.data" class="border-t border-gray-200 pt-4">
|
|
<h4 class="text-md font-semibold text-gray-700 mb-2">Job Data</h4>
|
|
<div class="p-3 bg-gray-50 border border-gray-200 rounded-md text-xs text-gray-500">
|
|
No data or data is not an object.
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<template #footer>
|
|
<div class="px-6 py-3 bg-gray-50 border-t border-gray-200 flex justify-end">
|
|
<rs-button variant="secondary" @click="emit('close')">Close</rs-button>
|
|
</div>
|
|
</template>
|
|
</rs-card>
|
|
</div>
|
|
</transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.modal-fade-enter-active,
|
|
.modal-fade-leave-active {
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.modal-fade-enter-from,
|
|
.modal-fade-leave-to {
|
|
opacity: 0;
|
|
}
|
|
|
|
pre::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
pre::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1; /* cool-gray-300 */
|
|
border-radius: 3px;
|
|
}
|
|
pre::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8; /* cool-gray-400 */
|
|
}
|
|
</style> |