Pārlūkot izejas kodu

feat: add global AppContext for centralized state management

- Create AppContext provider to manage all application-wide data globally
- Centralize settings, datasetsConfig, queueConfig, watcherConfig, and datasets
- Implement shadow copy pattern: maintain local state that updates with mutations
- Load all data on app initialization with WebSocket event listeners
- Provide mutation functions: updateSetting, deleteSetting, updateDatasets, updateQueueConfig, updateWatcherConfig
- Add refresh functions: refreshSettings, refreshDatasets, refreshAll
- Listen for WebSocket events and auto-update context state
- Initialize from AppProvider in root providers.tsx

This eliminates the need for duplicate queries across components and provides
a single source of truth for application state with automatic synchronization.
Timothy Pomeroy 1 mēnesi atpakaļ
vecāks
revīzija
70ae3e9d27
2 mainītis faili ar 300 papildinājumiem un 18 dzēšanām
  1. 21 18
      apps/web/src/app/providers.tsx
  2. 279 0
      apps/web/src/app/providers/AppContext.tsx

+ 21 - 18
apps/web/src/app/providers.tsx

@@ -6,6 +6,7 @@ import { wsService } from "../lib/websocket";
 import ErrorBoundary from "./components/ErrorBoundary";
 import Loading from "./components/Loading";
 import { NotificationProvider } from "./components/NotificationContext";
