WatcherStatus.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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 LoadingCard from "./Loading";
  7. import { useNotifications } from "./NotificationContext";
  8. export default function WatcherStatus() {
  9. const queryClient = useQueryClient();
  10. const { addNotification } = useNotifications();
  11. const { data, isLoading, error } = useQuery({
  12. queryKey: ["watcher", "status"],
  13. queryFn: () => get("/watcher/status"),
  14. });
  15. const { data: _datasets, isLoading: _datasetsLoading } = useQuery({
  16. queryKey: ["datasets"],
  17. queryFn: () => get("/files"),
  18. });
  19. const { data: settings } = useQuery({
  20. queryKey: ["settings", "datasets"],
  21. queryFn: () => get("/config/settings/datasets"),
  22. });
  23. const startMutation = useMutation({
  24. mutationFn: () => post("/watcher/start"),
  25. onSuccess: () => {
  26. toast.success("Watcher started successfully");
  27. addNotification({
  28. type: "success",
  29. title: "Watcher Started",
  30. message: "The file watcher has been started successfully.",
  31. });
  32. // Invalidate and refetch to ensure status updates immediately
  33. queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
  34. setTimeout(() => {
  35. queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
  36. }, 100);
  37. },
  38. });
  39. const stopMutation = useMutation({
  40. mutationFn: () => post("/watcher/stop"),
  41. onSuccess: () => {
  42. toast.success("Watcher stopped successfully");
  43. addNotification({
  44. type: "success",
  45. title: "Watcher Stopped",
  46. message: "The file watcher has been stopped successfully.",
  47. });
  48. // Invalidate and refetch to ensure status updates immediately
  49. queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
  50. setTimeout(() => {
  51. queryClient.refetchQueries({ queryKey: ["watcher", "status"] });
  52. }, 100);
  53. },
  54. });
  55. // Listen for WebSocket events
  56. useEffect(() => {
  57. const handleWatcherUpdate = (event: CustomEvent) => {
  58. const updateData = event.detail;
  59. if (updateData.type === "started" || updateData.type === "stopped") {
  60. // Invalidate and refetch the watcher status
  61. queryClient.invalidateQueries({ queryKey: ["watcher", "status"] });
  62. }
  63. };
  64. window.addEventListener(
  65. "watcherUpdate",
  66. handleWatcherUpdate as EventListener
  67. );
  68. return () => {
  69. window.removeEventListener(
  70. "watcherUpdate",
  71. handleWatcherUpdate as EventListener
  72. );
  73. };
  74. }, [queryClient]);
  75. if (isLoading) return <LoadingCard message="Loading watcher status..." />;
  76. if (error) {
  77. return (
  78. <div className="mb-6 p-4 border rounded bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800">
  79. <div className="text-center">
  80. <div className="mb-4">
  81. <svg
  82. className="mx-auto h-12 w-12 text-red-400"
  83. fill="none"
  84. viewBox="0 0 24 24"
  85. stroke="currentColor"
  86. >
  87. <path
  88. strokeLinecap="round"
  89. strokeLinejoin="round"
  90. strokeWidth={2}
  91. d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
  92. />
  93. </svg>
  94. </div>
  95. <h3 className="font-semibold text-red-800 dark:text-red-200 mb-2">
  96. Failed to load watcher status
  97. </h3>
  98. <p className="text-sm text-red-600 dark:text-red-400 mb-4">
  99. Unable to connect to the file watcher service.
  100. </p>
  101. <button
  102. onClick={() =>
  103. queryClient.invalidateQueries({ queryKey: ["watcher", "status"] })
  104. }
  105. className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900 hover:bg-red-200 dark:hover:bg-red-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
  106. >
  107. Retry
  108. </button>
  109. </div>
  110. </div>
  111. );
  112. }
  113. return (
  114. <div className="mb-6 p-4 border rounded bg-gray-50 dark:bg-gray-800">
  115. <div className="flex items-center justify-between mb-4">
  116. <h3 className="font-semibold">File Watcher</h3>
  117. <div className="flex gap-2">
  118. <button
  119. className="px-3 py-1 rounded bg-green-600 text-white disabled:opacity-50 text-sm"
  120. onClick={() => startMutation.mutate()}
  121. disabled={data.isWatching || startMutation.isPending}
  122. >
  123. {startMutation.isPending ? "Starting..." : "Start"}
  124. </button>
  125. <button
  126. className="px-3 py-1 rounded bg-red-600 text-white disabled:opacity-50 text-sm"
  127. onClick={() => stopMutation.mutate()}
  128. disabled={!data.isWatching || stopMutation.isPending}
  129. >
  130. {stopMutation.isPending ? "Stopping..." : "Stop"}
  131. </button>
  132. </div>
  133. </div>
  134. <div className="grid grid-cols-2 gap-4 mb-4 text-sm">
  135. <div>
  136. <span className="font-medium text-gray-700 dark:text-gray-300">
  137. Status:
  138. </span>
  139. <span
  140. className={`ml-2 ${data.isWatching ? "text-green-600" : "text-red-600"}`}
  141. >
  142. {data.isWatching ? "Watching" : "Idle"}
  143. </span>
  144. </div>
  145. <div>
  146. <span className="font-medium text-gray-700 dark:text-gray-300">
  147. Active Watches:
  148. </span>
  149. <span className="ml-2 text-gray-900 dark:text-gray-100">
  150. {settings
  151. ? Object.values(settings).filter(
  152. (dataset: any) => dataset.enabled !== false
  153. ).length
  154. : 0}
  155. </span>
  156. </div>
  157. </div>
  158. {settings && (
  159. <div className="mb-4">
  160. <span className="font-medium text-gray-700 dark:text-gray-300 text-sm">
  161. Configured Datasets ({Object.keys(settings).length}):
  162. </span>
  163. <div className="mt-1 flex flex-wrap gap-1">
  164. {Object.keys(settings).map((datasetName: string) => {
  165. const datasetConfig = settings[datasetName];
  166. const isEnabled = datasetConfig?.enabled !== false;
  167. return (
  168. <span
  169. key={datasetName}
  170. className={`text-xs px-2 py-1 rounded ${
  171. isEnabled
  172. ? "text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900"
  173. : "text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700"
  174. }`}
  175. >
  176. {datasetName}
  177. {!isEnabled && " (disabled)"}
  178. </span>
  179. );
  180. })}
  181. </div>
  182. </div>
  183. )}
  184. {data.watches && data.watches.length > 0 && (
  185. <div className="mb-4">
  186. <span className="font-medium text-gray-700 dark:text-gray-300 text-sm">
  187. Watched Paths:
  188. </span>
  189. <div className="mt-1 max-h-20 overflow-y-auto">
  190. {data.watches.map((watch: any, index: number) => (
  191. <div
  192. key={index}
  193. className="text-xs text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded mt-1"
  194. >
  195. {watch.path || watch}
  196. </div>
  197. ))}
  198. </div>
  199. </div>
  200. )}
  201. </div>
  202. );
  203. }