Skip to content

Commit eb4295e

Browse files
committed
chore: wip
1 parent 0c391f4 commit eb4295e

File tree

3 files changed

+772
-120
lines changed

3 files changed

+772
-120
lines changed
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<script lang="ts" setup>
2+
import { ref, onMounted } from 'vue'
3+
import { useHead } from '@vueuse/head'
4+
5+
useHead({
6+
title: 'Dashboard - Jobs History',
7+
})
8+
9+
interface JobHistoryEntry {
10+
id: string
11+
name: string
12+
queue: string
13+
status: 'queued' | 'processing' | 'failed' | 'completed'
14+
attempts: number
15+
runtime?: number
16+
started_at?: string
17+
finished_at?: string
18+
error?: string
19+
payload: any
20+
}
21+
22+
const jobs = ref<JobHistoryEntry[]>([])
23+
const isLoading = ref(true)
24+
const currentPage = ref(1)
25+
const perPage = ref(25)
26+
const totalJobs = ref(0)
27+
const selectedQueue = ref<string>('all')
28+
const selectedStatus = ref<string>('all')
29+
const searchQuery = ref('')
30+
31+
const queues = ['all', 'default', 'high', 'low']
32+
const statuses = ['all', 'queued', 'processing', 'completed', 'failed']
33+
34+
const jobStatusColors: Record<string, string> = {
35+
queued: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/50 ring-blue-600/20',
36+
processing: 'text-yellow-700 dark:text-yellow-300 bg-yellow-50 dark:bg-yellow-900/50 ring-yellow-600/20',
37+
completed: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-900/50 ring-green-600/20',
38+
failed: 'text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-900/50 ring-red-600/20',
39+
}
40+
41+
const getJobStatusColor = (status: string): string => {
42+
return jobStatusColors[status] || 'text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-900/50 ring-gray-600/20'
43+
}
44+
45+
// Mock data for demonstration
46+
onMounted(async () => {
47+
// Simulate API call
48+
await new Promise(resolve => setTimeout(resolve, 1000))
49+
50+
type JobName = 'ProcessPayment' | 'SendWelcomeEmail' | 'GenerateReport' | 'SyncInventory'
51+
type QueueName = 'default' | 'high' | 'low'
52+
type StatusType = 'queued' | 'processing' | 'completed' | 'failed'
53+
54+
const jobNames: readonly JobName[] = ['ProcessPayment', 'SendWelcomeEmail', 'GenerateReport', 'SyncInventory']
55+
const queueNames: readonly QueueName[] = ['default', 'high', 'low']
56+
const statusTypes: readonly StatusType[] = ['queued', 'processing', 'completed', 'failed']
57+
58+
const randomItem = <T>(arr: readonly T[]): T => {
59+
return arr[Math.floor(Math.random() * arr.length)]!
60+
}
61+
62+
jobs.value = Array.from({ length: 100 }, (_, i) => ({
63+
id: `${i + 1}`,
64+
name: randomItem(jobNames),
65+
queue: randomItem(queueNames),
66+
status: randomItem(statusTypes),
67+
attempts: Math.floor(Math.random() * 3) + 1,
68+
runtime: Math.random() * 10,
69+
started_at: new Date(Date.now() - Math.random() * 86400000).toISOString(),
70+
finished_at: new Date(Date.now() - Math.random() * 3600000).toISOString(),
71+
payload: { data: 'sample' },
72+
}))
73+
74+
totalJobs.value = jobs.value.length
75+
isLoading.value = false
76+
})
77+
78+
const filteredJobs = computed(() => {
79+
return jobs.value.filter((job) => {
80+
if (selectedQueue.value !== 'all' && job.queue !== selectedQueue.value) return false
81+
if (selectedStatus.value !== 'all' && job.status !== selectedStatus.value) return false
82+
if (searchQuery.value && !job.name.toLowerCase().includes(searchQuery.value.toLowerCase())) return false
83+
return true
84+
})
85+
})
86+
87+
const paginatedJobs = computed(() => {
88+
const start = (currentPage.value - 1) * perPage.value
89+
const end = start + perPage.value
90+
return filteredJobs.value.slice(start, end)
91+
})
92+
93+
const totalPages = computed(() => Math.ceil(filteredJobs.value.length / perPage.value))
94+
95+
const handleRetry = async (jobId: string) => {
96+
// Implement retry logic
97+
console.log('Retrying job:', jobId)
98+
}
99+
</script>
100+
101+
<template>
102+
<div class="min-h-screen py-4 dark:bg-blue-gray-800 lg:py-8">
103+
<div class="px-4 sm:px-6 lg:px-8">
104+
<div class="sm:flex sm:items-center">
105+
<div class="sm:flex-auto">
106+
<h1 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100">Jobs History</h1>
107+
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">
108+
A complete history of all jobs processed by the system
109+
</p>
110+
</div>
111+
</div>
112+
113+
<!-- Filters -->
114+
<div class="mt-4 flex flex-col space-y-4 sm:flex-row sm:items-center sm:space-x-4 sm:space-y-0">
115+
<div class="flex-1 min-w-0">
116+
<label for="search" class="sr-only">Search jobs</label>
117+
<div class="relative rounded-md shadow-sm">
118+
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
119+
<div class="i-heroicons-magnifying-glass h-5 w-5 text-gray-400" />
120+
</div>
121+
<input
122+
v-model="searchQuery"
123+
type="search"
124+
name="search"
125+
id="search"
126+
class="block w-full rounded-md border-0 py-1.5 pl-10 text-gray-900 dark:text-gray-100 dark:bg-blue-gray-600 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
127+
placeholder="Search jobs..."
128+
>
129+
</div>
130+
</div>
131+
132+
<div class="sm:flex-shrink-0">
133+
<select
134+
v-model="selectedQueue"
135+
class="h-9 w-full rounded-md border-0 py-1.5 pl-3 pr-8 text-gray-900 dark:text-gray-100 dark:bg-blue-gray-600 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
136+
>
137+
<option v-for="queue in queues" :key="queue" :value="queue">
138+
{{ queue.charAt(0).toUpperCase() + queue.slice(1) }} Queue
139+
</option>
140+
</select>
141+
</div>
142+
143+
<div class="sm:flex-shrink-0">
144+
<select
145+
v-model="selectedStatus"
146+
class="h-9 w-full rounded-md border-0 py-1.5 pl-3 pr-8 text-gray-900 dark:text-gray-100 dark:bg-blue-gray-600 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 focus:ring-2 focus:ring-blue-600 sm:text-sm sm:leading-6"
147+
>
148+
<option v-for="status in statuses" :key="status" :value="status">
149+
{{ status.charAt(0).toUpperCase() + status.slice(1) }}
150+
</option>
151+
</select>
152+
</div>
153+
</div>
154+
155+
<!-- Table -->
156+
<div class="mt-8 flow-root">
157+
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
158+
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
159+
<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 dark:ring-opacity-20 rounded-lg">
160+
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
161+
<thead class="bg-gray-50 dark:bg-blue-gray-600">
162+
<tr>
163+
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-6">Job</th>
164+
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Queue</th>
165+
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Status</th>
166+
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Attempts</th>
167+
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Runtime</th>
168+
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Started</th>
169+
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
170+
<span class="sr-only">Actions</span>
171+
</th>
172+
</tr>
173+
</thead>
174+
<tbody class="divide-y divide-gray-200 dark:divide-gray-600 bg-white dark:bg-blue-gray-700">
175+
<tr v-if="isLoading" class="animate-pulse">
176+
<td colspan="7" class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
177+
Loading jobs...
178+
</td>
179+
</tr>
180+
<tr v-else-if="paginatedJobs.length === 0" class="hover:bg-gray-50 dark:hover:bg-blue-gray-600/50">
181+
<td colspan="7" class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center">
182+
No jobs found matching your criteria
183+
</td>
184+
</tr>
185+
<tr v-for="job in paginatedJobs" :key="job.id" class="hover:bg-gray-50 dark:hover:bg-blue-gray-600/50">
186+
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
187+
<div class="font-medium text-gray-900 dark:text-gray-100">{{ job.name }}</div>
188+
<div class="text-gray-500 dark:text-gray-400 font-mono text-xs">{{ job.id }}</div>
189+
</td>
190+
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ job.queue }}</td>
191+
<td class="whitespace-nowrap px-3 py-4 text-sm">
192+
<span class="inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ring-1 ring-inset"
193+
:class="getJobStatusColor(job.status)">
194+
{{ job.status }}
195+
</span>
196+
</td>
197+
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ job.attempts }}</td>
198+
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400 font-mono">
199+
{{ job.runtime ? `${job.runtime.toFixed(1)}s` : '-' }}
200+
</td>
201+
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">{{ job.started_at }}</td>
202+
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
203+
<button
204+
v-if="job.status === 'failed'"
205+
type="button"
206+
class="text-blue-600 hover:text-blue-900 dark:hover:text-blue-400"
207+
@click="handleRetry(job.id)"
208+
>
209+
Retry
210+
</button>
211+
</td>
212+
</tr>
213+
</tbody>
214+
</table>
215+
</div>
216+
</div>
217+
</div>
218+
</div>
219+
220+
<!-- Pagination -->
221+
<div class="mt-4 flex items-center justify-between">
222+
<div class="flex items-center">
223+
<p class="text-sm text-gray-700 dark:text-gray-300">
224+
Showing
225+
<span class="font-medium">{{ (currentPage - 1) * perPage + 1 }}</span>
226+
to
227+
<span class="font-medium">{{ Math.min(currentPage * perPage, filteredJobs.length) }}</span>
228+
of
229+
<span class="font-medium">{{ filteredJobs.length }}</span>
230+
results
231+
</p>
232+
</div>
233+
<div class="flex items-center space-x-2">
234+
<button
235+
type="button"
236+
class="relative inline-flex items-center rounded-md bg-white dark:bg-blue-gray-600 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-blue-gray-500 focus:z-10"
237+
:disabled="currentPage === 1"
238+
@click="currentPage--"
239+
>
240+
Previous
241+
</button>
242+
<button
243+
type="button"
244+
class="relative inline-flex items-center rounded-md bg-white dark:bg-blue-gray-600 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-blue-gray-500 focus:z-10"
245+
:disabled="currentPage === totalPages"
246+
@click="currentPage++"
247+
>
248+
Next
249+
</button>
250+
</div>
251+
</div>
252+
</div>
253+
</div>
254+
</template>

0 commit comments

Comments
 (0)