Browse Source

refactor: migrate list and CRUD components to use centralized AppContext

- Update SettingsList to use AppContext for settings data and mutations
- Update SettingsCrud to use AppContext.updateSetting instead of direct API calls
- Update DatasetsSettingsEditor to use AppContext.datasetsConfig and updateDatasets
- Update FileCrud to use AppContext.datasets from global state
- Update FileList to use AppContext.datasets instead of duplicate useQuery
- Remove redundant React Query hooks for settings, datasets, and datasets list
- Consolidate all mutations to go through AppContext providers
- Maintain automatic WebSocket synchronization through context

Benefits:
- Single source of truth for application state
- Reduced API calls and duplicate queries
- Faster data access via context instead of queries
- Automatic synchronization across all components via WebSocket events
- Simplified component code with fewer hooks and mutations
Timothy Pomeroy 1 tháng trước cách đây
mục cha
commit
67d7d40276

+ 68 - 52
apps/web/src/app/components/DatasetsSettingsEditor.tsx

@@ -1,6 +1,7 @@
 "use client";
 import { useCallback, useMemo, useState } from "react";
 import toast from "react-hot-toast";
+import { useAppContext } from "../providers/AppContext";
 import DatasetCrud from "./DatasetCrud";
 import { useNotifications } from "./NotificationContext";
 
@@ -12,31 +13,25 @@ interface DatasetsSettings {
   [datasetName: string]: DatasetConfig;
 }
 
