@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,445 +0,0 @@
|
|
|
1
|
-
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
|
2
|
-
import {
|
|
3
|
-
Activity,
|
|
4
|
-
BarChart3,
|
|
5
|
-
CheckCircle,
|
|
6
|
-
Clock,
|
|
7
|
-
Hourglass,
|
|
8
|
-
LineChart,
|
|
9
|
-
RefreshCcw,
|
|
10
|
-
TrendingUp,
|
|
11
|
-
XCircle,
|
|
12
|
-
} from 'lucide-react'
|
|
13
|
-
import React from 'react'
|
|
14
|
-
import {
|
|
15
|
-
Area,
|
|
16
|
-
AreaChart,
|
|
17
|
-
Bar,
|
|
18
|
-
BarChart,
|
|
19
|
-
CartesianGrid,
|
|
20
|
-
Legend,
|
|
21
|
-
ResponsiveContainer,
|
|
22
|
-
Tooltip,
|
|
23
|
-
XAxis,
|
|
24
|
-
YAxis,
|
|
25
|
-
} from 'recharts'
|
|
26
|
-
import { cn } from '../utils'
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* System Metrics Page.
|
|
30
|
-
*
|
|
31
|
-
* Displays detailed performance analytics, throughput trends, and queue
|
|
32
|
-
* distribution charts using interactive visualizations.
|
|
33
|
-
*
|
|
34
|
-
* @public
|
|
35
|
-
* @since 3.0.0
|
|
36
|
-
*/
|
|
37
|
-
export function MetricsPage() {
|
|
38
|
-
const [timeRange, setTimeRange] = React.useState<'15m' | '1h' | '6h' | '24h'>('15m')
|
|
39
|
-
|
|
40
|
-
const { data: throughputData } = useQuery({
|
|
41
|
-
queryKey: ['throughput'],
|
|
42
|
-
queryFn: async () => {
|
|
43
|
-
const res = await fetch('/api/throughput')
|
|
44
|
-
const json = await res.json()
|
|
45
|
-
return json.data || []
|
|
46
|
-
},
|
|
47
|
-
staleTime: Infinity,
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
const { data: historyData } = useQuery<{ history: Record<string, number[]> }>({
|
|
51
|
-
queryKey: ['metrics-history'],
|
|
52
|
-
queryFn: () => fetch('/api/metrics/history').then((res) => res.json()),
|
|
53
|
-
refetchInterval: 30000,
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
const { isPending, data: queueData } = useQuery<{ queues: any[] }>({
|
|
57
|
-
queryKey: ['queues'],
|
|
58
|
-
queryFn: () => fetch('/api/queues').then((res) => res.json()),
|
|
59
|
-
staleTime: Infinity,
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const { data: workerData } = useQuery<{ workers: any[] }>({
|
|
63
|
-
queryKey: ['workers'],
|
|
64
|
-
queryFn: () => fetch('/api/workers').then((res) => res.json()),
|
|
65
|
-
staleTime: Infinity,
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const queryClient = useQueryClient()
|
|
69
|
-
// Live update from global stream
|
|
70
|
-
React.useEffect(() => {
|
|
71
|
-
const handler = (e: CustomEvent) => {
|
|
72
|
-
const stats = e.detail
|
|
73
|
-
if (!stats) {
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
if (stats.queues) {
|
|
77
|
-
queryClient.setQueryData(['queues'], { queues: stats.queues })
|
|
78
|
-
}
|
|
79
|
-
if (stats.workers) {
|
|
80
|
-
queryClient.setQueryData(['workers'], { workers: stats.workers })
|
|
81
|
-
}
|
|
82
|
-
if (stats.throughput) {
|
|
83
|
-
queryClient.setQueryData(['throughput'], stats.throughput)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
window.addEventListener('flux-stats-update', handler as EventListener)
|
|
87
|
-
return () => window.removeEventListener('flux-stats-update', handler as EventListener)
|
|
88
|
-
}, [queryClient])
|
|
89
|
-
|
|
90
|
-
const history = historyData?.history || {}
|
|
91
|
-
const queues = queueData?.queues || []
|
|
92
|
-
const workers = workerData?.workers || []
|
|
93
|
-
|
|
94
|
-
const chartData =
|
|
95
|
-
throughputData?.map((d: any) => ({
|
|
96
|
-
time: d.timestamp,
|
|
97
|
-
value: d.count,
|
|
98
|
-
})) || []
|
|
99
|
-
|
|
100
|
-
// Calculate totals
|
|
101
|
-
const totalWaiting = queues.reduce((acc, q) => acc + q.waiting, 0)
|
|
102
|
-
const totalDelayed = queues.reduce((acc, q) => acc + q.delayed, 0)
|
|
103
|
-
const totalFailed = queues.reduce((acc, q) => acc + q.failed, 0)
|
|
104
|
-
const totalActive = queues.reduce((acc, q) => acc + q.active, 0)
|
|
105
|
-
|
|
106
|
-
// Calculate throughput stats
|
|
107
|
-
const currentThroughput = chartData[chartData.length - 1]?.value || 0
|
|
108
|
-
const avgThroughput =
|
|
109
|
-
chartData.length > 0
|
|
110
|
-
? Math.round(chartData.reduce((acc: number, d: any) => acc + d.value, 0) / chartData.length)
|
|
111
|
-
: 0
|
|
112
|
-
const maxThroughput = chartData.length > 0 ? Math.max(...chartData.map((d: any) => d.value)) : 0
|
|
113
|
-
|
|
114
|
-
// Prepare queue distribution data
|
|
115
|
-
const queueDistribution = queues.slice(0, 10).map((q) => ({
|
|
116
|
-
name: q.name.length > 12 ? `${q.name.slice(0, 12)}...` : q.name,
|
|
117
|
-
waiting: q.waiting,
|
|
118
|
-
delayed: q.delayed,
|
|
119
|
-
failed: q.failed,
|
|
120
|
-
}))
|
|
121
|
-
|
|
122
|
-
// Prepare historical sparkline data
|
|
123
|
-
const historyLabels = Array.from({ length: 15 }, (_, i) => `${14 - i}m ago`)
|
|
124
|
-
const sparklineData = historyLabels.map((label, i) => ({
|
|
125
|
-
time: label,
|
|
126
|
-
waiting: history.waiting?.[i] || 0,
|
|
127
|
-
delayed: history.delayed?.[i] || 0,
|
|
128
|
-
failed: history.failed?.[i] || 0,
|
|
129
|
-
workers: history.workers?.[i] || 0,
|
|
130
|
-
}))
|
|
131
|
-
|
|
132
|
-
if (isPending) {
|
|
133
|
-
return (
|
|
134
|
-
<div className="flex flex-col items-center justify-center p-20 space-y-6">
|
|
135
|
-
<RefreshCcw className="animate-spin text-primary" size={48} />
|
|
136
|
-
<p className="text-muted-foreground font-bold uppercase tracking-[0.3em] text-xs">
|
|
137
|
-
Loading metrics...
|
|
138
|
-
</p>
|
|
139
|
-
</div>
|
|
140
|
-
)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return (
|
|
144
|
-
<div className="space-y-8">
|
|
145
|
-
{/* Header */}
|
|
146
|
-
<div className="flex justify-between items-end">
|
|
147
|
-
<div>
|
|
148
|
-
<h1 className="text-4xl font-black tracking-tighter">System Metrics</h1>
|
|
149
|
-
<p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
|
|
150
|
-
Real-time performance analytics and trends.
|
|
151
|
-
</p>
|
|
152
|
-
</div>
|
|
153
|
-
<div className="flex items-center gap-2">
|
|
154
|
-
{(['15m', '1h', '6h', '24h'] as const).map((range) => (
|
|
155
|
-
<button
|
|
156
|
-
type="button"
|
|
157
|
-
key={range}
|
|
158
|
-
onClick={() => setTimeRange(range)}
|
|
159
|
-
className={cn(
|
|
160
|
-
'px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all',
|
|
161
|
-
timeRange === range
|
|
162
|
-
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
|
|
163
|
-
: 'hover:bg-muted text-muted-foreground'
|
|
164
|
-
)}
|
|
165
|
-
>
|
|
166
|
-
{range}
|
|
167
|
-
</button>
|
|
168
|
-
))}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
{/* Quick Stats */}
|
|
173
|
-
<div className="grid grid-cols-2 lg:grid-cols-6 gap-4">
|
|
174
|
-
<StatCard icon={Hourglass} label="Waiting" value={totalWaiting} color="amber" />
|
|
175
|
-
<StatCard icon={Clock} label="Delayed" value={totalDelayed} color="blue" />
|
|
176
|
-
<StatCard icon={Activity} label="Active" value={totalActive} color="green" />
|
|
177
|
-
<StatCard icon={XCircle} label="Failed" value={totalFailed} color="red" />
|
|
178
|
-
<StatCard icon={CheckCircle} label="Workers" value={workers.length} color="indigo" />
|
|
179
|
-
<StatCard icon={TrendingUp} label="Jobs/min" value={currentThroughput} color="primary" />
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
{/* Throughput Chart */}
|
|
183
|
-
<div className="card-premium p-6">
|
|
184
|
-
<div className="flex justify-between items-start mb-6">
|
|
185
|
-
<div>
|
|
186
|
-
<div className="flex items-center gap-2">
|
|
187
|
-
<LineChart size={20} className="text-primary" />
|
|
188
|
-
<h3 className="text-xl font-bold tracking-tight">Throughput Over Time</h3>
|
|
189
|
-
</div>
|
|
190
|
-
<p className="text-[10px] text-muted-foreground uppercase tracking-[0.2em] font-bold mt-1">
|
|
191
|
-
Jobs processed per minute
|
|
192
|
-
</p>
|
|
193
|
-
</div>
|
|
194
|
-
<div className="text-right">
|
|
195
|
-
<div className="flex gap-6">
|
|
196
|
-
<div>
|
|
197
|
-
<p className="text-[9px] font-black text-muted-foreground/50 uppercase">Current</p>
|
|
198
|
-
<p className="text-2xl font-black text-primary">{currentThroughput}</p>
|
|
199
|
-
</div>
|
|
200
|
-
<div>
|
|
201
|
-
<p className="text-[9px] font-black text-muted-foreground/50 uppercase">Average</p>
|
|
202
|
-
<p className="text-2xl font-black">{avgThroughput}</p>
|
|
203
|
-
</div>
|
|
204
|
-
<div>
|
|
205
|
-
<p className="text-[9px] font-black text-muted-foreground/50 uppercase">Peak</p>
|
|
206
|
-
<p className="text-2xl font-black text-green-500">{maxThroughput}</p>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
<div className="h-[300px]">
|
|
212
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
213
|
-
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
|
214
|
-
<defs>
|
|
215
|
-
<linearGradient id="colorThroughput" x1="0" y1="0" x2="0" y2="1">
|
|
216
|
-
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.5} />
|
|
217
|
-
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
|
218
|
-
</linearGradient>
|
|
219
|
-
</defs>
|
|
220
|
-
<CartesianGrid
|
|
221
|
-
strokeDasharray="3 3"
|
|
222
|
-
vertical={false}
|
|
223
|
-
stroke="hsl(var(--border))"
|
|
224
|
-
opacity={0.3}
|
|
225
|
-
/>
|
|
226
|
-
<XAxis
|
|
227
|
-
dataKey="time"
|
|
228
|
-
axisLine={false}
|
|
229
|
-
tickLine={false}
|
|
230
|
-
tick={{
|
|
231
|
-
fontSize: 10,
|
|
232
|
-
fill: 'hsl(var(--muted-foreground))',
|
|
233
|
-
fontWeight: 700,
|
|
234
|
-
fontFamily: 'Fira Code',
|
|
235
|
-
}}
|
|
236
|
-
/>
|
|
237
|
-
<YAxis
|
|
238
|
-
axisLine={false}
|
|
239
|
-
tickLine={false}
|
|
240
|
-
tick={{
|
|
241
|
-
fontSize: 10,
|
|
242
|
-
fill: 'hsl(var(--muted-foreground))',
|
|
243
|
-
fontWeight: 700,
|
|
244
|
-
fontFamily: 'Fira Code',
|
|
245
|
-
}}
|
|
246
|
-
/>
|
|
247
|
-
<Tooltip
|
|
248
|
-
contentStyle={{
|
|
249
|
-
backgroundColor: 'rgba(9, 9, 11, 0.95)',
|
|
250
|
-
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
251
|
-
borderRadius: '12px',
|
|
252
|
-
fontSize: '11px',
|
|
253
|
-
fontFamily: 'Fira Code',
|
|
254
|
-
backdropFilter: 'blur(8px)',
|
|
255
|
-
}}
|
|
256
|
-
/>
|
|
257
|
-
<Area
|
|
258
|
-
type="monotone"
|
|
259
|
-
dataKey="value"
|
|
260
|
-
stroke="hsl(var(--primary))"
|
|
261
|
-
fillOpacity={1}
|
|
262
|
-
fill="url(#colorThroughput)"
|
|
263
|
-
strokeWidth={3}
|
|
264
|
-
/>
|
|
265
|
-
</AreaChart>
|
|
266
|
-
</ResponsiveContainer>
|
|
267
|
-
</div>
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
{/* Two Column Layout */}
|
|
271
|
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
272
|
-
{/* Queue Distribution */}
|
|
273
|
-
<div className="card-premium p-6">
|
|
274
|
-
<div className="flex items-center gap-2 mb-6">
|
|
275
|
-
<BarChart3 size={20} className="text-primary" />
|
|
276
|
-
<h3 className="text-lg font-bold tracking-tight">Queue Distribution</h3>
|
|
277
|
-
</div>
|
|
278
|
-
<div className="h-[300px]">
|
|
279
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
280
|
-
<BarChart
|
|
281
|
-
data={queueDistribution}
|
|
282
|
-
margin={{ top: 10, right: 10, left: -20, bottom: 40 }}
|
|
283
|
-
>
|
|
284
|
-
<CartesianGrid
|
|
285
|
-
strokeDasharray="3 3"
|
|
286
|
-
vertical={false}
|
|
287
|
-
stroke="hsl(var(--border))"
|
|
288
|
-
opacity={0.5}
|
|
289
|
-
/>
|
|
290
|
-
<XAxis
|
|
291
|
-
dataKey="name"
|
|
292
|
-
axisLine={false}
|
|
293
|
-
tickLine={false}
|
|
294
|
-
tick={{
|
|
295
|
-
fontSize: 9,
|
|
296
|
-
fill: 'hsl(var(--muted-foreground))',
|
|
297
|
-
fontWeight: 700,
|
|
298
|
-
fontFamily: 'Fira Code',
|
|
299
|
-
}}
|
|
300
|
-
angle={-45}
|
|
301
|
-
textAnchor="end"
|
|
302
|
-
/>
|
|
303
|
-
<YAxis
|
|
304
|
-
axisLine={false}
|
|
305
|
-
tickLine={false}
|
|
306
|
-
tick={{
|
|
307
|
-
fontSize: 10,
|
|
308
|
-
fill: 'hsl(var(--muted-foreground))',
|
|
309
|
-
fontWeight: 700,
|
|
310
|
-
fontFamily: 'Fira Code',
|
|
311
|
-
}}
|
|
312
|
-
/>
|
|
313
|
-
<Tooltip
|
|
314
|
-
contentStyle={{
|
|
315
|
-
backgroundColor: 'rgba(9, 9, 11, 0.95)',
|
|
316
|
-
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
317
|
-
borderRadius: '12px',
|
|
318
|
-
fontSize: '11px',
|
|
319
|
-
fontFamily: 'Fira Code',
|
|
320
|
-
backdropFilter: 'blur(8px)',
|
|
321
|
-
}}
|
|
322
|
-
/>
|
|
323
|
-
<Legend iconType="circle" />
|
|
324
|
-
<Bar dataKey="waiting" fill="#F59E0B" name="Waiting" radius={[4, 4, 0, 0]} />
|
|
325
|
-
<Bar dataKey="delayed" fill="#3B82F6" name="Delayed" radius={[4, 4, 0, 0]} />
|
|
326
|
-
<Bar dataKey="failed" fill="#EF4444" name="Failed" radius={[4, 4, 0, 0]} />
|
|
327
|
-
</BarChart>
|
|
328
|
-
</ResponsiveContainer>
|
|
329
|
-
</div>
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
{/* Historical Trends */}
|
|
333
|
-
<div className="card-premium p-6">
|
|
334
|
-
<div className="flex items-center gap-2 mb-6">
|
|
335
|
-
<TrendingUp size={20} className="text-primary" />
|
|
336
|
-
<h3 className="text-lg font-bold tracking-tight">15-Minute Trends</h3>
|
|
337
|
-
</div>
|
|
338
|
-
<div className="h-[300px]">
|
|
339
|
-
<ResponsiveContainer width="100%" height="100%">
|
|
340
|
-
<AreaChart data={sparklineData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
|
341
|
-
<defs>
|
|
342
|
-
<linearGradient id="colorWaiting" x1="0" y1="0" x2="0" y2="1">
|
|
343
|
-
<stop offset="5%" stopColor="hsl(45, 93%, 47%)" stopOpacity={0.3} />
|
|
344
|
-
<stop offset="95%" stopColor="hsl(45, 93%, 47%)" stopOpacity={0} />
|
|
345
|
-
</linearGradient>
|
|
346
|
-
<linearGradient id="colorFailed" x1="0" y1="0" x2="0" y2="1">
|
|
347
|
-
<stop offset="5%" stopColor="hsl(0, 84%, 60%)" stopOpacity={0.3} />
|
|
348
|
-
<stop offset="95%" stopColor="hsl(0, 84%, 60%)" stopOpacity={0} />
|
|
349
|
-
</linearGradient>
|
|
350
|
-
</defs>
|
|
351
|
-
<CartesianGrid
|
|
352
|
-
strokeDasharray="3 3"
|
|
353
|
-
vertical={false}
|
|
354
|
-
stroke="hsl(var(--border))"
|
|
355
|
-
opacity={0.5}
|
|
356
|
-
/>
|
|
357
|
-
<XAxis
|
|
358
|
-
dataKey="time"
|
|
359
|
-
axisLine={false}
|
|
360
|
-
tickLine={false}
|
|
361
|
-
tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
|
|
362
|
-
/>
|
|
363
|
-
<YAxis
|
|
364
|
-
axisLine={false}
|
|
365
|
-
tickLine={false}
|
|
366
|
-
tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
|
|
367
|
-
/>
|
|
368
|
-
<Tooltip
|
|
369
|
-
contentStyle={{
|
|
370
|
-
backgroundColor: 'hsl(var(--card))',
|
|
371
|
-
border: '1px solid hsl(var(--border))',
|
|
372
|
-
borderRadius: '12px',
|
|
373
|
-
fontSize: '12px',
|
|
374
|
-
}}
|
|
375
|
-
/>
|
|
376
|
-
<Area
|
|
377
|
-
type="monotone"
|
|
378
|
-
dataKey="waiting"
|
|
379
|
-
stroke="hsl(45, 93%, 47%)"
|
|
380
|
-
fill="url(#colorWaiting)"
|
|
381
|
-
strokeWidth={2}
|
|
382
|
-
/>
|
|
383
|
-
<Area
|
|
384
|
-
type="monotone"
|
|
385
|
-
dataKey="failed"
|
|
386
|
-
stroke="hsl(0, 84%, 60%)"
|
|
387
|
-
fill="url(#colorFailed)"
|
|
388
|
-
strokeWidth={2}
|
|
389
|
-
/>
|
|
390
|
-
</AreaChart>
|
|
391
|
-
</ResponsiveContainer>
|
|
392
|
-
</div>
|
|
393
|
-
</div>
|
|
394
|
-
</div>
|
|
395
|
-
</div>
|
|
396
|
-
)
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
interface StatCardProps {
|
|
400
|
-
icon: React.ComponentType<{ size?: number; className?: string }>
|
|
401
|
-
label: string
|
|
402
|
-
value: number
|
|
403
|
-
color: 'amber' | 'blue' | 'green' | 'red' | 'indigo' | 'primary'
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function StatCard({ icon: Icon, label, value, color }: StatCardProps) {
|
|
407
|
-
const colorClasses = {
|
|
408
|
-
amber:
|
|
409
|
-
'text-amber-500 bg-amber-500/10 border-amber-500/20 shadow-[0_0_15px_rgba(245,158,11,0.05)]',
|
|
410
|
-
blue: 'text-blue-500 bg-blue-500/10 border-blue-500/20 shadow-[0_0_15px_rgba(59,130,246,0.05)]',
|
|
411
|
-
green:
|
|
412
|
-
'text-emerald-500 bg-emerald-500/10 border-emerald-500/20 shadow-[0_0_15px_rgba(16,185,129,0.05)]',
|
|
413
|
-
red: 'text-red-500 bg-red-500/10 border-red-500/20 shadow-[0_0_15px_rgba(239,68,68,0.05)]',
|
|
414
|
-
indigo:
|
|
415
|
-
'text-indigo-400 bg-indigo-500/10 border-indigo-500/20 shadow-[0_0_15px_rgba(99,102,241,0.05)]',
|
|
416
|
-
primary: 'text-primary bg-primary/10 border-primary/20 shadow-[0_0_15px_rgba(0,240,255,0.05)]',
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return (
|
|
420
|
-
<div
|
|
421
|
-
className={cn(
|
|
422
|
-
'card-premium p-4 flex items-center gap-4 border-l-4',
|
|
423
|
-
colorClasses[color]
|
|
424
|
-
.split(' ')
|
|
425
|
-
.find((c) => c.startsWith('border-'))
|
|
426
|
-
?.replace('border-', 'border-l-')
|
|
427
|
-
)}
|
|
428
|
-
>
|
|
429
|
-
<div
|
|
430
|
-
className={cn(
|
|
431
|
-
'w-10 h-10 rounded-xl flex items-center justify-center border transition-transform group-hover:scale-110',
|
|
432
|
-
colorClasses[color]
|
|
433
|
-
)}
|
|
434
|
-
>
|
|
435
|
-
<Icon size={20} />
|
|
436
|
-
</div>
|
|
437
|
-
<div>
|
|
438
|
-
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] font-heading mb-0.5">
|
|
439
|
-
{label}
|
|
440
|
-
</p>
|
|
441
|
-
<p className="text-xl font-black font-mono tracking-tighter">{value.toLocaleString()}</p>
|
|
442
|
-
</div>
|
|
443
|
-
</div>
|
|
444
|
-
)
|
|
445
|
-
}
|