StatsSection.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. "use client";
  2. import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
  3. import { useEffect } from "react";
  4. import toast from "react-hot-toast";
  5. import { get, post } from "../../lib/api";
  6. import ApiHealth from "./ApiHealth";
  7. export default function StatsSection() {
  8. const queryClient = useQueryClient();
  9. const { data: tasks, isLoading: _tasksLoading } = useQuery({
  10. queryKey: ["tasks"],
  11. queryFn: () => get("/tasks"),
  12. });
  13. const { data: filesSuccessful, isLoading: filesSuccessfulLoading } = useQuery(
  14. {
  15. queryKey: ["files-stats-successful"],
  16. queryFn: () => get("/files/stats/successful"),
  17. }
  18. );
  19. const { data: filesProcessedTotal, isLoading: filesProcessedLoading } =
  20. useQuery({
  21. queryKey: ["files-stats-processed"],
  22. queryFn: () => get("/files/stats/processed"),
  23. });
  24. const { data: _datasets, isLoading: _datasetsLoading } = useQuery({
  25. queryKey: ["datasets"],
  26. queryFn: () => get("/files"),
  27. });
  28. const { data: settings, isLoading: settingsLoading } = useQuery({
  29. queryKey: ["settings", "datasets"],
  30. queryFn: () => get("/config/settings/datasets"),
  31. });
  32. const { data: watcherStatus, isLoading: watcherLoading } = useQuery({
  33. queryKey: ["watcher", "status"],
  34. queryFn: () => get("/watcher/status"),
  35. });
  36. const { data: taskProcessingStatus, isLoading: taskProcessingLoading } =
  37. useQuery({
  38. queryKey: ["tasks", "processing-status"],
  39. queryFn: () => get("/tasks/processing-status"),
  40. });
  41. const { data: queueStatus, isLoading: _queueLoading } = useQuery({
  42. queryKey: ["tasks", "queue", "status"],
  43. queryFn: () => get("/tasks/queue/status"),
  44. });
  45. const { data: apiHealth, isLoading: apiHealthLoading } = useQuery({
  46. queryKey: ["api", "health"],
  47. queryFn: () => get("/health"),
  48. refetchInterval: 30000,
  49. });
  50. // Mutations for controlling services
  51. const startWatcherMutation = useMutation({
  52. mutationFn: () => post("/watcher/start"),
  53. onSuccess: () => {
  54. toast.success("File watcher started");
  55. // Invalidate and refetch to ensure status updates immediately
  56. queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
  57. setTimeout(() => {
  58. queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
  59. }, 100);
  60. },
  61. onError: () => {
  62. toast.error("Failed to start file watcher");
  63. },
  64. });
  65. const stopWatcherMutation = useMutation({
  66. mutationFn: () => post("/watcher/stop"),
  67. onSuccess: () => {
  68. toast.success("File watcher stopped");
  69. // Invalidate and refetch to ensure status updates immediately
  70. queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
  71. setTimeout(() => {
  72. queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
  73. }, 100);
  74. },
  75. onError: () => {
  76. toast.error("Failed to stop file watcher");
  77. },
  78. });
  79. const startTaskProcessingMutation = useMutation({
  80. mutationFn: () => post("/tasks/start-processing"),
  81. onSuccess: () => {
  82. queryClient.invalidateQueries({
  83. queryKey: ["tasks", "processing-status"],
  84. });
  85. queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
  86. toast.success("Task processing started");
  87. },
  88. onError: () => {
  89. toast.error("Failed to start task processing");
  90. },
  91. });
  92. const stopTaskProcessingMutation = useMutation({
  93. mutationFn: () => post("/tasks/stop-processing"),
  94. onSuccess: () => {
  95. queryClient.invalidateQueries({
  96. queryKey: ["tasks", "processing-status"],
  97. });
  98. queryClient.invalidateQueries({ queryKey: ["tasks", "queue", "status"] });
  99. toast.success("Task processing stopped");
  100. },
  101. onError: () => {
  102. toast.error("Failed to stop task processing");
  103. },
  104. });
  105. const _tasksRunning = tasks?.length || 0;
  106. const filesProcessed = filesSuccessful || 0;
  107. const totalProcessed = filesProcessedTotal || 0;
  108. const successRate =
  109. totalProcessed > 0
  110. ? Math.round((filesProcessed / totalProcessed) * 100)
  111. : 0;
  112. const activeWatchers = settings
  113. ? Object.values(settings).filter((dataset: any) => dataset.enabled === true)
  114. .length
  115. : 0;
  116. const isApiHealthy = apiHealth?.status === "healthy";
  117. const isWatcherActive = watcherStatus?.isWatching;
  118. const isTaskProcessingActive = taskProcessingStatus?.isProcessing;
  119. // Listen for WebSocket updates to refresh stats
  120. useEffect(() => {
  121. const handleTaskUpdate = (event: CustomEvent) => {
  122. const taskData = event.detail;
  123. // Refresh task-related queries when tasks are updated
  124. if (
  125. taskData.type === "progress" ||
  126. taskData.type === "completed" ||
  127. taskData.type === "failed"
  128. ) {
  129. queryClient.invalidateQueries({ queryKey: ["tasks"] });
  130. queryClient.invalidateQueries({
  131. queryKey: ["tasks", "processing-status"],
  132. });
  133. queryClient.invalidateQueries({
  134. queryKey: ["tasks", "queue", "status"],
  135. });
  136. }
  137. };
  138. const handleFileUpdate = (event: CustomEvent) => {
  139. const fileData = event.detail;
  140. // Refresh file stats when files are processed
  141. if (fileData.type === "processed" || fileData.type === "success") {
  142. queryClient.invalidateQueries({ queryKey: ["files-stats-successful"] });
  143. queryClient.invalidateQueries({ queryKey: ["files-stats-processed"] });
  144. }
  145. };
  146. window.addEventListener("taskUpdate", handleTaskUpdate as EventListener);
  147. window.addEventListener("fileUpdate", handleFileUpdate as EventListener);
  148. return () => {
  149. window.removeEventListener(
  150. "taskUpdate",
  151. handleTaskUpdate as EventListener
  152. );
  153. window.removeEventListener(
  154. "fileUpdate",
  155. handleFileUpdate as EventListener
  156. );
  157. };
  158. }, [queryClient]);
  159. return (
  160. <div className="space-y-6">
  161. {/* API Health Widget */}
  162. <ApiHealth />
  163. {/* Stats Grid */}
  164. <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
  165. {/* File Watcher */}
  166. <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
  167. <div className="flex items-center justify-between">
  168. <div className="flex items-center gap-x-3">
  169. <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-indigo-500/20 ring-1 ring-indigo-500/30">
  170. <svg
  171. className="h-6 w-6 text-indigo-400"
  172. fill="none"
  173. viewBox="0 0 24 24"
  174. strokeWidth="1.5"
  175. stroke="currentColor"
  176. >
  177. <path
  178. strokeLinecap="round"
  179. strokeLinejoin="round"
  180. d="M2.457 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.543-7z"
  181. />
  182. <path
  183. strokeLinecap="round"
  184. strokeLinejoin="round"
  185. d="M12 9a3 3 0 100 6 3 3 0 000-6z"
  186. />
  187. </svg>
  188. </div>
  189. <div>
  190. <div className="text-2xl font-bold text-white">
  191. {watcherLoading ? (
  192. <div className="flex justify-center">
  193. <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
  194. </div>
  195. ) : (
  196. <span
  197. className={
  198. isWatcherActive ? "text-green-400" : "text-red-400"
  199. }
  200. >
  201. {isWatcherActive ? "Active" : "Idle"}
  202. </span>
  203. )}
  204. </div>
  205. <div className="text-sm font-medium text-gray-400">
  206. File Watcher
  207. </div>
  208. <div className="text-xs text-gray-500 mt-1">
  209. {settingsLoading ? "..." : `${activeWatchers} datasets`}
  210. </div>
  211. </div>
  212. </div>
  213. <div className="flex gap-2">
  214. <button
  215. onClick={() =>
  216. isWatcherActive
  217. ? stopWatcherMutation.mutate()
  218. : startWatcherMutation.mutate()
  219. }
  220. disabled={
  221. watcherLoading ||
  222. startWatcherMutation.isPending ||
  223. stopWatcherMutation.isPending
  224. }
  225. className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
  226. isWatcherActive
  227. ? "bg-red-600 hover:bg-red-700 text-white"
  228. : "bg-green-600 hover:bg-green-700 text-white"
  229. } disabled:opacity-50`}
  230. >
  231. {startWatcherMutation.isPending || stopWatcherMutation.isPending
  232. ? "..."
  233. : isWatcherActive
  234. ? "Stop"
  235. : "Start"}
  236. </button>
  237. </div>
  238. </div>
  239. {watcherStatus?.watches && watcherStatus.watches.length > 0 && (
  240. <div className="mt-4 pt-4 border-t border-white/10">
  241. <div className="text-xs text-gray-400 mb-2">Watching:</div>
  242. <div className="flex flex-wrap gap-1">
  243. {watcherStatus.watches
  244. .slice(0, 3)
  245. .map((watch: any, index: number) => (
  246. <span
  247. key={index}
  248. className="text-xs bg-white/10 px-2 py-1 rounded text-gray-300"
  249. >
  250. {typeof watch === "string"
  251. ? watch.split("/").pop()
  252. : watch.path?.split("/").pop() || "Unknown"}
  253. </span>
  254. ))}
  255. {watcherStatus.watches.length > 3 && (
  256. <span className="text-xs text-gray-500">
  257. +{watcherStatus.watches.length - 3} more
  258. </span>
  259. )}
  260. </div>
  261. </div>
  262. )}
  263. </div>
  264. {/* Task Processing */}
  265. <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
  266. <div className="flex items-center justify-between">
  267. <div className="flex items-center gap-x-3">
  268. <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-emerald-500/20 ring-1 ring-emerald-500/30">
  269. <svg
  270. className="h-6 w-6 text-emerald-400"
  271. fill="none"
  272. viewBox="0 0 24 24"
  273. strokeWidth="1.5"
  274. stroke="currentColor"
  275. >
  276. <path
  277. strokeLinecap="round"
  278. strokeLinejoin="round"
  279. d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
  280. />
  281. </svg>
  282. </div>
  283. <div>
  284. <div className="text-2xl font-bold text-white">
  285. {taskProcessingLoading ? (
  286. <div className="flex justify-center">
  287. <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
  288. </div>
  289. ) : (
  290. <span
  291. className={
  292. isTaskProcessingActive
  293. ? "text-green-400"
  294. : "text-red-400"
  295. }
  296. >
  297. {isTaskProcessingActive ? "Active" : "Idle"}
  298. </span>
  299. )}
  300. </div>
  301. <div className="text-sm font-medium text-gray-400">
  302. Task Processing
  303. </div>
  304. <div className="text-xs text-gray-500 mt-1">
  305. {filesSuccessfulLoading || filesProcessedLoading
  306. ? "..."
  307. : `${successRate}% success`}
  308. </div>
  309. </div>
  310. </div>
  311. <div className="flex gap-2">
  312. <button
  313. onClick={() =>
  314. isTaskProcessingActive
  315. ? stopTaskProcessingMutation.mutate()
  316. : startTaskProcessingMutation.mutate()
  317. }
  318. disabled={
  319. taskProcessingLoading ||
  320. startTaskProcessingMutation.isPending ||
  321. stopTaskProcessingMutation.isPending
  322. }
  323. className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
  324. isTaskProcessingActive
  325. ? "bg-red-600 hover:bg-red-700 text-white"
  326. : "bg-green-600 hover:bg-green-700 text-white"
  327. } disabled:opacity-50`}
  328. >
  329. {startTaskProcessingMutation.isPending ||
  330. stopTaskProcessingMutation.isPending
  331. ? "..."
  332. : isTaskProcessingActive
  333. ? "Stop"
  334. : "Start"}
  335. </button>
  336. </div>
  337. </div>
  338. {queueStatus && (
  339. <div className="mt-4 pt-4 border-t border-white/10">
  340. <div className="grid grid-cols-4 gap-2 text-xs">
  341. <div className="text-center">
  342. <div className="text-gray-400">Pending</div>
  343. <div className="text-white font-medium">
  344. {queueStatus.pending || 0}
  345. </div>
  346. </div>
  347. <div className="text-center">
  348. <div className="text-gray-400">Active</div>
  349. <div className="text-white font-medium">
  350. {queueStatus.processing || 0}
  351. </div>
  352. </div>
  353. <div className="text-center">
  354. <div className="text-gray-400">Done</div>
  355. <div className="text-white font-medium">
  356. {queueStatus.completed || 0}
  357. </div>
  358. </div>
  359. <div className="text-center">
  360. <div className="text-gray-400">Failed</div>
  361. <div className="text-white font-medium">
  362. {queueStatus.failed || 0}
  363. </div>
  364. </div>
  365. </div>
  366. </div>
  367. )}
  368. </div>
  369. {/* API Health */}
  370. <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
  371. <div className="flex items-center gap-x-3">
  372. <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-amber-500/20 ring-1 ring-amber-500/30">
  373. <svg
  374. className="h-6 w-6 text-amber-400"
  375. fill="none"
  376. viewBox="0 0 24 24"
  377. strokeWidth="1.5"
  378. stroke="currentColor"
  379. >
  380. <path
  381. strokeLinecap="round"
  382. strokeLinejoin="round"
  383. d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
  384. />
  385. </svg>
  386. </div>
  387. <div>
  388. <div className="text-2xl font-bold text-white">
  389. {apiHealthLoading ? (
  390. <div className="flex justify-center">
  391. <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
  392. </div>
  393. ) : (
  394. <span
  395. className={isApiHealthy ? "text-green-400" : "text-red-400"}
  396. >
  397. {isApiHealthy ? "Healthy" : "Issues"}
  398. </span>
  399. )}
  400. </div>
  401. <div className="text-sm font-medium text-gray-400">
  402. API Health
  403. </div>
  404. <div className="text-xs text-gray-500 mt-1">
  405. {apiHealth?.datetime
  406. ? new Date(apiHealth.datetime).toLocaleTimeString()
  407. : "Checking..."}
  408. </div>
  409. </div>
  410. </div>
  411. </div>
  412. {/* Files Processed */}
  413. <div className="group relative overflow-hidden rounded-2xl bg-white/5 p-8 ring-1 ring-white/10 transition-all duration-300 hover:bg-white/10 hover:ring-white/20">
  414. <div className="flex items-center gap-x-3">
  415. <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-rose-500/20 ring-1 ring-rose-500/30">
  416. <svg
  417. className="h-6 w-6 text-rose-400"
  418. fill="none"
  419. viewBox="0 0 24 24"
  420. strokeWidth="1.5"
  421. stroke="currentColor"
  422. >
  423. <path
  424. strokeLinecap="round"
  425. strokeLinejoin="round"
  426. d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
  427. />
  428. </svg>
  429. </div>
  430. <div className="flex-1">
  431. <div className="text-2xl font-bold text-white">
  432. {filesSuccessfulLoading ? (
  433. <div className="flex justify-center">
  434. <div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
  435. </div>
  436. ) : (
  437. filesProcessed.toLocaleString()
  438. )}
  439. </div>
  440. <div className="text-sm font-medium text-gray-400">
  441. Files Processed
  442. </div>
  443. {/* Current Task Progress */}
  444. {tasks && tasks.length > 0 && (
  445. <div className="mt-3 pt-3 border-t border-white/10">
  446. {(() => {
  447. const processingTask = tasks.find(
  448. (t: any) => t.status === "processing"
  449. );
  450. if (!processingTask) return null;
  451. const progress = processingTask.progress || 0;
  452. const fileName = processingTask.input
  453. ? processingTask.input.split("/").pop()
  454. : "Unknown file";
  455. return (
  456. <div>
  457. <div className="flex items-center gap-2 mb-2">
  458. <svg
  459. className="h-4 w-4 text-gray-400 flex-shrink-0"
  460. fill="none"
  461. viewBox="0 0 24 24"
  462. strokeWidth="1.5"
  463. stroke="currentColor"
  464. >
  465. <title>{fileName}</title>
  466. <path
  467. strokeLinecap="round"
  468. strokeLinejoin="round"
  469. d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
  470. />
  471. </svg>
  472. <span
  473. className="text-xs text-gray-400 truncate"
  474. title={fileName}
  475. >
  476. Processing...
  477. </span>
  478. </div>
  479. <div className="flex items-center gap-2">
  480. <div className="flex-1 bg-white/10 rounded-full h-2 overflow-hidden">
  481. <div
  482. className="h-full bg-gradient-to-r from-rose-400 to-rose-500 transition-all duration-300"
  483. style={{ width: `${progress}%` }}
  484. />
  485. </div>
  486. <div className="text-xs font-medium text-rose-400 w-8 text-right">
  487. {progress}%
  488. </div>
  489. </div>
  490. </div>
  491. );
  492. })()}
  493. </div>
  494. )}
  495. </div>
  496. </div>
  497. </div>
  498. </div>
  499. </div>
  500. );
  501. }