@gravito/zenith 1.1.3 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/dist/bin.js +43235 -76691
- package/dist/client/index.html +13 -0
- package/dist/server/index.js +43235 -76691
- package/package.json +16 -7
- package/CHANGELOG.md +0 -62
- package/Dockerfile +0 -46
- package/Dockerfile.demo-worker +0 -29
- package/bin/flux-console.ts +0 -2
- package/doc/ECOSYSTEM_EXPANSION_RFC.md +0 -130
- package/docker-compose.yml +0 -40
- package/docs/ALERTING_GUIDE.md +0 -71
- package/docs/DEPLOYMENT.md +0 -157
- package/docs/DOCS_INTERNAL.md +0 -73
- package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
- package/docs/QUASAR_MASTER_PLAN.md +0 -140
- package/docs/QUICK_TEST_GUIDE.md +0 -72
- package/docs/ROADMAP.md +0 -85
- package/docs/integrations/LARAVEL.md +0 -207
- package/postcss.config.js +0 -6
- package/scripts/debug_redis_keys.ts +0 -24
- package/scripts/flood-logs.ts +0 -21
- package/scripts/seed.ts +0 -213
- package/scripts/verify-throttle.ts +0 -49
- package/scripts/worker.ts +0 -124
- package/specs/PULSE_SPEC.md +0 -86
- package/src/bin.ts +0 -6
- package/src/client/App.tsx +0 -72
- package/src/client/Layout.tsx +0 -669
- package/src/client/Sidebar.tsx +0 -112
- package/src/client/ThroughputChart.tsx +0 -158
- package/src/client/WorkerStatus.tsx +0 -202
- package/src/client/components/BrandIcons.tsx +0 -168
- package/src/client/components/ConfirmDialog.tsx +0 -134
- package/src/client/components/JobInspector.tsx +0 -487
- package/src/client/components/LogArchiveModal.tsx +0 -432
- package/src/client/components/NotificationBell.tsx +0 -212
- package/src/client/components/PageHeader.tsx +0 -47
- package/src/client/components/Toaster.tsx +0 -90
- package/src/client/components/UserProfileDropdown.tsx +0 -186
- package/src/client/contexts/AuthContext.tsx +0 -105
- package/src/client/contexts/NotificationContext.tsx +0 -128
- package/src/client/index.css +0 -172
- package/src/client/main.tsx +0 -15
- package/src/client/pages/LoginPage.tsx +0 -164
- package/src/client/pages/MetricsPage.tsx +0 -445
- package/src/client/pages/OverviewPage.tsx +0 -519
- package/src/client/pages/PulsePage.tsx +0 -409
- package/src/client/pages/QueuesPage.tsx +0 -378
- package/src/client/pages/SchedulesPage.tsx +0 -535
- package/src/client/pages/SettingsPage.tsx +0 -1001
- package/src/client/pages/WorkersPage.tsx +0 -380
- package/src/client/pages/index.ts +0 -8
- package/src/client/utils.ts +0 -15
- package/src/server/config/ServerConfigManager.ts +0 -90
- package/src/server/index.ts +0 -860
- package/src/server/middleware/auth.ts +0 -127
- package/src/server/services/AlertService.ts +0 -321
- package/src/server/services/CommandService.ts +0 -136
- package/src/server/services/LogStreamProcessor.ts +0 -93
- package/src/server/services/MaintenanceScheduler.ts +0 -78
- package/src/server/services/PulseService.ts +0 -148
- package/src/server/services/QueueMetricsCollector.ts +0 -138
- package/src/server/services/QueueService.ts +0 -924
- package/src/shared/types.ts +0 -223
- package/tailwind.config.js +0 -80
- package/tests/placeholder.test.ts +0 -7
- package/tsconfig.json +0 -29
- package/tsconfig.node.json +0 -10
- package/vite.config.ts +0 -27
|
@@ -1,378 +0,0 @@
|
|
|
1
|
-
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
-
import { AnimatePresence } from 'framer-motion'
|
|
3
|
-
import {
|
|
4
|
-
Activity,
|
|
5
|
-
AlertCircle,
|
|
6
|
-
ArrowRight,
|
|
7
|
-
Filter,
|
|
8
|
-
ListTree,
|
|
9
|
-
Pause,
|
|
10
|
-
Play,
|
|
11
|
-
RefreshCcw,
|
|
12
|
-
Search,
|
|
13
|
-
XCircle,
|
|
14
|
-
} from 'lucide-react'
|
|
15
|
-
import React from 'react'
|
|
16
|
-
import { JobInspector } from '../components/JobInspector'
|
|
17
|
-
import { cn } from '../utils'
|
|
18
|
-
|
|
19
|
-
interface QueueStats {
|
|
20
|
-
name: string
|
|
21
|
-
waiting: number
|
|
22
|
-
delayed: number
|
|
23
|
-
active: number
|
|
24
|
-
failed: number
|
|
25
|
-
paused?: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Queue Management Page.
|
|
30
|
-
*
|
|
31
|
-
* Provides detailed monitoring and management controls for message queues,
|
|
32
|
-
* including job inspection, pausing/resuming queues, and bulk retry/purge actions.
|
|
33
|
-
*
|
|
34
|
-
* @public
|
|
35
|
-
* @since 3.0.0
|
|
36
|
-
*/
|
|
37
|
-
export function QueuesPage() {
|
|
38
|
-
const [selectedQueue, setSelectedQueue] = React.useState<string | null>(null)
|
|
39
|
-
const [searchQuery, setSearchQuery] = React.useState('')
|
|
40
|
-
const [statusFilter, setStatusFilter] = React.useState<'all' | 'active' | 'idle' | 'critical'>(
|
|
41
|
-
'all'
|
|
42
|
-
)
|
|
43
|
-
const queryClient = useQueryClient()
|
|
44
|
-
|
|
45
|
-
const { isPending, error, data } = useQuery<{ queues: QueueStats[] }>({
|
|
46
|
-
queryKey: ['queues'],
|
|
47
|
-
queryFn: () => fetch('/api/queues').then((res) => res.json()),
|
|
48
|
-
staleTime: Infinity, // No auto refetch
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
// Listen for real-time updates from Layout's global stream
|
|
52
|
-
React.useEffect(() => {
|
|
53
|
-
const handler = (e: CustomEvent) => {
|
|
54
|
-
if (e.detail?.queues) {
|
|
55
|
-
queryClient.setQueryData(['queues'], { queues: e.detail.queues })
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
window.addEventListener('flux-stats-update', handler as EventListener)
|
|
59
|
-
return () => window.removeEventListener('flux-stats-update', handler as EventListener)
|
|
60
|
-
}, [queryClient])
|
|
61
|
-
|
|
62
|
-
// Note: We intentionally do NOT scroll to top when JobInspector opens
|
|
63
|
-
// This allows users to quickly inspect multiple queues without losing their scroll position
|
|
64
|
-
|
|
65
|
-
const queues = data?.queues || []
|
|
66
|
-
|
|
67
|
-
const filteredQueues = queues.filter((q) => {
|
|
68
|
-
const matchesSearch = q.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
69
|
-
const status = q.failed > 0 ? 'critical' : q.active > 0 ? 'active' : 'idle'
|
|
70
|
-
const matchesStatus = statusFilter === 'all' || status === statusFilter
|
|
71
|
-
return matchesSearch && matchesStatus
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const totalWaiting = queues.reduce((acc, q) => acc + q.waiting, 0)
|
|
75
|
-
const totalDelayed = queues.reduce((acc, q) => acc + q.delayed, 0)
|
|
76
|
-
const totalFailed = queues.reduce((acc, q) => acc + q.failed, 0)
|
|
77
|
-
const totalActive = queues.reduce((acc, q) => acc + q.active, 0)
|
|
78
|
-
|
|
79
|
-
if (isPending) {
|
|
80
|
-
return (
|
|
81
|
-
<div className="flex flex-col items-center justify-center p-20 space-y-6">
|
|
82
|
-
<RefreshCcw className="animate-spin text-primary" size={48} />
|
|
83
|
-
<p className="text-muted-foreground font-bold uppercase tracking-[0.3em] text-xs">
|
|
84
|
-
Loading queues...
|
|
85
|
-
</p>
|
|
86
|
-
</div>
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (error) {
|
|
91
|
-
return (
|
|
92
|
-
<div className="text-center p-20">
|
|
93
|
-
<div className="bg-red-500/10 text-red-500 p-10 rounded-3xl border border-red-500/20 max-w-md mx-auto shadow-2xl">
|
|
94
|
-
<AlertCircle size={56} className="mx-auto mb-6 opacity-80" />
|
|
95
|
-
<h3 className="text-2xl font-black mb-2 uppercase tracking-tight">
|
|
96
|
-
Failed to Load Queues
|
|
97
|
-
</h3>
|
|
98
|
-
<p className="text-sm font-medium opacity-70 mb-8">{error.message}</p>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<>
|
|
106
|
-
{/* JobInspector as full-screen modal overlay */}
|
|
107
|
-
<AnimatePresence>
|
|
108
|
-
{selectedQueue && (
|
|
109
|
-
<JobInspector queueName={selectedQueue} onClose={() => setSelectedQueue(null)} />
|
|
110
|
-
)}
|
|
111
|
-
</AnimatePresence>
|
|
112
|
-
|
|
113
|
-
{/* Main page content */}
|
|
114
|
-
<div className="space-y-8">
|
|
115
|
-
{/* Header */}
|
|
116
|
-
<div className="flex justify-between items-end">
|
|
117
|
-
<div>
|
|
118
|
-
<h1 className="text-4xl font-black tracking-tighter">Processing Queues</h1>
|
|
119
|
-
<p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
|
|
120
|
-
Manage and monitor all processing pipelines.
|
|
121
|
-
</p>
|
|
122
|
-
</div>
|
|
123
|
-
<div className="flex items-center gap-2 text-[10px] font-black text-green-500 bg-green-500/10 px-4 py-2 rounded-full border border-green-500/20 uppercase tracking-[0.2em] animate-pulse">
|
|
124
|
-
<span className="w-2 h-2 bg-green-500 rounded-full shadow-[0_0_8px_rgba(34,197,94,0.6)]"></span>
|
|
125
|
-
{queues.length} Queues
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
|
|
129
|
-
{/* Summary Cards */}
|
|
130
|
-
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
|
131
|
-
<div className="card-premium p-5 flex flex-col justify-between">
|
|
132
|
-
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] mb-2 font-heading">
|
|
133
|
-
Total Waiting
|
|
134
|
-
</p>
|
|
135
|
-
<p className="text-3xl font-black font-mono tracking-tighter">
|
|
136
|
-
{totalWaiting.toLocaleString()}
|
|
137
|
-
</p>
|
|
138
|
-
</div>
|
|
139
|
-
<div className="card-premium p-5 flex flex-col justify-between">
|
|
140
|
-
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] mb-2 font-heading">
|
|
141
|
-
Total Delayed
|
|
142
|
-
</p>
|
|
143
|
-
<p className="text-3xl font-black text-amber-500 font-mono tracking-tighter">
|
|
144
|
-
{totalDelayed.toLocaleString()}
|
|
145
|
-
</p>
|
|
146
|
-
</div>
|
|
147
|
-
<div className="card-premium p-5 flex flex-col justify-between">
|
|
148
|
-
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] mb-2 font-heading">
|
|
149
|
-
Total Failed
|
|
150
|
-
</p>
|
|
151
|
-
<p className="text-3xl font-black text-red-500 font-mono tracking-tighter">
|
|
152
|
-
{totalFailed.toLocaleString()}
|
|
153
|
-
</p>
|
|
154
|
-
</div>
|
|
155
|
-
<div className="card-premium p-5 flex flex-col justify-between">
|
|
156
|
-
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] mb-2 font-heading">
|
|
157
|
-
Currently Active
|
|
158
|
-
</p>
|
|
159
|
-
<p className="text-3xl font-black text-emerald-500 font-mono tracking-tighter">
|
|
160
|
-
{totalActive.toLocaleString()}
|
|
161
|
-
</p>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
|
|
165
|
-
{/* Filters */}
|
|
166
|
-
<div className="card-premium p-3 flex flex-wrap gap-4 items-center">
|
|
167
|
-
<div className="relative flex-1 min-w-[240px]">
|
|
168
|
-
<Search
|
|
169
|
-
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground/40"
|
|
170
|
-
size={16}
|
|
171
|
-
/>
|
|
172
|
-
<input
|
|
173
|
-
type="text"
|
|
174
|
-
aria-label="Filter pipelines"
|
|
175
|
-
placeholder="Filter pipelines..."
|
|
176
|
-
value={searchQuery}
|
|
177
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
178
|
-
className="w-full bg-black/20 border border-white/5 rounded-lg py-2 pl-10 pr-4 text-xs font-bold placeholder:text-muted-foreground/30 focus:outline-none focus:ring-1 focus:ring-primary/30 transition-all font-mono"
|
|
179
|
-
/>
|
|
180
|
-
</div>
|
|
181
|
-
<div className="flex items-center gap-2">
|
|
182
|
-
<Filter size={14} className="text-muted-foreground/40" />
|
|
183
|
-
{(['all', 'active', 'idle', 'critical'] as const).map((status) => (
|
|
184
|
-
<button
|
|
185
|
-
type="button"
|
|
186
|
-
key={status}
|
|
187
|
-
onClick={() => setStatusFilter(status)}
|
|
188
|
-
className={cn(
|
|
189
|
-
'px-3 py-1.5 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all border',
|
|
190
|
-
statusFilter === status
|
|
191
|
-
? 'bg-primary text-primary-foreground border-primary shadow-lg shadow-primary/20'
|
|
192
|
-
: 'bg-muted/40 text-muted-foreground border-transparent hover:bg-muted/60'
|
|
193
|
-
)}
|
|
194
|
-
>
|
|
195
|
-
{status}
|
|
196
|
-
</button>
|
|
197
|
-
))}
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
|
|
201
|
-
{/* Queue List */}
|
|
202
|
-
<div className="card-premium overflow-hidden">
|
|
203
|
-
<div className="overflow-x-auto">
|
|
204
|
-
<table className="w-full text-left border-collapse">
|
|
205
|
-
<thead className="bg-black/20 text-muted-foreground/60 uppercase text-[9px] font-black tracking-widest border-b border-white/5">
|
|
206
|
-
<tr>
|
|
207
|
-
<th className="px-6 py-4">Pipeline Architecture</th>
|
|
208
|
-
<th className="px-6 py-4 text-center">Waiting</th>
|
|
209
|
-
<th className="px-6 py-4 text-center">Delayed</th>
|
|
210
|
-
<th className="px-6 py-4 text-center">Active</th>
|
|
211
|
-
<th className="px-6 py-4 text-center">Failed</th>
|
|
212
|
-
<th className="px-6 py-4 text-center">Status</th>
|
|
213
|
-
<th className="px-6 py-4 text-right">Operations</th>
|
|
214
|
-
</tr>
|
|
215
|
-
</thead>
|
|
216
|
-
<tbody className="divide-y divide-white/5 text-xs font-mono">
|
|
217
|
-
{filteredQueues.map((queue) => {
|
|
218
|
-
const status =
|
|
219
|
-
queue.failed > 0 ? 'critical' : queue.active > 0 ? 'active' : 'idle'
|
|
220
|
-
return (
|
|
221
|
-
<tr key={queue.name} className="hover:bg-white/[0.02] transition-colors group">
|
|
222
|
-
<td className="px-6 py-4">
|
|
223
|
-
<div className="flex items-center gap-4">
|
|
224
|
-
<div className="w-10 h-10 bg-zinc-800/50 border border-white/5 rounded-xl flex items-center justify-center text-primary group-hover:scale-110 group-hover:shadow-[0_0_15px_rgba(0,240,255,0.2)] transition-all">
|
|
225
|
-
<ListTree size={18} />
|
|
226
|
-
</div>
|
|
227
|
-
<span className="font-black tracking-tight text-sm font-heading">
|
|
228
|
-
{queue.name}
|
|
229
|
-
</span>
|
|
230
|
-
</div>
|
|
231
|
-
</td>
|
|
232
|
-
<td className="px-6 py-4 text-center font-bold text-foreground/80">
|
|
233
|
-
{queue.waiting.toLocaleString()}
|
|
234
|
-
</td>
|
|
235
|
-
<td className="px-6 py-4 text-center text-amber-500/80">{queue.delayed}</td>
|
|
236
|
-
<td className="px-6 py-4 text-center text-emerald-500/80">{queue.active}</td>
|
|
237
|
-
<td className="px-6 py-4 text-center">
|
|
238
|
-
<span
|
|
239
|
-
className={cn(
|
|
240
|
-
'font-black',
|
|
241
|
-
queue.failed > 0 ? 'text-red-500' : 'text-muted-foreground/20'
|
|
242
|
-
)}
|
|
243
|
-
>
|
|
244
|
-
{queue.failed}
|
|
245
|
-
</span>
|
|
246
|
-
</td>
|
|
247
|
-
<td className="px-6 py-4 text-center">
|
|
248
|
-
<span
|
|
249
|
-
className={cn(
|
|
250
|
-
'px-2.5 py-1 rounded-md text-[8px] font-black uppercase tracking-widest border transition-all',
|
|
251
|
-
queue.paused
|
|
252
|
-
? 'bg-amber-500/10 text-amber-500 border-amber-500/20'
|
|
253
|
-
: status === 'critical'
|
|
254
|
-
? 'bg-red-500 text-white border-red-600 shadow-[0_0_10px_rgba(239,68,68,0.3)] animate-pulse'
|
|
255
|
-
: status === 'active'
|
|
256
|
-
? 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20 shadow-[0_0_10px_rgba(16,185,129,0.1)]'
|
|
257
|
-
: 'bg-zinc-800/50 text-muted-foreground/40 border-transparent'
|
|
258
|
-
)}
|
|
259
|
-
>
|
|
260
|
-
{queue.paused ? 'paused' : status}
|
|
261
|
-
</span>
|
|
262
|
-
</td>
|
|
263
|
-
<td className="px-6 py-4 text-right">
|
|
264
|
-
<div className="flex justify-end gap-1.5 items-center">
|
|
265
|
-
{/* Pause/Resume button */}
|
|
266
|
-
<button
|
|
267
|
-
type="button"
|
|
268
|
-
onClick={async () => {
|
|
269
|
-
const action = queue.paused ? 'resume' : 'pause'
|
|
270
|
-
await fetch(`/api/queues/${queue.name}/${action}`, { method: 'POST' })
|
|
271
|
-
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
272
|
-
}}
|
|
273
|
-
className={cn(
|
|
274
|
-
'p-2 rounded-lg transition-all border border-transparent hover:border-white/10',
|
|
275
|
-
queue.paused
|
|
276
|
-
? 'text-emerald-500 bg-emerald-500/5 hover:bg-emerald-500/10'
|
|
277
|
-
: 'text-muted-foreground hover:bg-amber-500/10 hover:text-amber-500'
|
|
278
|
-
)}
|
|
279
|
-
title={queue.paused ? 'Resume Queue' : 'Pause Queue'}
|
|
280
|
-
aria-label={
|
|
281
|
-
queue.paused ? `Resume ${queue.name}` : `Pause ${queue.name}`
|
|
282
|
-
}
|
|
283
|
-
>
|
|
284
|
-
{queue.paused ? <Play size={14} /> : <Pause size={14} />}
|
|
285
|
-
</button>
|
|
286
|
-
|
|
287
|
-
<div className="w-px h-4 bg-white/5 mx-1" />
|
|
288
|
-
|
|
289
|
-
{queue.delayed > 0 && (
|
|
290
|
-
<button
|
|
291
|
-
type="button"
|
|
292
|
-
onClick={() =>
|
|
293
|
-
fetch(`/api/queues/${queue.name}/retry-all`, {
|
|
294
|
-
method: 'POST',
|
|
295
|
-
}).then(() =>
|
|
296
|
-
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
297
|
-
)
|
|
298
|
-
}
|
|
299
|
-
className="p-2 text-amber-500 hover:bg-amber-500/10 rounded-lg transition-all"
|
|
300
|
-
title="Retry All Delayed"
|
|
301
|
-
aria-label={`Retry all delayed jobs in ${queue.name}`}
|
|
302
|
-
>
|
|
303
|
-
<RefreshCcw size={14} />
|
|
304
|
-
</button>
|
|
305
|
-
)}
|
|
306
|
-
{queue.failed > 0 && (
|
|
307
|
-
<>
|
|
308
|
-
<button
|
|
309
|
-
type="button"
|
|
310
|
-
onClick={() =>
|
|
311
|
-
fetch(`/api/queues/${queue.name}/retry-all-failed`, {
|
|
312
|
-
method: 'POST',
|
|
313
|
-
}).then(() =>
|
|
314
|
-
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
className="p-2 text-primary hover:bg-primary/10 rounded-lg transition-all"
|
|
318
|
-
title="Retry All Failed"
|
|
319
|
-
aria-label={`Retry all failed jobs in ${queue.name}`}
|
|
320
|
-
>
|
|
321
|
-
<RefreshCcw size={14} />
|
|
322
|
-
</button>
|
|
323
|
-
<button
|
|
324
|
-
type="button"
|
|
325
|
-
onClick={() => {
|
|
326
|
-
if (
|
|
327
|
-
confirm(
|
|
328
|
-
`Are you sure you want to clear all failed jobs in queue "${queue.name}"?`
|
|
329
|
-
)
|
|
330
|
-
) {
|
|
331
|
-
fetch(`/api/queues/${queue.name}/clear-failed`, {
|
|
332
|
-
method: 'POST',
|
|
333
|
-
}).then(() =>
|
|
334
|
-
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
335
|
-
)
|
|
336
|
-
}
|
|
337
|
-
}}
|
|
338
|
-
className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg transition-all"
|
|
339
|
-
title="Clear Failed Jobs"
|
|
340
|
-
aria-label={`Clear all failed jobs in ${queue.name}`}
|
|
341
|
-
>
|
|
342
|
-
<XCircle size={14} />
|
|
343
|
-
</button>
|
|
344
|
-
</>
|
|
345
|
-
)}
|
|
346
|
-
|
|
347
|
-
<button
|
|
348
|
-
type="button"
|
|
349
|
-
onClick={() => setSelectedQueue(queue.name)}
|
|
350
|
-
className="ml-2 px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-foreground/80 rounded-lg transition-all flex items-center gap-2 text-[9px] font-black uppercase tracking-widest border border-white/5 hover:border-primary/40"
|
|
351
|
-
>
|
|
352
|
-
Inspect <ArrowRight size={10} />
|
|
353
|
-
</button>
|
|
354
|
-
</div>
|
|
355
|
-
</td>
|
|
356
|
-
</tr>
|
|
357
|
-
)
|
|
358
|
-
})}
|
|
359
|
-
{filteredQueues.length === 0 && (
|
|
360
|
-
<tr>
|
|
361
|
-
<td colSpan={7} className="px-6 py-20 text-center text-muted-foreground">
|
|
362
|
-
<Activity size={40} className="mx-auto mb-4 opacity-10 animate-pulse" />
|
|
363
|
-
<p className="text-sm font-bold opacity-30 italic uppercase tracking-widest">
|
|
364
|
-
{searchQuery || statusFilter !== 'all'
|
|
365
|
-
? 'No queues match your filters'
|
|
366
|
-
: 'No queues available'}
|
|
367
|
-
</p>
|
|
368
|
-
</td>
|
|
369
|
-
</tr>
|
|
370
|
-
)}
|
|
371
|
-
</tbody>
|
|
372
|
-
</table>
|
|
373
|
-
</div>
|
|
374
|
-
</div>
|
|
375
|
-
</div>
|
|
376
|
-
</>
|
|
377
|
-
)
|
|
378
|
-
}
|