+import { AppProvider } from "./providers/AppContext";
 
 export default function Providers({ children }: { children: ReactNode }) {
   const [queryClient] = useState(
@@ -50,24 +51,26 @@ export default function Providers({ children }: { children: ReactNode }) {
   return (
     <ErrorBoundary>
       <QueryClientProvider client={queryClient}>
-        <NotificationProvider>
-          <Suspense
-            fallback={<Loading message="Loading application..." size="lg" />}
-          >
-            {children}
-          </Suspense>
-          <Toaster
-            position="top-right"
-            toastOptions={{
-              duration: 4000,
-              style: {
-                background: "var(--toast-bg, #363636)",
-                color: "var(--toast-color, #fff)",
-                top: "80px" // Position below the 64px header + some padding
-              }
-            }}
-          />
-        </NotificationProvider>
+        <AppProvider>
+          <NotificationProvider>
+            <Suspense
+              fallback={<Loading message="Loading application..." size="lg" />}
+            >
+              {children}
+            </Suspense>
+            <Toaster
+              position="top-right"
+              toastOptions={{
+                duration: 4000,
+                style: {
+                  background: "var(--toast-bg, #363636)",
+                  color: "var(--toast-color, #fff)",
+                  top: "80px" // Position below the 64px header + some padding
+                }
+              }}
+            />
+          </NotificationProvider>
+        </AppProvider>
       </QueryClientProvider>
     </ErrorBoundary>
   );

+ 279 - 0
apps/web/src/app/providers/AppContext.tsx

@@ -0,0 +1,279 @@
+"use client";
+
+import React, { ReactNode, useCallback, useEffect, useState } from "react";
+import { get, post, del, put } from "../../lib/api";
+
+export interface Settings {
+  [key: string]: any;
+}
+
+export interface Dataset {
+  [path: string]: any;
+}
+
+export interface DatasetsConfig {
+  [datasetName: string]: Dataset;
+}
+
+export interface QueueConfig {
+  maxConcurrent?: number;
+  maxRetries?: number;
+  [key: string]: any;
+}
+
+export interface WatcherConfig {
+  enabled?: boolean;
+  [key: string]: any;
+}
+
+export interface AppContextType {
+  // Data
+  settings: Settings | null;
+  datasetsConfig: DatasetsConfig | null;
+  queueConfig: QueueConfig | null;
+  watcherConfig: WatcherConfig | null;
+  datasets: string[] | null;
+  
+  // Loading states
+  isLoading: boolean;
+  isInitialized: boolean;
+  
+  // Errors
+  error: Error | null;
+  
+  // Mutation functions
+  updateSetting: (key: string, value: any) => Promise<void>;
+  deleteSetting: (key: string) => Promise<void>;
+  updateDatasets: (datasets: DatasetsConfig) => Promise<void>;
+  updateQueueConfig: (config: QueueConfig) => Promise<void>;
+  updateWatcherConfig: (config: WatcherConfig) => Promise<void>;
+  
+  // Refresh functions
+  refreshSettings: () => Promise<void>;
+  refreshDatasets: () => Promise<void>;
+  refreshAll: () => Promise<void>;
+}
+
+const AppContext = React.createContext<AppContextType | undefined>(undefined);
+
+export function AppProvider({ children }: { children: ReactNode }) {
+  const [settings, setSettings] = useState<Settings | null>(null);
+  const [datasetsConfig, setDatasetsConfig] = useState<DatasetsConfig | null>(
+    null
+  );
+  const [queueConfig, setQueueConfig] = useState<QueueConfig | null>(null);
+  const [watcherConfig, setWatcherConfig] = useState<WatcherConfig | null>(
+    null
+  );
+  const [datasets, setDatasets] = useState<string[] | null>(null);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isInitialized, setIsInitialized] = useState(false);
+  const [error, setError] = useState<Error | null>(null);
+
+  // Load all initial data
+  const initializeData = useCallback(async () => {
+    try {
+      setIsLoading(true);
+      setError(null);
+
+      // Load settings
+      const settingsData = await get("/config/settings");
+      setSettings(settingsData || {});
+
+      // Extract specific configs from settings
+      if (settingsData) {
+        const queue = settingsData.queue || {};
+        const watcher = settingsData.watcher || {};
+        const datasetsData = settingsData.datasets || {};
+
+        setQueueConfig(queue);
+        setWatcherConfig(watcher);
+        setDatasetsConfig(datasetsData);
+      }
+
+      // Load datasets list
+      const datasetsList = await get("/files/all-datasets");
+      setDatasets(datasetsList || []);
+
+      setIsInitialized(true);
+    } catch (err) {
+      const error = err instanceof Error ? err : new Error(String(err));
+      setError(error);
+      console.error("Failed to initialize app context:", error);
+    } finally {
+      setIsLoading(false);
+    }
+  }, []);
+
+  // Initial load
+  useEffect(() => {
+    initializeData();
+  }, [initializeData]);
+
+  // Listen for WebSocket events
+  useEffect(() => {
+    const handleSettingsUpdate = () => {
+      initializeData();
+    };
+
+    const handleTaskUpdate = () => {
+      // Refetch datasets in case new ones were created
+      initializeData();
+    };
+
+    const handleFileUpdate = () => {
+      // Refetch datasets in case new ones were created
+      initializeData();
+    };
+
+    window.addEventListener("settingsUpdate", handleSettingsUpdate as EventListener);
+    window.addEventListener("taskUpdate", handleTaskUpdate as EventListener);
+    window.addEventListener("fileUpdate", handleFileUpdate as EventListener);
+
+    return () => {
+      window.removeEventListener("settingsUpdate", handleSettingsUpdate as EventListener);
+      window.removeEventListener("taskUpdate", handleTaskUpdate as EventListener);
+      window.removeEventListener("fileUpdate", handleFileUpdate as EventListener);
+    };
+  }, [initializeData]);
+
+  // Mutation functions
+  const updateSetting = useCallback(
+    async (key: string, value: any) => {
+      try {
+        await post("/config/settings", { [key]: value });
+        setSettings((prev) =>
+          prev ? { ...prev, [key]: value } : { [key]: value }
+        );
+        await initializeData();
+      } catch (err) {
+        const error = err instanceof Error ? err : new Error(String(err));
+        setError(error);
+        throw error;
+      }
+    },
+    [initializeData]
+  );
+
+  const deleteSetting = useCallback(
+    async (key: string) => {
+      try {
+        await del(`/config/settings/${key}`);
+        setSettings((prev) => {
+          if (!prev) return null;
+          const updated = { ...prev };
+          delete updated[key];
+          return updated;
+        });
+        await initializeData();
+      } catch (err) {
+        const error = err instanceof Error ? err : new Error(String(err));
+        setError(error);
+        throw error;
+      }
+    },
+    [initializeData]
+  );
+
+  const updateDatasets = useCallback(
+    async (datasetsData: DatasetsConfig) => {
+      try {
+        setDatasetsConfig(datasetsData);
+        await updateSetting("datasets", datasetsData);
+      } catch (err) {
+        const error = err instanceof Error ? err : new Error(String(err));
+        setError(error);
+        throw error;
+      }
+    },
+    [updateSetting]
+  );
+
+  const updateQueueConfig = useCallback(
+    async (config: QueueConfig) => {
+      try {
+        setQueueConfig(config);
+        await updateSetting("queue", config);
+      } catch (err) {
+        const error = err instanceof Error ? err : new Error(String(err));
+        setError(error);
+        throw error;
+      }
+    },
+    [updateSetting]
+  );
+
+  const updateWatcherConfig = useCallback(
+    async (config: WatcherConfig) => {
+      try {
+        setWatcherConfig(config);
+        await updateSetting("watcher", config);
+      } catch (err) {
+        const error = err instanceof Error ? err : new Error(String(err));
+        setError(error);
+        throw error;
+      }
+    },
+    [updateSetting]
+  );
+
+  const refreshSettings = useCallback(async () => {
+    try {
+      const settingsData = await get("/config/settings");
+      setSettings(settingsData || {});
+      if (settingsData) {
+        setQueueConfig(settingsData.queue || {});
+        setWatcherConfig(settingsData.watcher || {});
+        setDatasetsConfig(settingsData.datasets || {});
+      }
+    } catch (err) {
+      const error = err instanceof Error ? err : new Error(String(err));
+      setError(error);
+      throw error;
+    }
+  }, []);
+
+  const refreshDatasets = useCallback(async () => {
+    try {
+      const datasetsList = await get("/files/all-datasets");
+      setDatasets(datasetsList || []);
+    } catch (err) {
+      const error = err instanceof Error ? err : new Error(String(err));
+      setError(error);
+      throw error;
+    }
+  }, []);
+
+  const refreshAll = useCallback(async () => {
+    await initializeData();
+  }, [initializeData]);
+
+  const value: AppContextType = {
+    settings,
+    datasetsConfig,
+    queueConfig,
+    watcherConfig,
+    datasets,
+    isLoading,
+    isInitialized,
+    error,
+    updateSetting,
+    deleteSetting,
+    updateDatasets,
+    updateQueueConfig,
+    updateWatcherConfig,
+    refreshSettings,
+    refreshDatasets,
+    refreshAll
+  };
+
+  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
+}
+
+export function useAppContext() {
+  const context = React.useContext(AppContext);
+  if (context === undefined) {
+    throw new Error("useAppContext must be used within an AppProvider");
+  }
+  return context;
+}