@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,134 +0,0 @@
|
|
|
1
|
-
import { AnimatePresence, motion } from 'framer-motion'
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
3
|
-
import { cn } from '../utils'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Props for the ConfirmDialog component.
|
|
7
|
-
*
|
|
8
|
-
* @public
|
|
9
|
-
* @since 3.0.0
|
|
10
|
-
*/
|
|
11
|
-
export interface ConfirmDialogProps {
|
|
12
|
-
/** Whether the dialog is visible. */
|
|
13
|
-
open: boolean
|
|
14
|
-
/** Dialog title text. */
|
|
15
|
-
title: string
|
|
16
|
-
/** Detailed confirmation message. */
|
|
17
|
-
message: string
|
|
18
|
-
/** Text for the confirmation button. @default 'Confirm' */
|
|
19
|
-
confirmText?: string
|
|
20
|
-
/** Text for the cancel button. @default 'Cancel' */
|
|
21
|
-
cancelText?: string
|
|
22
|
-
/** Callback triggered when user confirms the action. */
|
|
23
|
-
onConfirm: () => void
|
|
24
|
-
/** Callback triggered when user cancels the action. */
|
|
25
|
-
onCancel: () => void
|
|
26
|
-
/** Visual style of the confirmation button. @default 'danger' */
|
|
27
|
-
variant?: 'danger' | 'warning' | 'info'
|
|
28
|
-
/** Whether an action is currently in progress (shows a spinner). @default false */
|
|
29
|
-
isProcessing?: boolean
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* A modal dialog used for user confirmation before performing sensitive actions.
|
|
34
|
-
*
|
|
35
|
-
* It provides a consistent UI for confirmations across the Zenith dashboard
|
|
36
|
-
* and supports different visual variants.
|
|
37
|
-
*
|
|
38
|
-
* @public
|
|
39
|
-
* @since 3.0.0
|
|
40
|
-
*/
|
|
41
|
-
export function ConfirmDialog({
|
|
42
|
-
open,
|
|
43
|
-
title,
|
|
44
|
-
message,
|
|
45
|
-
confirmText = 'Confirm',
|
|
46
|
-
cancelText = 'Cancel',
|
|
47
|
-
onConfirm,
|
|
48
|
-
onCancel,
|
|
49
|
-
variant = 'danger',
|
|
50
|
-
isProcessing = false,
|
|
51
|
-
}: ConfirmDialogProps) {
|
|
52
|
-
return createPortal(
|
|
53
|
-
<AnimatePresence>
|
|
54
|
-
{open && (
|
|
55
|
-
// biome-ignore lint/a11y/noStaticElementInteractions: Backdrop needs click handler to stop propagation
|
|
56
|
-
<div
|
|
57
|
-
role="presentation"
|
|
58
|
-
className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-[5000] pointer-events-auto"
|
|
59
|
-
onClick={(e) => e.stopPropagation()}
|
|
60
|
-
onKeyDown={(e) => e.stopPropagation()}
|
|
61
|
-
>
|
|
62
|
-
<motion.div
|
|
63
|
-
initial={{ scale: 0.95, opacity: 0, y: 20 }}
|
|
64
|
-
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
65
|
-
exit={{ scale: 0.95, opacity: 0, y: 20 }}
|
|
66
|
-
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
|
67
|
-
className="bg-zinc-900 border border-white/10 rounded-3xl p-8 max-w-md shadow-[0_0_50px_rgba(0,0,0,0.8)] scanline overflow-hidden"
|
|
68
|
-
onClick={(e) => e.stopPropagation()}
|
|
69
|
-
>
|
|
70
|
-
<h3 className="text-2xl font-black mb-3 font-heading tracking-tight text-white uppercase italic italic">
|
|
71
|
-
{title}
|
|
72
|
-
</h3>
|
|
73
|
-
<div className="h-px w-full bg-white/5 mb-6" />
|
|
74
|
-
<p className="text-[13px] font-bold text-muted-foreground mb-8 leading-relaxed uppercase tracking-wide opacity-80">
|
|
75
|
-
{message}
|
|
76
|
-
</p>
|
|
77
|
-
<div className="flex gap-4 justify-end">
|
|
78
|
-
<button
|
|
79
|
-
type="button"
|
|
80
|
-
onClick={(e) => {
|
|
81
|
-
e.stopPropagation()
|
|
82
|
-
onCancel()
|
|
83
|
-
}}
|
|
84
|
-
disabled={isProcessing}
|
|
85
|
-
className="px-6 py-3 bg-zinc-800 text-white/60 rounded-xl hover:bg-zinc-700 transition-all disabled:opacity-20 disabled:cursor-not-allowed text-[10px] font-black uppercase tracking-[0.2em] font-heading border border-white/5"
|
|
86
|
-
>
|
|
87
|
-
{cancelText}
|
|
88
|
-
</button>
|
|
89
|
-
<button
|
|
90
|
-
type="button"
|
|
91
|
-
onClick={(e) => {
|
|
92
|
-
e.stopPropagation()
|
|
93
|
-
onConfirm()
|
|
94
|
-
}}
|
|
95
|
-
disabled={isProcessing}
|
|
96
|
-
className={cn(
|
|
97
|
-
'px-6 py-3 rounded-xl text-black transition-all disabled:opacity-20 disabled:cursor-not-allowed flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] font-heading shadow-lg',
|
|
98
|
-
variant === 'danger' &&
|
|
99
|
-
'bg-red-500 shadow-[0_0_20px_rgba(239,68,68,0.3)] hover:bg-red-400',
|
|
100
|
-
variant === 'warning' &&
|
|
101
|
-
'bg-amber-500 shadow-[0_0_20px_rgba(245,158,11,0.3)] hover:bg-amber-400',
|
|
102
|
-
variant === 'info' &&
|
|
103
|
-
'bg-primary shadow-[0_0_20px_rgba(0,240,255,0.3)] hover:bg-primary/80'
|
|
104
|
-
)}
|
|
105
|
-
>
|
|
106
|
-
{isProcessing && (
|
|
107
|
-
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" aria-label="Loading">
|
|
108
|
-
<title>Loading</title>
|
|
109
|
-
<circle
|
|
110
|
-
className="opacity-25"
|
|
111
|
-
cx="12"
|
|
112
|
-
cy="12"
|
|
113
|
-
r="10"
|
|
114
|
-
stroke="currentColor"
|
|
115
|
-
strokeWidth="4"
|
|
116
|
-
fill="none"
|
|
117
|
-
/>
|
|
118
|
-
<path
|
|
119
|
-
className="opacity-75"
|
|
120
|
-
fill="currentColor"
|
|
121
|
-
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
122
|
-
/>
|
|
123
|
-
</svg>
|
|
124
|
-
)}
|
|
125
|
-
{isProcessing ? 'Executing...' : confirmText}
|
|
126
|
-
</button>
|
|
127
|
-
</div>
|
|
128
|
-
</motion.div>
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
131
|
-
</AnimatePresence>,
|
|
132
|
-
document.body
|
|
133
|
-
)
|
|
134
|
-
}
|
|
@@ -1,487 +0,0 @@
|
|
|
1
|
-
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
-
import { motion } from 'framer-motion'
|
|
3
|
-
import { AlertCircle, CheckCircle2, Clock, RefreshCcw, Search } from 'lucide-react'
|
|
4
|
-
import React from 'react'
|
|
5
|
-
import { createPortal } from 'react-dom'
|
|
6
|
-
import { cn } from '../utils'
|
|
7
|
-
import { ConfirmDialog } from './ConfirmDialog'
|
|
8
|
-
|
|
9
|
-
interface Job {
|
|
10
|
-
id: string
|
|
11
|
-
name?: string
|
|
12
|
-
data?: any
|
|
13
|
-
status?: string
|
|
14
|
-
timestamp?: number
|
|
15
|
-
scheduledAt?: string
|
|
16
|
-
error?: string
|
|
17
|
-
failedAt?: number
|
|
18
|
-
_raw?: string
|
|
19
|
-
_archived?: boolean
|
|
20
|
-
_status?: 'completed' | 'failed'
|
|
21
|
-
_archivedAt?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Props for the JobInspector component.
|
|
26
|
-
*
|
|
27
|
-
* @public
|
|
28
|
-
* @since 3.0.0
|
|
29
|
-
*/
|
|
30
|
-
export interface JobInspectorProps {
|
|
31
|
-
/** The name of the queue to inspect. */
|
|
32
|
-
queueName: string
|
|
33
|
-
/** Callback triggered when the inspector is closed. */
|
|
34
|
-
onClose: () => void
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A detailed view for inspecting jobs within a specific queue.
|
|
39
|
-
*
|
|
40
|
-
* It provides tabs for viewing waiting, delayed, and failed jobs,
|
|
41
|
-
* as well as an archive search. Users can perform bulk actions like
|
|
42
|
-
* deleting or retrying jobs.
|
|
43
|
-
*
|
|
44
|
-
* @public
|
|
45
|
-
* @since 3.0.0
|
|
46
|
-
*/
|
|
47
|
-
export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
48
|
-
const [view, setView] = React.useState<'waiting' | 'delayed' | 'failed' | 'archive'>('waiting')
|
|
49
|
-
const [page, setPage] = React.useState(1)
|
|
50
|
-
const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set())
|
|
51
|
-
const [isProcessing, setIsProcessing] = React.useState(false)
|
|
52
|
-
const [confirmDialog, setConfirmDialog] = React.useState<{
|
|
53
|
-
open: boolean
|
|
54
|
-
title: string
|
|
55
|
-
message: string
|
|
56
|
-
action: () => void
|
|
57
|
-
variant?: 'danger' | 'warning' | 'info'
|
|
58
|
-
} | null>(null)
|
|
59
|
-
|
|
60
|
-
const queryClient = useQueryClient()
|
|
61
|
-
|
|
62
|
-
const { isPending, error, data } = useQuery<{ jobs: Job[]; total?: number }>({
|
|
63
|
-
queryKey: ['jobs', queueName, view, page],
|
|
64
|
-
queryFn: () => {
|
|
65
|
-
const url =
|
|
66
|
-
view === 'archive'
|
|
67
|
-
? `/api/queues/${queueName}/archive?page=${page}&limit=50`
|
|
68
|
-
: `/api/queues/${queueName}/jobs?type=${view}`
|
|
69
|
-
return fetch(url).then((res) => res.json())
|
|
70
|
-
},
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
// Reset selection when view changes
|
|
74
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: We want to reset when view changes
|
|
75
|
-
React.useEffect(() => {
|
|
76
|
-
setSelectedIndices(new Set())
|
|
77
|
-
setPage(1)
|
|
78
|
-
}, [view])
|
|
79
|
-
|
|
80
|
-
const toggleSelection = (index: number) => {
|
|
81
|
-
const next = new Set(selectedIndices)
|
|
82
|
-
if (next.has(index)) {
|
|
83
|
-
next.delete(index)
|
|
84
|
-
} else {
|
|
85
|
-
next.add(index)
|
|
86
|
-
}
|
|
87
|
-
setSelectedIndices(next)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const toggleSelectAll = React.useCallback(() => {
|
|
91
|
-
if (!data?.jobs) {
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
const availableCount = data.jobs.filter((j) => j._raw && !j._archived).length
|
|
95
|
-
if (selectedIndices.size === availableCount && availableCount > 0) {
|
|
96
|
-
setSelectedIndices(new Set())
|
|
97
|
-
} else {
|
|
98
|
-
const indices = new Set<number>()
|
|
99
|
-
data.jobs.forEach((j, i) => {
|
|
100
|
-
if (j._raw && !j._archived) {
|
|
101
|
-
indices.add(i)
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
setSelectedIndices(indices)
|
|
105
|
-
}
|
|
106
|
-
}, [data?.jobs, selectedIndices])
|
|
107
|
-
|
|
108
|
-
// Keyboard shortcuts
|
|
109
|
-
React.useEffect(() => {
|
|
110
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
111
|
-
if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
|
|
112
|
-
e.preventDefault()
|
|
113
|
-
toggleSelectAll()
|
|
114
|
-
}
|
|
115
|
-
if (e.key === 'Escape') {
|
|
116
|
-
if (confirmDialog?.open) {
|
|
117
|
-
setConfirmDialog(null)
|
|
118
|
-
} else if (selectedIndices.size > 0) {
|
|
119
|
-
setSelectedIndices(new Set())
|
|
120
|
-
} else {
|
|
121
|
-
onClose()
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
window.addEventListener('keydown', handleKeyDown)
|
|
127
|
-
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
128
|
-
}, [selectedIndices, confirmDialog, toggleSelectAll, onClose])
|
|
129
|
-
|
|
130
|
-
// Lock body scroll when modal opens
|
|
131
|
-
React.useEffect(() => {
|
|
132
|
-
const originalOverflow = document.body.style.overflow
|
|
133
|
-
const originalPaddingRight = document.body.style.paddingRight
|
|
134
|
-
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
|
|
135
|
-
|
|
136
|
-
document.body.style.overflow = 'hidden'
|
|
137
|
-
document.body.style.paddingRight = `${scrollbarWidth}px`
|
|
138
|
-
|
|
139
|
-
return () => {
|
|
140
|
-
document.body.style.overflow = originalOverflow
|
|
141
|
-
document.body.style.paddingRight = originalPaddingRight
|
|
142
|
-
}
|
|
143
|
-
}, [])
|
|
144
|
-
|
|
145
|
-
const handleAction = async (action: 'delete' | 'retry', job: Job) => {
|
|
146
|
-
if (!job._raw) {
|
|
147
|
-
return
|
|
148
|
-
}
|
|
149
|
-
const endpoint = action === 'delete' ? 'delete' : 'retry'
|
|
150
|
-
const body: any = { raw: job._raw }
|
|
151
|
-
if (action === 'delete') {
|
|
152
|
-
body.type = view
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
await fetch(`/api/queues/${queueName}/jobs/${endpoint}`, {
|
|
156
|
-
method: 'POST',
|
|
157
|
-
headers: { 'Content-Type': 'application/json' },
|
|
158
|
-
body: JSON.stringify(body),
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
queryClient.invalidateQueries({ queryKey: ['jobs', queueName] })
|
|
162
|
-
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const handleBulkAction = async (action: 'delete' | 'retry') => {
|
|
166
|
-
if (selectedIndices.size === 0 || !data?.jobs) {
|
|
167
|
-
return
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const count = selectedIndices.size
|
|
171
|
-
setConfirmDialog({
|
|
172
|
-
open: true,
|
|
173
|
-
title: `${action === 'delete' ? 'Delete' : 'Retry'} ${count} Jobs?`,
|
|
174
|
-
message: `Are you sure you want to ${action} ${count} selected ${view} jobs in "${queueName}"?\n\nThis action cannot be undone.`,
|
|
175
|
-
variant: action === 'delete' ? 'danger' : 'warning',
|
|
176
|
-
action: async () => {
|
|
177
|
-
setIsProcessing(true)
|
|
178
|
-
try {
|
|
179
|
-
const endpoint = action === 'delete' ? 'bulk-delete' : 'bulk-retry'
|
|
180
|
-
const raws = Array.from(selectedIndices)
|
|
181
|
-
.map((i) => data?.jobs[i]?._raw)
|
|
182
|
-
.filter(Boolean) as string[]
|
|
183
|
-
|
|
184
|
-
await fetch(`/api/queues/${queueName}/jobs/${endpoint}`, {
|
|
185
|
-
method: 'POST',
|
|
186
|
-
headers: { 'Content-Type': 'application/json' },
|
|
187
|
-
body: JSON.stringify({ type: view, raws }),
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
setSelectedIndices(new Set())
|
|
191
|
-
queryClient.invalidateQueries({ queryKey: ['jobs', queueName] })
|
|
192
|
-
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
193
|
-
setConfirmDialog(null)
|
|
194
|
-
} catch (err) {
|
|
195
|
-
console.error(`Failed to ${action} jobs:`, err)
|
|
196
|
-
} finally {
|
|
197
|
-
setIsProcessing(false)
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return createPortal(
|
|
204
|
-
<div className="fixed inset-0 z-[1001] flex items-center justify-end p-4 sm:p-6 outline-none pointer-events-none">
|
|
205
|
-
<motion.div
|
|
206
|
-
initial={{ opacity: 0 }}
|
|
207
|
-
animate={{ opacity: 1 }}
|
|
208
|
-
exit={{ opacity: 0 }}
|
|
209
|
-
className="absolute inset-0 bg-black/60 backdrop-blur-md cursor-default pointer-events-auto"
|
|
210
|
-
onClick={onClose}
|
|
211
|
-
/>
|
|
212
|
-
|
|
213
|
-
<motion.div
|
|
214
|
-
initial={{ x: '100%', opacity: 0 }}
|
|
215
|
-
animate={{ x: 0, opacity: 1 }}
|
|
216
|
-
exit={{ x: '100%', opacity: 0 }}
|
|
217
|
-
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
|
218
|
-
className="bg-zinc-950 border-l border-white/10 h-screen w-full max-w-2xl shadow-2xl flex flex-col overflow-hidden relative z-[1002] pointer-events-auto"
|
|
219
|
-
onClick={(e) => e.stopPropagation()}
|
|
220
|
-
>
|
|
221
|
-
<div className="p-8 border-b border-white/5 bg-black/40 flex justify-between items-center flex-shrink-0">
|
|
222
|
-
<div>
|
|
223
|
-
<h2 className="text-2xl font-black flex items-center gap-3 font-heading tracking-tight italic uppercase">
|
|
224
|
-
<Search className="text-primary" size={24} />
|
|
225
|
-
Inspector <span className="text-primary/60">/</span> {queueName}
|
|
226
|
-
</h2>
|
|
227
|
-
<div className="flex items-center gap-3 mt-4">
|
|
228
|
-
{(['waiting', 'delayed', 'failed', 'archive'] as const).map((v) => (
|
|
229
|
-
<button
|
|
230
|
-
type="button"
|
|
231
|
-
key={v}
|
|
232
|
-
onClick={() => setView(v)}
|
|
233
|
-
className={cn(
|
|
234
|
-
'text-[9px] font-black px-3 py-1.5 rounded-lg transition-all border shrink-0 uppercase tracking-[0.2em] font-mono',
|
|
235
|
-
view === v
|
|
236
|
-
? v === 'failed'
|
|
237
|
-
? 'bg-red-500 text-black border-red-500 shadow-[0_0_20px_rgba(239,68,68,0.2)]'
|
|
238
|
-
: v === 'delayed'
|
|
239
|
-
? 'bg-amber-500 text-black border-amber-500 shadow-[0_0_20px_rgba(245,158,11,0.2)]'
|
|
240
|
-
: v === 'archive'
|
|
241
|
-
? 'bg-indigo-500 text-black border-indigo-500 shadow-[0_0_20px_rgba(99,102,241,0.2)]'
|
|
242
|
-
: 'bg-primary text-black border-primary shadow-[0_0_20px_rgba(0,240,255,0.2)]'
|
|
243
|
-
: 'bg-zinc-900 text-muted-foreground border-white/5 hover:bg-zinc-800'
|
|
244
|
-
)}
|
|
245
|
-
>
|
|
246
|
-
{v}
|
|
247
|
-
</button>
|
|
248
|
-
))}
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
251
|
-
<button
|
|
252
|
-
type="button"
|
|
253
|
-
onClick={onClose}
|
|
254
|
-
aria-label="Close"
|
|
255
|
-
className="w-12 h-12 rounded-2xl bg-white/5 border border-white/5 hover:bg-white/10 flex items-center justify-center transition-all text-white/40 hover:text-white"
|
|
256
|
-
>
|
|
257
|
-
✕
|
|
258
|
-
</button>
|
|
259
|
-
</div>
|
|
260
|
-
|
|
261
|
-
<div className="flex-1 overflow-y-auto bg-black/20 min-h-0 scrollbar-thin">
|
|
262
|
-
{isPending && (
|
|
263
|
-
<div className="p-20 text-center flex flex-col items-center gap-4">
|
|
264
|
-
<RefreshCcw className="animate-spin text-primary opacity-40" size={32} />
|
|
265
|
-
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-muted-foreground animate-pulse">
|
|
266
|
-
Syncing jobs...
|
|
267
|
-
</p>
|
|
268
|
-
</div>
|
|
269
|
-
)}
|
|
270
|
-
{error && (
|
|
271
|
-
<div className="p-20 text-center">
|
|
272
|
-
<div className="bg-red-500/10 text-red-500 p-8 rounded-2xl border border-red-500/20 font-black uppercase text-xs tracking-widest italic">
|
|
273
|
-
Connection Fault: {error.message}
|
|
274
|
-
</div>
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
|
|
278
|
-
{data?.jobs && data.jobs.length > 0 && (
|
|
279
|
-
<div className="px-8 py-4 border-b border-white/5 bg-white/[0.02] flex items-center gap-4">
|
|
280
|
-
<input
|
|
281
|
-
type="checkbox"
|
|
282
|
-
aria-label="Select all jobs"
|
|
283
|
-
className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
|
|
284
|
-
checked={
|
|
285
|
-
selectedIndices.size === data.jobs.filter((j) => j._raw && !j._archived).length &&
|
|
286
|
-
selectedIndices.size > 0
|
|
287
|
-
}
|
|
288
|
-
onChange={toggleSelectAll}
|
|
289
|
-
/>
|
|
290
|
-
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 font-heading">
|
|
291
|
-
Batch Operations
|
|
292
|
-
</span>
|
|
293
|
-
{selectedIndices.size > 0 && (
|
|
294
|
-
<div className="ml-auto flex items-center gap-3">
|
|
295
|
-
<span className="text-[10px] font-black uppercase text-primary font-mono bg-primary/10 px-2 py-0.5 rounded border border-primary/20">
|
|
296
|
-
{selectedIndices.size} Selected
|
|
297
|
-
</span>
|
|
298
|
-
<button
|
|
299
|
-
type="button"
|
|
300
|
-
onClick={() => handleBulkAction('delete')}
|
|
301
|
-
className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg text-[10px] font-black uppercase hover:bg-red-500 hover:text-white transition-all border border-red-500/20"
|
|
302
|
-
>
|
|
303
|
-
Delete
|
|
304
|
-
</button>
|
|
305
|
-
{(view === 'delayed' || view === 'failed') && (
|
|
306
|
-
<button
|
|
307
|
-
type="button"
|
|
308
|
-
onClick={() => handleBulkAction('retry')}
|
|
309
|
-
className="px-3 py-1.5 bg-primary/10 text-primary rounded-lg text-[10px] font-black uppercase hover:bg-primary hover:text-black transition-all border border-primary/20"
|
|
310
|
-
>
|
|
311
|
-
Retry
|
|
312
|
-
</button>
|
|
313
|
-
)}
|
|
314
|
-
</div>
|
|
315
|
-
)}
|
|
316
|
-
</div>
|
|
317
|
-
)}
|
|
318
|
-
|
|
319
|
-
{data?.jobs && data.jobs.length === 0 && (
|
|
320
|
-
<div className="p-20 text-center text-muted-foreground flex flex-col items-center gap-6 opacity-40">
|
|
321
|
-
<div className="w-20 h-20 bg-white/5 rounded-full flex items-center justify-center text-primary/40 border border-white/5">
|
|
322
|
-
<CheckCircle2 size={40} />
|
|
323
|
-
</div>
|
|
324
|
-
<div className="space-y-2">
|
|
325
|
-
<p className="text-xl font-black font-heading uppercase italic tracking-widest">
|
|
326
|
-
Pipeline Clear
|
|
327
|
-
</p>
|
|
328
|
-
<p className="text-[10px] font-black uppercase tracking-[0.2em]">
|
|
329
|
-
Zero incidents detected in spectrum
|
|
330
|
-
</p>
|
|
331
|
-
</div>
|
|
332
|
-
</div>
|
|
333
|
-
)}
|
|
334
|
-
|
|
335
|
-
{data?.jobs && (
|
|
336
|
-
<div className="p-8 space-y-6">
|
|
337
|
-
{data.jobs.map((job, i) => (
|
|
338
|
-
<div
|
|
339
|
-
key={i}
|
|
340
|
-
className={cn(
|
|
341
|
-
'bg-zinc-900/40 border rounded-2xl overflow-hidden transition-all group border-white/5',
|
|
342
|
-
selectedIndices.has(i) && 'ring-2 ring-primary border-primary bg-primary/5'
|
|
343
|
-
)}
|
|
344
|
-
>
|
|
345
|
-
<div className="p-4 border-b border-white/5 bg-black/40 flex justify-between items-center text-[10px] font-mono">
|
|
346
|
-
<div className="flex items-center gap-4">
|
|
347
|
-
{job._raw && !job._archived && (
|
|
348
|
-
<input
|
|
349
|
-
type="checkbox"
|
|
350
|
-
aria-label={`Select job ${job.id || i}`}
|
|
351
|
-
className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
|
|
352
|
-
checked={selectedIndices.has(i)}
|
|
353
|
-
onChange={() => toggleSelection(i)}
|
|
354
|
-
/>
|
|
355
|
-
)}
|
|
356
|
-
<span className="bg-primary/10 text-primary px-2 py-1 rounded-md font-black uppercase tracking-widest flex items-center gap-2 border border-primary/20 shadow-[0_0_15px_rgba(0,240,255,0.05)]">
|
|
357
|
-
ID:{job.id || 'N/A'}
|
|
358
|
-
{job._archived && (
|
|
359
|
-
<span
|
|
360
|
-
className={cn(
|
|
361
|
-
'px-1.5 py-0.5 rounded text-[8px] border ml-1',
|
|
362
|
-
job._status === 'completed'
|
|
363
|
-
? 'bg-green-500/20 text-green-500 border-green-500/20'
|
|
364
|
-
: 'bg-red-500/20 text-red-500 border-red-500/20'
|
|
365
|
-
)}
|
|
366
|
-
>
|
|
367
|
-
{job._status}
|
|
368
|
-
</span>
|
|
369
|
-
)}
|
|
370
|
-
</span>
|
|
371
|
-
</div>
|
|
372
|
-
<span className="text-white/20 font-black flex items-center gap-4 uppercase tracking-tighter">
|
|
373
|
-
{view === 'delayed' && job.scheduledAt && (
|
|
374
|
-
<span className="text-amber-500 flex items-center gap-1.5">
|
|
375
|
-
<Clock size={12} /> {new Date(job.scheduledAt).toLocaleString()}
|
|
376
|
-
</span>
|
|
377
|
-
)}
|
|
378
|
-
{view === 'failed' && job.failedAt && (
|
|
379
|
-
<span className="text-red-500 flex items-center gap-1.5">
|
|
380
|
-
<AlertCircle size={12} /> {new Date(job.failedAt).toLocaleString()}
|
|
381
|
-
</span>
|
|
382
|
-
)}
|
|
383
|
-
{job.timestamp &&
|
|
384
|
-
!job._archivedAt &&
|
|
385
|
-
new Date(job.timestamp).toLocaleString()}
|
|
386
|
-
</span>
|
|
387
|
-
</div>
|
|
388
|
-
<button
|
|
389
|
-
type="button"
|
|
390
|
-
onClick={() => job._raw && !job._archived && toggleSelection(i)}
|
|
391
|
-
className="w-full text-left cursor-pointer focus:outline-none focus:ring-inset"
|
|
392
|
-
>
|
|
393
|
-
{job.error && (
|
|
394
|
-
<div className="p-5 bg-red-500/10 text-red-500 text-xs font-black border-b border-red-500/10 flex items-start gap-3 uppercase tracking-tight">
|
|
395
|
-
<AlertCircle size={16} className="mt-0.5 shrink-0" />
|
|
396
|
-
<p>{job.error}</p>
|
|
397
|
-
</div>
|
|
398
|
-
)}
|
|
399
|
-
<pre className="text-[11px] font-mono p-6 overflow-x-auto text-white/60 leading-relaxed bg-black/40">
|
|
400
|
-
{JSON.stringify(job, null, 2)}
|
|
401
|
-
</pre>
|
|
402
|
-
</button>
|
|
403
|
-
<div className="p-4 bg-black/20 border-t border-white/5 flex justify-end gap-3">
|
|
404
|
-
{!job._archived && (
|
|
405
|
-
<button
|
|
406
|
-
type="button"
|
|
407
|
-
onClick={() => handleAction('delete', job)}
|
|
408
|
-
className="text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl hover:bg-red-500/10 text-red-500/60 hover:text-red-500 transition-all font-heading border border-transparent hover:border-red-500/20"
|
|
409
|
-
>
|
|
410
|
-
Terminate
|
|
411
|
-
</button>
|
|
412
|
-
)}
|
|
413
|
-
{!job._archived && (view === 'delayed' || view === 'failed') && (
|
|
414
|
-
<button
|
|
415
|
-
type="button"
|
|
416
|
-
onClick={() => handleAction('retry', job)}
|
|
417
|
-
className={cn(
|
|
418
|
-
'text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl text-black shadow-lg transition-all font-heading',
|
|
419
|
-
view === 'delayed'
|
|
420
|
-
? 'bg-amber-500 shadow-amber-500/20 hover:bg-amber-400'
|
|
421
|
-
: 'bg-primary shadow-primary/20 hover:bg-primary/80'
|
|
422
|
-
)}
|
|
423
|
-
>
|
|
424
|
-
{view === 'delayed' ? 'Execute Now' : 'Re-Run Cycle'}
|
|
425
|
-
</button>
|
|
426
|
-
)}
|
|
427
|
-
</div>
|
|
428
|
-
</div>
|
|
429
|
-
))}
|
|
430
|
-
|
|
431
|
-
{view === 'archive' && data?.total && data.total > 50 && (
|
|
432
|
-
<div className="flex items-center justify-between py-6 border-t border-border/30">
|
|
433
|
-
<p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">
|
|
434
|
-
Total {data.total} archived jobs
|
|
435
|
-
</p>
|
|
436
|
-
<div className="flex items-center gap-2">
|
|
437
|
-
<button
|
|
438
|
-
type="button"
|
|
439
|
-
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
440
|
-
disabled={page === 1}
|
|
441
|
-
aria-label="Previous page"
|
|
442
|
-
className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
|
|
443
|
-
>
|
|
444
|
-
←
|
|
445
|
-
</button>
|
|
446
|
-
<span className="text-xs font-bold px-4">{page}</span>
|
|
447
|
-
<button
|
|
448
|
-
type="button"
|
|
449
|
-
onClick={() => setPage((p) => p + 1)}
|
|
450
|
-
disabled={page * 50 >= (data.total || 0)}
|
|
451
|
-
aria-label="Next page"
|
|
452
|
-
className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
|
|
453
|
-
>
|
|
454
|
-
→
|
|
455
|
-
</button>
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
)}
|
|
459
|
-
</div>
|
|
460
|
-
)}
|
|
461
|
-
</div>
|
|
462
|
-
<div className="p-4 border-t bg-card text-right flex-shrink-0">
|
|
463
|
-
<button
|
|
464
|
-
type="button"
|
|
465
|
-
onClick={onClose}
|
|
466
|
-
className="px-8 py-3 bg-muted text-foreground rounded-xl hover:bg-muted/80 text-sm font-bold transition-all active:scale-95 uppercase tracking-widest"
|
|
467
|
-
>
|
|
468
|
-
Dismiss
|
|
469
|
-
</button>
|
|
470
|
-
</div>
|
|
471
|
-
</motion.div>
|
|
472
|
-
|
|
473
|
-
{confirmDialog && (
|
|
474
|
-
<ConfirmDialog
|
|
475
|
-
open={confirmDialog.open}
|
|
476
|
-
title={confirmDialog.title}
|
|
477
|
-
message={confirmDialog.message}
|
|
478
|
-
variant={confirmDialog.variant}
|
|
479
|
-
isProcessing={isProcessing}
|
|
480
|
-
onConfirm={confirmDialog.action}
|
|
481
|
-
onCancel={() => setConfirmDialog(null)}
|
|
482
|
-
/>
|
|
483
|
-
)}
|
|
484
|
-
</div>,
|
|
485
|
-
document.body
|
|
486
|
-
)
|
|
487
|
-
}
|