mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: refactor server data management to use global context (#319)
This commit is contained in:
@@ -3,6 +3,7 @@ import { BrowserRouter as Router, Route, Routes, Navigate, useParams } from 'rea
|
|||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { ToastProvider } from './contexts/ToastContext';
|
import { ToastProvider } from './contexts/ToastContext';
|
||||||
import { ThemeProvider } from './contexts/ThemeContext';
|
import { ThemeProvider } from './contexts/ThemeContext';
|
||||||
|
import { ServerProvider } from './contexts/ServerContext';
|
||||||
import MainLayout from './layouts/MainLayout';
|
import MainLayout from './layouts/MainLayout';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
import LoginPage from './pages/LoginPage';
|
import LoginPage from './pages/LoginPage';
|
||||||
@@ -26,6 +27,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
<ServerProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<Router basename={basename}>
|
<Router basename={basename}>
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -57,6 +59,7 @@ function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
|
</ServerProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
346
frontend/src/contexts/ServerContext.tsx
Normal file
346
frontend/src/contexts/ServerContext.tsx
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
import React, { createContext, useState, useEffect, useRef, useCallback, useContext } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Server, ApiResponse } from '@/types';
|
||||||
|
import { apiGet, apiPost, apiDelete } from '../utils/fetchInterceptor';
|
||||||
|
import { useAuth } from './AuthContext';
|
||||||
|
|
||||||
|
// Configuration options
|
||||||
|
const CONFIG = {
|
||||||
|
// Initialization phase configuration
|
||||||
|
startup: {
|
||||||
|
maxAttempts: 60, // Maximum number of attempts during initialization
|
||||||
|
pollingInterval: 3000, // Polling interval during initialization (3 seconds)
|
||||||
|
},
|
||||||
|
// Normal operation phase configuration
|
||||||
|
normal: {
|
||||||
|
pollingInterval: 10000, // Polling interval during normal operation (10 seconds)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Context type definition
|
||||||
|
interface ServerContextType {
|
||||||
|
servers: Server[];
|
||||||
|
error: string | null;
|
||||||
|
setError: (error: string | null) => void;
|
||||||
|
isLoading: boolean;
|
||||||
|
fetchAttempts: number;
|
||||||
|
triggerRefresh: () => void;
|
||||||
|
refreshIfNeeded: () => void; // Smart refresh with debounce
|
||||||
|
handleServerAdd: () => void;
|
||||||
|
handleServerEdit: (server: Server) => Promise<any>;
|
||||||
|
handleServerRemove: (serverName: string) => Promise<boolean>;
|
||||||
|
handleServerToggle: (server: Server, enabled: boolean) => Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Context
|
||||||
|
const ServerContext = createContext<ServerContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
// Provider component
|
||||||
|
export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { auth } = useAuth();
|
||||||
|
const [servers, setServers] = useState<Server[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
||||||
|
const [fetchAttempts, setFetchAttempts] = useState(0);
|
||||||
|
|
||||||
|
// Timer reference for polling
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
// Track current attempt count to avoid dependency cycles
|
||||||
|
const attemptsRef = useRef<number>(0);
|
||||||
|
// Track last fetch time to implement smart refresh
|
||||||
|
const lastFetchTimeRef = useRef<number>(0);
|
||||||
|
// Minimum interval between manual refreshes (5 seconds in dev, 3 seconds in prod)
|
||||||
|
const MIN_REFRESH_INTERVAL = process.env.NODE_ENV === 'development' ? 5000 : 3000;
|
||||||
|
|
||||||
|
// Clear the timer
|
||||||
|
const clearTimer = () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start normal polling
|
||||||
|
const startNormalPolling = useCallback((options?: { immediate?: boolean }) => {
|
||||||
|
const immediate = options?.immediate ?? true;
|
||||||
|
// Ensure no other timers are running
|
||||||
|
clearTimer();
|
||||||
|
|
||||||
|
const fetchServers = async () => {
|
||||||
|
try {
|
||||||
|
console.log('[ServerContext] Fetching servers from API...');
|
||||||
|
const data = await apiGet('/servers');
|
||||||
|
|
||||||
|
// Update last fetch time
|
||||||
|
lastFetchTimeRef.current = Date.now();
|
||||||
|
|
||||||
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
|
setServers(data.data);
|
||||||
|
} else if (data && Array.isArray(data)) {
|
||||||
|
setServers(data);
|
||||||
|
} else {
|
||||||
|
console.error('Invalid server data format:', data);
|
||||||
|
setServers([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset error state
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching servers during normal polling:', err);
|
||||||
|
|
||||||
|
// Use friendly error message
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
setError(t('errors.network'));
|
||||||
|
} else if (
|
||||||
|
err instanceof TypeError &&
|
||||||
|
(err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
|
||||||
|
) {
|
||||||
|
setError(t('errors.serverConnection'));
|
||||||
|
} else {
|
||||||
|
setError(t('errors.serverFetch'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute immediately unless explicitly skipped
|
||||||
|
if (immediate) {
|
||||||
|
fetchServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up regular polling
|
||||||
|
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
// Watch for authentication status changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (auth.isAuthenticated) {
|
||||||
|
console.log('[ServerContext] User authenticated, triggering refresh');
|
||||||
|
// When user logs in, trigger a refresh to load servers
|
||||||
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
|
} else {
|
||||||
|
console.log('[ServerContext] User not authenticated, clearing data and stopping polling');
|
||||||
|
// When user logs out, clear data and stop polling
|
||||||
|
clearTimer();
|
||||||
|
setServers([]);
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
}, [auth.isAuthenticated]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If not authenticated, don't poll
|
||||||
|
if (!auth.isAuthenticated) {
|
||||||
|
console.log('[ServerContext] User not authenticated, skipping polling setup');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset attempt count
|
||||||
|
if (refreshKey > 0) {
|
||||||
|
attemptsRef.current = 0;
|
||||||
|
setFetchAttempts(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization phase request function
|
||||||
|
const fetchInitialData = async () => {
|
||||||
|
try {
|
||||||
|
console.log('[ServerContext] Initial fetch - attempt', attemptsRef.current + 1);
|
||||||
|
const data = await apiGet('/servers');
|
||||||
|
|
||||||
|
// Update last fetch time
|
||||||
|
lastFetchTimeRef.current = Date.now();
|
||||||
|
|
||||||
|
// Handle API response wrapper object, extract data field
|
||||||
|
if (data && data.success && Array.isArray(data.data)) {
|
||||||
|
setServers(data.data);
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
// Initialization successful, start normal polling (skip immediate to avoid duplicate fetch)
|
||||||
|
startNormalPolling({ immediate: false });
|
||||||
|
return true;
|
||||||
|
} else if (data && Array.isArray(data)) {
|
||||||
|
// Compatibility handling, if API directly returns array
|
||||||
|
setServers(data);
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
// Initialization successful, start normal polling (skip immediate to avoid duplicate fetch)
|
||||||
|
startNormalPolling({ immediate: false });
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// If data format is not as expected, set to empty array
|
||||||
|
console.error('Invalid server data format:', data);
|
||||||
|
setServers([]);
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
// Initialization successful but data is empty, start normal polling (skip immediate)
|
||||||
|
startNormalPolling({ immediate: false });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Increment attempt count, use ref to avoid triggering effect rerun
|
||||||
|
attemptsRef.current += 1;
|
||||||
|
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);
|
||||||
|
|
||||||
|
// Update state for display
|
||||||
|
setFetchAttempts(attemptsRef.current);
|
||||||
|
|
||||||
|
// Set appropriate error message
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
setError(t('errors.network'));
|
||||||
|
} else {
|
||||||
|
setError(t('errors.initialStartup'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If maximum attempt count is exceeded, give up initialization and switch to normal polling
|
||||||
|
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
|
||||||
|
console.log('Maximum startup attempts reached, switching to normal polling');
|
||||||
|
setIsInitialLoading(false);
|
||||||
|
// Clear initialization polling
|
||||||
|
clearTimer();
|
||||||
|
// Switch to normal polling mode
|
||||||
|
startNormalPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// On component mount, set appropriate polling based on current state
|
||||||
|
if (isInitialLoading) {
|
||||||
|
// Ensure no other timers are running
|
||||||
|
clearTimer();
|
||||||
|
|
||||||
|
// Execute initial request immediately
|
||||||
|
fetchInitialData();
|
||||||
|
|
||||||
|
// Set polling interval for initialization phase
|
||||||
|
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
|
||||||
|
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
|
||||||
|
} else {
|
||||||
|
// Initialization completed, start normal polling
|
||||||
|
startNormalPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
clearTimer();
|
||||||
|
};
|
||||||
|
}, [refreshKey, t, isInitialLoading, startNormalPolling]);
|
||||||
|
|
||||||
|
// Manually trigger refresh (always refreshes)
|
||||||
|
const triggerRefresh = useCallback(() => {
|
||||||
|
// Clear current timer
|
||||||
|
clearTimer();
|
||||||
|
|
||||||
|
// If in initialization phase, reset initialization state
|
||||||
|
if (isInitialLoading) {
|
||||||
|
setIsInitialLoading(true);
|
||||||
|
attemptsRef.current = 0;
|
||||||
|
setFetchAttempts(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change in refreshKey will trigger useEffect to run again
|
||||||
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
|
}, [isInitialLoading]);
|
||||||
|
|
||||||
|
// Smart refresh with debounce (only refresh if enough time has passed)
|
||||||
|
const refreshIfNeeded = useCallback(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastFetch = now - lastFetchTimeRef.current;
|
||||||
|
|
||||||
|
// Log who is calling this
|
||||||
|
console.log('[ServerContext] refreshIfNeeded called, time since last fetch:', timeSinceLastFetch, 'ms');
|
||||||
|
|
||||||
|
// Only refresh if enough time has passed since last fetch
|
||||||
|
if (timeSinceLastFetch >= MIN_REFRESH_INTERVAL) {
|
||||||
|
console.log('[ServerContext] Triggering refresh (exceeded MIN_REFRESH_INTERVAL:', MIN_REFRESH_INTERVAL, 'ms)');
|
||||||
|
triggerRefresh();
|
||||||
|
} else {
|
||||||
|
console.log('[ServerContext] Skipping refresh (MIN_REFRESH_INTERVAL:', MIN_REFRESH_INTERVAL, 'ms, time since last:', timeSinceLastFetch, 'ms)');
|
||||||
|
}
|
||||||
|
}, [triggerRefresh]);
|
||||||
|
|
||||||
|
// Server related operations
|
||||||
|
const handleServerAdd = useCallback(() => {
|
||||||
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleServerEdit = useCallback(async (server: Server) => {
|
||||||
|
try {
|
||||||
|
// Fetch server config from the dedicated server config endpoint
|
||||||
|
const serverConfigData: ApiResponse<any> = await apiGet(`/servers/${server.name}`);
|
||||||
|
|
||||||
|
if (serverConfigData && serverConfigData.success && serverConfigData.data) {
|
||||||
|
return serverConfigData.data;
|
||||||
|
} else {
|
||||||
|
console.error('Failed to get server config:', serverConfigData);
|
||||||
|
setError(t('server.invalidConfig', { serverName: server.name }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching server config:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const handleServerRemove = useCallback(async (serverName: string) => {
|
||||||
|
try {
|
||||||
|
const result = await apiDelete(`/servers/${serverName}`);
|
||||||
|
|
||||||
|
if (!result || !result.success) {
|
||||||
|
setError(result?.message || t('server.deleteError', { serverName }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const handleServerToggle = useCallback(async (server: Server, enabled: boolean) => {
|
||||||
|
try {
|
||||||
|
const result = await apiPost(`/servers/${server.name}/toggle`, { enabled });
|
||||||
|
|
||||||
|
if (!result || !result.success) {
|
||||||
|
console.error('Failed to toggle server:', result);
|
||||||
|
setError(result?.message || t('server.toggleError', { serverName: server.name }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the UI immediately to reflect the change
|
||||||
|
setRefreshKey((prevKey) => prevKey + 1);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling server:', err);
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [t]);
|
||||||
|
|
||||||
|
const value: ServerContextType = {
|
||||||
|
servers,
|
||||||
|
error,
|
||||||
|
setError,
|
||||||
|
isLoading: isInitialLoading,
|
||||||
|
fetchAttempts,
|
||||||
|
triggerRefresh,
|
||||||
|
refreshIfNeeded,
|
||||||
|
handleServerAdd,
|
||||||
|
handleServerEdit,
|
||||||
|
handleServerRemove,
|
||||||
|
handleServerToggle,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <ServerContext.Provider value={value}>{children}</ServerContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom hook to use the Server context
|
||||||
|
export const useServerContext = () => {
|
||||||
|
const context = useContext(ServerContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useServerContext must be used within a ServerProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,272 +1,19 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
// This hook now delegates to the ServerContext to avoid duplicate requests
|
||||||
import { useTranslation } from 'react-i18next';
|
// All components will share the same server data and polling mechanism
|
||||||
import { Server, ApiResponse } from '@/types';
|
import { useServerContext } from '@/contexts/ServerContext';
|
||||||
import { apiGet, apiPost, apiDelete } from '../utils/fetchInterceptor';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
// Configuration options
|
export const useServerData = (options?: { refreshOnMount?: boolean }) => {
|
||||||
const CONFIG = {
|
const context = useServerContext();
|
||||||
// Initialization phase configuration
|
const { refreshIfNeeded } = context;
|
||||||
startup: {
|
|
||||||
maxAttempts: 60, // Maximum number of attempts during initialization
|
|
||||||
pollingInterval: 3000, // Polling interval during initialization (3 seconds)
|
|
||||||
},
|
|
||||||
// Normal operation phase configuration
|
|
||||||
normal: {
|
|
||||||
pollingInterval: 10000, // Polling interval during normal operation (10 seconds)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useServerData = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const [servers, setServers] = useState<Server[]>([]);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
|
||||||
const [isInitialLoading, setIsInitialLoading] = useState(true);
|
|
||||||
const [fetchAttempts, setFetchAttempts] = useState(0);
|
|
||||||
|
|
||||||
// Timer reference for polling
|
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
// Track current attempt count to avoid dependency cycles
|
|
||||||
const attemptsRef = useRef<number>(0);
|
|
||||||
|
|
||||||
// Clear the timer
|
|
||||||
const clearTimer = () => {
|
|
||||||
if (intervalRef.current) {
|
|
||||||
clearInterval(intervalRef.current);
|
|
||||||
intervalRef.current = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start normal polling
|
|
||||||
const startNormalPolling = useCallback(() => {
|
|
||||||
// Ensure no other timers are running
|
|
||||||
clearTimer();
|
|
||||||
|
|
||||||
const fetchServers = async () => {
|
|
||||||
try {
|
|
||||||
const data = await apiGet('/servers');
|
|
||||||
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
|
||||||
setServers(data.data);
|
|
||||||
} else if (data && Array.isArray(data)) {
|
|
||||||
setServers(data);
|
|
||||||
} else {
|
|
||||||
console.error('Invalid server data format:', data);
|
|
||||||
setServers([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset error state
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching servers during normal polling:', err);
|
|
||||||
|
|
||||||
// Use friendly error message
|
|
||||||
if (!navigator.onLine) {
|
|
||||||
setError(t('errors.network'));
|
|
||||||
} else if (
|
|
||||||
err instanceof TypeError &&
|
|
||||||
(err.message.includes('NetworkError') || err.message.includes('Failed to fetch'))
|
|
||||||
) {
|
|
||||||
setError(t('errors.serverConnection'));
|
|
||||||
} else {
|
|
||||||
setError(t('errors.serverFetch'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute immediately
|
|
||||||
fetchServers();
|
|
||||||
|
|
||||||
// Set up regular polling
|
|
||||||
intervalRef.current = setInterval(fetchServers, CONFIG.normal.pollingInterval);
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
|
// Optionally refresh on mount for pages that need fresh data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset attempt count
|
if (options?.refreshOnMount) {
|
||||||
if (refreshKey > 0) {
|
refreshIfNeeded();
|
||||||
attemptsRef.current = 0;
|
|
||||||
setFetchAttempts(0);
|
|
||||||
}
|
}
|
||||||
|
}, [options?.refreshOnMount, refreshIfNeeded]);
|
||||||
|
|
||||||
// Initialization phase request function
|
return context;
|
||||||
const fetchInitialData = async () => {
|
|
||||||
try {
|
|
||||||
const data = await apiGet('/servers');
|
|
||||||
|
|
||||||
// Handle API response wrapper object, extract data field
|
|
||||||
if (data && data.success && Array.isArray(data.data)) {
|
|
||||||
setServers(data.data);
|
|
||||||
setIsInitialLoading(false);
|
|
||||||
// Initialization successful, start normal polling
|
|
||||||
startNormalPolling();
|
|
||||||
return true;
|
|
||||||
} else if (data && Array.isArray(data)) {
|
|
||||||
// Compatibility handling, if API directly returns array
|
|
||||||
setServers(data);
|
|
||||||
setIsInitialLoading(false);
|
|
||||||
// Initialization successful, start normal polling
|
|
||||||
startNormalPolling();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// If data format is not as expected, set to empty array
|
|
||||||
console.error('Invalid server data format:', data);
|
|
||||||
setServers([]);
|
|
||||||
setIsInitialLoading(false);
|
|
||||||
// Initialization successful but data is empty, start normal polling
|
|
||||||
startNormalPolling();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Increment attempt count, use ref to avoid triggering effect rerun
|
|
||||||
attemptsRef.current += 1;
|
|
||||||
console.error(`Initial loading attempt ${attemptsRef.current} failed:`, err);
|
|
||||||
|
|
||||||
// Update state for display
|
|
||||||
setFetchAttempts(attemptsRef.current);
|
|
||||||
|
|
||||||
// Set appropriate error message
|
|
||||||
if (!navigator.onLine) {
|
|
||||||
setError(t('errors.network'));
|
|
||||||
} else {
|
|
||||||
setError(t('errors.initialStartup'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If maximum attempt count is exceeded, give up initialization and switch to normal polling
|
|
||||||
if (attemptsRef.current >= CONFIG.startup.maxAttempts) {
|
|
||||||
console.log('Maximum startup attempts reached, switching to normal polling');
|
|
||||||
setIsInitialLoading(false);
|
|
||||||
// Clear initialization polling
|
|
||||||
clearTimer();
|
|
||||||
// Switch to normal polling mode
|
|
||||||
startNormalPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// On component mount, set appropriate polling based on current state
|
|
||||||
if (isInitialLoading) {
|
|
||||||
// Ensure no other timers are running
|
|
||||||
clearTimer();
|
|
||||||
|
|
||||||
// Execute initial request immediately
|
|
||||||
fetchInitialData();
|
|
||||||
|
|
||||||
// Set polling interval for initialization phase
|
|
||||||
intervalRef.current = setInterval(fetchInitialData, CONFIG.startup.pollingInterval);
|
|
||||||
console.log(`Started initial polling with interval: ${CONFIG.startup.pollingInterval}ms`);
|
|
||||||
} else {
|
|
||||||
// Initialization completed, start normal polling
|
|
||||||
startNormalPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup function
|
|
||||||
return () => {
|
|
||||||
clearTimer();
|
|
||||||
};
|
|
||||||
}, [refreshKey, t, isInitialLoading, startNormalPolling]);
|
|
||||||
|
|
||||||
// Manually trigger refresh
|
|
||||||
const triggerRefresh = () => {
|
|
||||||
// Clear current timer
|
|
||||||
clearTimer();
|
|
||||||
|
|
||||||
// If in initialization phase, reset initialization state
|
|
||||||
if (isInitialLoading) {
|
|
||||||
setIsInitialLoading(true);
|
|
||||||
attemptsRef.current = 0;
|
|
||||||
setFetchAttempts(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change in refreshKey will trigger useEffect to run again
|
|
||||||
setRefreshKey((prevKey) => prevKey + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Server related operations
|
|
||||||
const handleServerAdd = () => {
|
|
||||||
setRefreshKey((prevKey) => prevKey + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleServerEdit = async (server: Server) => {
|
|
||||||
try {
|
|
||||||
// Fetch settings to get the full server config before editing
|
|
||||||
const settingsData: ApiResponse<{ mcpServers: Record<string, any> }> =
|
|
||||||
await apiGet('/settings');
|
|
||||||
|
|
||||||
if (
|
|
||||||
settingsData &&
|
|
||||||
settingsData.success &&
|
|
||||||
settingsData.data &&
|
|
||||||
settingsData.data.mcpServers &&
|
|
||||||
settingsData.data.mcpServers[server.name]
|
|
||||||
) {
|
|
||||||
const serverConfig = settingsData.data.mcpServers[server.name];
|
|
||||||
return {
|
|
||||||
name: server.name,
|
|
||||||
status: server.status,
|
|
||||||
tools: server.tools || [],
|
|
||||||
config: serverConfig,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.error('Failed to get server config from settings:', settingsData);
|
|
||||||
setError(t('server.invalidConfig', { serverName: server.name }));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching server settings:', err);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleServerRemove = async (serverName: string) => {
|
|
||||||
try {
|
|
||||||
const result = await apiDelete(`/servers/${serverName}`);
|
|
||||||
|
|
||||||
if (!result || !result.success) {
|
|
||||||
setError(result?.message || t('server.deleteError', { serverName }));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setRefreshKey((prevKey) => prevKey + 1);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
setError(t('errors.general') + ': ' + (err instanceof Error ? err.message : String(err)));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleServerToggle = async (server: Server, enabled: boolean) => {
|
|
||||||
try {
|
|
||||||
const result = await apiPost(`/servers/${server.name}/toggle`, { enabled });
|
|
||||||
|
|
||||||
if (!result || !result.success) {
|
|
||||||
console.error('Failed to toggle server:', result);
|
|
||||||
setError(result?.message || t('server.toggleError', { serverName: server.name }));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the UI immediately to reflect the change
|
|
||||||
setRefreshKey((prevKey) => prevKey + 1);
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error toggling server:', err);
|
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
servers,
|
|
||||||
error,
|
|
||||||
setError,
|
|
||||||
isLoading: isInitialLoading,
|
|
||||||
fetchAttempts,
|
|
||||||
triggerRefresh,
|
|
||||||
handleServerAdd,
|
|
||||||
handleServerEdit,
|
|
||||||
handleServerRemove,
|
|
||||||
handleServerToggle,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useServerData } from '@/hooks/useServerData';
|
import { useServerData } from '@/hooks/useServerData';
|
||||||
|
import { Server } from '@/types';
|
||||||
|
|
||||||
const DashboardPage: React.FC = () => {
|
const DashboardPage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { servers, error, setError, isLoading } = useServerData();
|
const { servers, error, setError, isLoading } = useServerData({ refreshOnMount: true });
|
||||||
|
|
||||||
// Calculate server statistics
|
// Calculate server statistics
|
||||||
const serverStats = {
|
const serverStats = {
|
||||||
total: servers.length,
|
total: servers.length,
|
||||||
online: servers.filter(server => server.status === 'connected').length,
|
online: servers.filter((server: Server) => server.status === 'connected').length,
|
||||||
offline: servers.filter(server => server.status === 'disconnected').length,
|
offline: servers.filter((server: Server) => server.status === 'disconnected').length,
|
||||||
connecting: servers.filter(server => server.status === 'connecting').length
|
connecting: servers.filter((server: Server) => server.status === 'connecting').length
|
||||||
};
|
};
|
||||||
|
|
||||||
// Map status to translation keys
|
// Map status to translation keys
|
||||||
const statusTranslations = {
|
const statusTranslations: Record<string, string> = {
|
||||||
connected: 'status.online',
|
connected: 'status.online',
|
||||||
disconnected: 'status.offline',
|
disconnected: 'status.offline',
|
||||||
connecting: 'status.connecting'
|
connecting: 'status.connecting'
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const GroupsPage: React.FC = () => {
|
|||||||
deleteGroup,
|
deleteGroup,
|
||||||
triggerRefresh
|
triggerRefresh
|
||||||
} = useGroupData();
|
} = useGroupData();
|
||||||
const { servers } = useServerData();
|
const { servers } = useServerData({ refreshOnMount: true });
|
||||||
|
|
||||||
const [editingGroup, setEditingGroup] = useState<Group | null>(null);
|
const [editingGroup, setEditingGroup] = useState<Group | null>(null);
|
||||||
const [showAddForm, setShowAddForm] = useState(false);
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const ServersPage: React.FC = () => {
|
|||||||
handleServerRemove,
|
handleServerRemove,
|
||||||
handleServerToggle,
|
handleServerToggle,
|
||||||
triggerRefresh
|
triggerRefresh
|
||||||
} = useServerData();
|
} = useServerData({ refreshOnMount: true });
|
||||||
const [editingServer, setEditingServer] = useState<Server | null>(null);
|
const [editingServer, setEditingServer] = useState<Server | null>(null);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [showDxtUpload, setShowDxtUpload] = useState(false);
|
const [showDxtUpload, setShowDxtUpload] = useState(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user