-interface DatasetsSettingsEditorProps {
-  value: string;
-  onChange: (value: string) => void;
-}
-
-export default function DatasetsSettingsEditor({
-  value,
-  onChange
-}: DatasetsSettingsEditorProps) {
+export default function DatasetsSettingsEditor() {
+  const { datasetsConfig, updateDatasets } = useAppContext();
   const [isDatasetFormOpen, setIsDatasetFormOpen] = useState(false);
   const [editingDataset, setEditingDataset] = useState<string | null>(null);
   const [isJsonMode, setIsJsonMode] = useState(false);
   const { addNotification } = useNotifications();
 
-  // Derive settings from the prop value to avoid state sync issues
+  // Derive settings from context
   const settings = useMemo<DatasetsSettings>(() => {
+    return datasetsConfig || {};
+  }, [datasetsConfig]);
+
+  const handleUpdateDatasets = async (newSettings: DatasetsSettings) => {
     try {
-      return JSON.parse(value);
-    } catch {
-      return {};
+      await updateDatasets(newSettings);
+    } catch (error) {
+      console.error("Failed to update datasets:", error);
+      throw error;
     }
-  }, [value]);
-
-  const updateSettings = (newSettings: DatasetsSettings) => {
-    onChange(JSON.stringify(newSettings, null, 2));
   };
 
   const handleAddDataset = () => {
@@ -49,18 +44,23 @@ export default function DatasetsSettingsEditor({
     setIsDatasetFormOpen(true);
   };
 
-  const handleSaveDataset = (name: string, config: DatasetConfig) => {
-    const newSettings = { ...settings };
+  const handleSaveDataset = async (name: string, config: DatasetConfig) => {
+    try {
+      const newSettings = { ...settings };
 
-    // If editing and name changed, remove old key
-    if (editingDataset && editingDataset !== name) {
-      delete newSettings[editingDataset];
-    }
+      // If editing and name changed, remove old key
+      if (editingDataset && editingDataset !== name) {
+        delete newSettings[editingDataset];
+      }
 
-    newSettings[name] = config;
-    updateSettings(newSettings);
-    setIsDatasetFormOpen(false);
-    setEditingDataset(null);
+      newSettings[name] = config;
+      await handleUpdateDatasets(newSettings);
+      setIsDatasetFormOpen(false);
+      setEditingDataset(null);
+    } catch (error) {
+      console.error("Failed to save dataset:", error);
+      toast.error("Failed to save dataset");
+    }
   };
 
   const handleCloseDatasetForm = () => {
@@ -68,37 +68,53 @@ export default function DatasetsSettingsEditor({
     setEditingDataset(null);
   };
 
-  const removeDataset = (datasetName: string) => {
-    const newSettings = { ...settings };
-    delete newSettings[datasetName];
-    updateSettings(newSettings);
+  const removeDataset = async (datasetName: string) => {
+    try {
+      const newSettings = { ...settings };
+      delete newSettings[datasetName];
+      await handleUpdateDatasets(newSettings);
+    } catch (error) {
+      console.error("Failed to remove dataset:", error);
+      toast.error("Failed to remove dataset");
+    }
   };
 
-  const handleJsonChange = (jsonValue: string) => {
-    onChange(jsonValue);
+  const handleJsonChange = async (jsonValue: string) => {
+    try {
+      const parsed = JSON.parse(jsonValue);
+      await handleUpdateDatasets(parsed);
+    } catch (error) {
+      console.error("Invalid JSON:", error);
+      toast.error("Invalid JSON format");
+    }
   };
 
   const handleEnabledChange = useCallback(
-    (datasetName: string, enabled: boolean) => {
-      const newSettings = { ...settings };
-      const dataset = newSettings[datasetName];
-      newSettings[datasetName] = {
-        ...dataset,
-        enabled
-      };
-      updateSettings(newSettings);
+    async (datasetName: string, enabled: boolean) => {
+      try {
+        const newSettings = { ...settings };
+        const dataset = newSettings[datasetName];
+        newSettings[datasetName] = {
+          ...dataset,
+          enabled
+        };
+        await handleUpdateDatasets(newSettings);
 
-      // Show toast notification
-      toast.success(
-        `Dataset "${datasetName}" ${enabled ? "enabled" : "disabled"} successfully`
-      );
-      addNotification({
-        type: "success",
-        title: "Dataset Updated",
-        message: `Dataset "${datasetName}" has been ${enabled ? "enabled" : "disabled"} successfully.`
-      });
+        // Show toast notification
+        toast.success(
+          `Dataset "${datasetName}" ${enabled ? "enabled" : "disabled"} successfully`
+        );
+        addNotification({
+          type: "success",
+          title: "Dataset Updated",
+          message: `Dataset "${datasetName}" has been ${enabled ? "enabled" : "disabled"} successfully.`
+        });
+      } catch (error) {
+        console.error("Failed to toggle dataset:", error);
+        toast.error("Failed to update dataset");
+      }
     },
-    [settings, addNotification]
+    [settings, addNotification, handleUpdateDatasets]
   );
 
   const datasetConfig = useMemo(() => {
@@ -154,7 +170,7 @@ export default function DatasetsSettingsEditor({
             Raw JSON Configuration
           </label>
           <textarea
-            value={value}
+            value={JSON.stringify(settings, null, 2)}
             onChange={(e) => handleJsonChange(e.target.value)}
             rows={15}
             className="block w-full rounded-md border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 font-mono"

+ 4 - 8
apps/web/src/app/components/FileCrud.tsx

@@ -1,9 +1,10 @@
 "use client";
 import { PlusIcon } from "@heroicons/react/24/outline";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
 import { useEffect, useState } from "react";
 import toast from "react-hot-toast";
-import { get, post } from "../../lib/api";
+import { post } from "../../lib/api";
+import { useAppContext } from "../providers/AppContext";
 import { useNotifications } from "./NotificationContext";
 import SlideInForm from "./SlideInForm";
 
@@ -31,16 +32,11 @@ const initialFileState: FileFormState = {
 export default function FileCrud({ editFile, onEditClose }: FileCrudProps) {
   const queryClient = useQueryClient();
   const { addNotification } = useNotifications();
+  const { datasets } = useAppContext();
   const [formState, setFormState] = useState<FileFormState>(initialFileState);
 
   const isEditing = !!editFile;
 
-  // Get available datasets
-  const { data: datasets } = useQuery({
-    queryKey: ["datasets"],
-    queryFn: () => get(`/files/all-datasets`)
-  });
-
   useEffect(() => {
     if (isEditing && editFile) {
       setFormState({

+ 18 - 16
apps/web/src/app/components/SettingsCrud.tsx

@@ -1,10 +1,8 @@
 "use client";
 import { PlusIcon } from "@heroicons/react/24/outline";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
 import { useEffect, useState } from "react";
 import toast from "react-hot-toast";
-import { post } from "../../lib/api";
-import DatasetsSettingsEditor from "./DatasetsSettingsEditor";
+import { useAppContext } from "../providers/AppContext";
 import JsonInput from "./JsonInput";
 import { useNotifications } from "./NotificationContext";
 import QueueSettingsEditor from "./QueueSettingsEditor";
@@ -34,10 +32,11 @@ export default function SettingsCrud({
   editValue,
   onEditClose
 }: SettingsCrudProps) {
-  const queryClient = useQueryClient();
+  const { updateSetting } = useAppContext();
   const { addNotification } = useNotifications();
   const [formState, setFormState] =
     useState<SettingsFormState>(initialSettingsState);
+  const [isSaving, setIsSaving] = useState(false);
 
   const isEditing = !!editKey;
 
@@ -51,11 +50,10 @@ export default function SettingsCrud({
     }
   }, [editKey, editValue, isEditing]);
 
-  const createMutation = useMutation({
-    mutationFn: () =>
-      post(`/config/settings`, { [formState.key]: formState.value }),
-    onSuccess: () => {
-      queryClient.invalidateQueries({ queryKey: ["settings"] });
+  const handleSave = async () => {
+    try {
+      setIsSaving(true);
+      await updateSetting(formState.key, formState.value);
       setFormState(initialSettingsState);
       if (onEditClose) onEditClose();
       const message = isEditing
@@ -67,8 +65,7 @@ export default function SettingsCrud({
         title: "Setting Saved",
         message: `${formState.key} has been ${isEditing ? "updated" : "added"} successfully.`
       });
-    },
-    onError: (error) => {
+    } catch (error) {
       console.error("Failed to save setting:", error);
       toast.error("Failed to save setting");
       addNotification({
@@ -76,12 +73,14 @@ export default function SettingsCrud({
         title: "Save Failed",
         message: "Failed to save the setting. Please try again."
       });
+    } finally {
+      setIsSaving(false);
     }
-  });
+  };
 
   const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault();
-    createMutation.mutate();
+    handleSave();
   };
 
   const getValueEditor = (currentKey?: string) => {
@@ -102,10 +101,13 @@ export default function SettingsCrud({
           />
         );
       case "datasets":
+        // For datasets, just use JsonInput since DatasetsSettingsEditor uses context
         return (
-          <DatasetsSettingsEditor
+          <JsonInput
             value={formState.value}
             onChange={(val) => setFormState({ ...formState, value: val })}
+            className="w-full"
+            placeholder="Datasets configuration (JSON)"
           />
         );
       default:
@@ -143,7 +145,7 @@ export default function SettingsCrud({
             <button
               type="button"
               onClick={handleSubmit}
-              disabled={createMutation.isPending}
+              disabled={isSaving}
               className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
             >
               Update Setting
@@ -204,7 +206,7 @@ export default function SettingsCrud({
             <button
               type="submit"
               onClick={handleSubmit}
-              disabled={createMutation.isPending}
+              disabled={isSaving}
               className="inline-flex items-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
             >
               {isEditing ? "Update Setting" : "Add Setting"}

+ 30 - 65
apps/web/src/app/components/SettingsList.tsx

@@ -1,8 +1,7 @@
 "use client";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
 import { useEffect, useMemo, useState } from "react";
 import toast from "react-hot-toast";
-import { del, get } from "../../lib/api";
+import { useAppContext } from "../providers/AppContext";
 import ConfirmationDialog from "./ConfirmationDialog";
 import LoadingCard from "./Loading";
 import { useNotifications } from "./NotificationContext";
@@ -10,35 +9,8 @@ import Pagination from "./Pagination";
 import SettingsCrud from "./SettingsCrud";
 
 export default function SettingsList() {
-  const queryClient = useQueryClient();
+  const { settings, isLoading, error, deleteSetting } = useAppContext();
   const { addNotification } = useNotifications();
-  const { data, isLoading, error } = useQuery({
-    queryKey: ["settings"],
-    queryFn: () => get("/config/settings")
-  });
-
-  // Listen for WebSocket events
-  useEffect(() => {
-    const handleSettingsUpdate = (event: CustomEvent) => {
-      const settingsData = event.detail;
-      if (settingsData.type === "settings") {
-        // Invalidate and refetch settings when settings updates occur
-        queryClient.invalidateQueries({ queryKey: ["settings"] });
-      }
-    };
-
-    window.addEventListener(
-      "settingsUpdate",
-      handleSettingsUpdate as EventListener
-    );
-
-    return () => {
-      window.removeEventListener(
-        "settingsUpdate",
-        handleSettingsUpdate as EventListener
-      );
-    };
-  }, [queryClient]);
 
   const [editKey, setEditKey] = useState<string | null>(null);
   const [editValue, setEditValue] = useState<string>("");
@@ -58,27 +30,28 @@ export default function SettingsList() {
   const [currentPage, setCurrentPage] = useState(1);
   const [pageSize, setPageSize] = useState(25);
 
-  const deleteMutation = useMutation({
-    mutationFn: (key: string) => del(`/config/settings/${key}`),
-    onSuccess: (_, key) => {
-      queryClient.invalidateQueries({ queryKey: ["settings"] });
-      toast.success("Setting deleted successfully");
-      addNotification({
-        type: "success",
-        title: "Setting Deleted",
-        message: `Setting "${key}" has been deleted successfully.`
-      });
-    },
-    onError: (error, key) => {
-      console.error("Failed to delete setting:", error);
-      toast.error("Failed to delete setting");
-      addNotification({
-        type: "error",
-        title: "Delete Failed",
-        message: `Failed to delete setting "${key}". Please try again.`
-      });
+  const handleDeleteConfirm = async () => {
+    if (deleteConfirm.setting) {
+      try {
+        await deleteSetting(deleteConfirm.setting.key);
+        toast.success("Setting deleted successfully");
+        addNotification({
+          type: "success",
+          title: "Setting Deleted",
+          message: `Setting "${deleteConfirm.setting.key}" has been deleted successfully.`
+        });
+      } catch (error) {
+        console.error("Failed to delete setting:", error);
+        toast.error("Failed to delete setting");
+        addNotification({
+          type: "error",
+          title: "Delete Failed",
+          message: `Failed to delete setting "${deleteConfirm.setting.key}". Please try again.`
+        });
+      }
     }
-  });
+    setDeleteConfirm({ isOpen: false });
+  };
 
   const handleEdit = (key: string, value: any) => {
     setEditKey(key);
@@ -98,13 +71,6 @@ export default function SettingsList() {
     });
   };
 
-  const handleDeleteConfirm = () => {
-    if (deleteConfirm.setting) {
-      deleteMutation.mutate(deleteConfirm.setting.key);
-    }
-    setDeleteConfirm({ isOpen: false });
-  };
-
   const handleDeleteCancel = () => {
     setDeleteConfirm({ isOpen: false });
   };
@@ -125,9 +91,9 @@ export default function SettingsList() {
 
   // Filter and sort data
   const filteredAndSortedData = useMemo(() => {
-    if (!data) return [];
+    if (!settings) return [];
 
-    const filtered = Object.entries(data).filter(
+    const filtered = Object.entries(settings).filter(
       ([key, value]: [string, any]) =>
         key.toLowerCase().includes(searchTerm.toLowerCase()) ||
         JSON.stringify(value).toLowerCase().includes(searchTerm.toLowerCase())
@@ -148,7 +114,7 @@ export default function SettingsList() {
     );
 
     return filtered;
-  }, [data, searchTerm, sortField, sortDirection]);
+  }, [settings, searchTerm, sortField, sortDirection]);
 
   // Handle pagination
   const paginatedData = useMemo(() => {
@@ -190,9 +156,9 @@ export default function SettingsList() {
           There was an error loading the settings data.
         </p>
         <button
-          onClick={() =>
-            queryClient.invalidateQueries({ queryKey: ["settings"] })
-          }
+          onClick={() => {
+            window.location.reload();
+          }}
           className="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
         >
           Try Again
@@ -276,7 +242,6 @@ export default function SettingsList() {
                         Edit
                       </button>
                       <button
-                        disabled={deleteMutation.isPending}
                         className="inline-flex items-center rounded-md bg-red-600 px-3 py-1 text-xs font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50"
                         onClick={() => handleDeleteClick({ key, value })}
                       >
@@ -328,7 +293,7 @@ export default function SettingsList() {
         type="danger"
         onConfirm={handleDeleteConfirm}
         onClose={handleDeleteCancel}
-        isLoading={deleteMutation.isPending}
+        isLoading={false}
       />
     </>
   );

+ 5 - 10
apps/web/src/app/files/FileList.tsx

@@ -14,17 +14,12 @@ import FileCrud from "../components/FileCrud";
 import LoadingCard from "../components/Loading";
 import { useNotifications } from "../components/NotificationContext";
 import Pagination from "../components/Pagination";
+import { useAppContext } from "../providers/AppContext";
 
 export default function FileList() {
   const queryClient = useQueryClient();
   const { addNotification } = useNotifications();
-
-  // Get available datasets
-  const { data: datasets, isLoading: isDatasetsLoading } = useQuery({
-    queryKey: ["datasets"],
-    queryFn: () => get(`/files/all-datasets`),
-    staleTime: 30000 // 30 seconds
-  });
+  const { datasets, isLoading: isContextLoading } = useAppContext();
 
   // Get files from all datasets
   const {
@@ -32,7 +27,7 @@ export default function FileList() {
     isLoading,
     error
   } = useQuery({
-    queryKey: ["all-files"],
+    queryKey: ["all-files", datasets],
     queryFn: async () => {
       if (!datasets || datasets.length === 0) return [];
       const allFilesPromises = datasets.map((datasetName: string) =>
@@ -126,7 +121,7 @@ export default function FileList() {
       // Only auto-select all datasets if nothing was saved in localStorage
       const datasetNames = datasets
         .map((path: string) => path.split("/").pop())
-        .filter(Boolean);
+        .filter(Boolean) as string[];
       setEnabledDatasets(new Set(datasetNames));
     }
   }, [datasets]);
@@ -451,7 +446,7 @@ export default function FileList() {
   const displayFiles = paginatedData;
 
   // Show loading state if datasets are loading, allFiles is loading, or data is undefined
-  if (isDatasetsLoading || isLoading || !allFiles) {
+  if (isContextLoading || isLoading || !allFiles) {
     return <LoadingCard message="Loading files..." />;
   }
   if (error) {