diff --git a/frontend/src/components/AddGroupForm.tsx b/frontend/src/components/AddGroupForm.tsx index bffc09b..36b9671 100644 --- a/frontend/src/components/AddGroupForm.tsx +++ b/frontend/src/components/AddGroupForm.tsx @@ -1,67 +1,67 @@ -import { useState, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { useGroupData } from '@/hooks/useGroupData' -import { useServerData } from '@/hooks/useServerData' -import { GroupFormData, Server, IGroupServerConfig } from '@/types' -import { ServerToolConfig } from './ServerToolConfig' +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useGroupData } from '@/hooks/useGroupData'; +import { useServerData } from '@/hooks/useServerData'; +import { GroupFormData, Server, IGroupServerConfig } from '@/types'; +import { ServerToolConfig } from './ServerToolConfig'; interface AddGroupFormProps { - onAdd: () => void - onCancel: () => void + onAdd: () => void; + onCancel: () => void; } const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => { - const { t } = useTranslation() - const { createGroup } = useGroupData() - const { servers } = useServerData() - const [availableServers, setAvailableServers] = useState([]) - const [error, setError] = useState(null) - const [isSubmitting, setIsSubmitting] = useState(false) + const { t } = useTranslation(); + const { createGroup } = useGroupData(); + const { allServers } = useServerData(); + const [availableServers, setAvailableServers] = useState([]); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: '', description: '', - servers: [] as IGroupServerConfig[] - }) + servers: [] as IGroupServerConfig[], + }); useEffect(() => { // Filter available servers (enabled only) - setAvailableServers(servers.filter(server => server.enabled !== false)) - }, [servers]) + setAvailableServers(allServers.filter((server) => server.enabled !== false)); + }, [allServers]); const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - setFormData(prev => ({ + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, - [name]: value - })) - } + [name]: value, + })); + }; const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsSubmitting(true) - setError(null) + e.preventDefault(); + setIsSubmitting(true); + setError(null); try { if (!formData.name.trim()) { - setError(t('groups.nameRequired')) - setIsSubmitting(false) - return + setError(t('groups.nameRequired')); + setIsSubmitting(false); + return; } - const result = await createGroup(formData.name, formData.description, formData.servers) + const result = await createGroup(formData.name, formData.description, formData.servers); if (!result || !result.success) { - setError(result?.message || t('groups.createError')) - setIsSubmitting(false) - return + setError(result?.message || t('groups.createError')); + setIsSubmitting(false); + return; } - onAdd() + onAdd(); } catch (err) { - setError(err instanceof Error ? err.message : String(err)) - setIsSubmitting(false) + setError(err instanceof Error ? err.message : String(err)); + setIsSubmitting(false); } - } + }; return (
@@ -102,7 +102,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => { setFormData(prev => ({ ...prev, servers }))} + onChange={(servers) => setFormData((prev) => ({ ...prev, servers }))} className="border border-gray-200 rounded-lg p-4 bg-gray-50" />
@@ -129,7 +129,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => { - ) -} + ); +}; -export default AddGroupForm \ No newline at end of file +export default AddGroupForm; diff --git a/frontend/src/components/EditGroupForm.tsx b/frontend/src/components/EditGroupForm.tsx index 75cf71c..4fd395e 100644 --- a/frontend/src/components/EditGroupForm.tsx +++ b/frontend/src/components/EditGroupForm.tsx @@ -1,73 +1,73 @@ -import { useState, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types' -import { useGroupData } from '@/hooks/useGroupData' -import { useServerData } from '@/hooks/useServerData' -import { ServerToolConfig } from './ServerToolConfig' +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types'; +import { useGroupData } from '@/hooks/useGroupData'; +import { useServerData } from '@/hooks/useServerData'; +import { ServerToolConfig } from './ServerToolConfig'; interface EditGroupFormProps { - group: Group - onEdit: () => void - onCancel: () => void + group: Group; + onEdit: () => void; + onCancel: () => void; } const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => { - const { t } = useTranslation() - const { updateGroup } = useGroupData() - const { servers } = useServerData() - const [availableServers, setAvailableServers] = useState([]) - const [error, setError] = useState(null) - const [isSubmitting, setIsSubmitting] = useState(false) + const { t } = useTranslation(); + const { updateGroup } = useGroupData(); + const { allServers } = useServerData(); + const [availableServers, setAvailableServers] = useState([]); + const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const [formData, setFormData] = useState({ name: group.name, description: group.description || '', - servers: group.servers || [] - }) + servers: group.servers || [], + }); useEffect(() => { // Filter available servers (enabled only) - setAvailableServers(servers.filter(server => server.enabled !== false)) - }, [servers]) + setAvailableServers(allServers.filter((server) => server.enabled !== false)); + }, [allServers]); const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target - setFormData(prev => ({ + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, - [name]: value - })) - } + [name]: value, + })); + }; const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsSubmitting(true) - setError(null) + e.preventDefault(); + setIsSubmitting(true); + setError(null); try { if (!formData.name.trim()) { - setError(t('groups.nameRequired')) - setIsSubmitting(false) - return + setError(t('groups.nameRequired')); + setIsSubmitting(false); + return; } const result = await updateGroup(group.id, { name: formData.name, description: formData.description, - servers: formData.servers - }) + servers: formData.servers, + }); if (!result || !result.success) { - setError(result?.message || t('groups.updateError')) - setIsSubmitting(false) - return + setError(result?.message || t('groups.updateError')); + setIsSubmitting(false); + return; } - onEdit() + onEdit(); } catch (err) { - setError(err instanceof Error ? err.message : String(err)) - setIsSubmitting(false) + setError(err instanceof Error ? err.message : String(err)); + setIsSubmitting(false); } - } + }; return (
@@ -108,7 +108,7 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => { setFormData(prev => ({ ...prev, servers }))} + onChange={(servers) => setFormData((prev) => ({ ...prev, servers }))} className="border border-gray-200 rounded-lg p-4 bg-gray-50" />
@@ -135,7 +135,7 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => { - ) -} + ); +}; -export default EditGroupForm \ No newline at end of file +export default EditGroupForm; diff --git a/frontend/src/contexts/ServerContext.tsx b/frontend/src/contexts/ServerContext.tsx index 3d3f25f..a07ba92 100644 --- a/frontend/src/contexts/ServerContext.tsx +++ b/frontend/src/contexts/ServerContext.tsx @@ -30,6 +30,7 @@ interface PaginationInfo { // Context type definition interface ServerContextType { servers: Server[]; + allServers: Server[]; // All servers without pagination, for Dashboard, Groups, Settings error: string | null; setError: (error: string | null) => void; isLoading: boolean; @@ -56,6 +57,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr const { t } = useTranslation(); const { auth } = useAuth(); const [servers, setServers] = useState([]); + const [allServers, setAllServers] = useState([]); // All servers without pagination const [error, setError] = useState(null); const [refreshKey, setRefreshKey] = useState(0); const [isInitialLoading, setIsInitialLoading] = useState(true); @@ -95,29 +97,44 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr const params = new URLSearchParams(); params.append('page', currentPage.toString()); params.append('limit', serversPerPage.toString()); - const data = await apiGet(`/servers?${params.toString()}`); + + // Fetch both paginated servers and all servers in parallel + const [paginatedData, allData] = await Promise.all([ + apiGet(`/servers?${params.toString()}`), + apiGet('/servers'), // Fetch all servers without pagination + ]); // Update last fetch time lastFetchTimeRef.current = Date.now(); - if (data && data.success && Array.isArray(data.data)) { - setServers(data.data); + // Handle paginated response + if (paginatedData && paginatedData.success && Array.isArray(paginatedData.data)) { + setServers(paginatedData.data); // Update pagination info if available - if (data.pagination) { - setPagination(data.pagination); + if (paginatedData.pagination) { + setPagination(paginatedData.pagination); } else { setPagination(null); } - } else if (data && Array.isArray(data)) { + } else if (paginatedData && Array.isArray(paginatedData)) { // Compatibility handling for non-paginated responses - setServers(data); + setServers(paginatedData); setPagination(null); } else { - console.error('Invalid server data format:', data); + console.error('Invalid server data format:', paginatedData); setServers([]); setPagination(null); } + // Handle all servers response + if (allData && allData.success && Array.isArray(allData.data)) { + setAllServers(allData.data); + } else if (allData && Array.isArray(allData)) { + setAllServers(allData); + } else { + setAllServers([]); + } + // Reset error state setError(null); } catch (err) { @@ -159,6 +176,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr // When user logs out, clear data and stop polling clearTimer(); setServers([]); + setAllServers([]); setIsInitialLoading(false); setError(null); } @@ -185,42 +203,49 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr const params = new URLSearchParams(); params.append('page', currentPage.toString()); params.append('limit', serversPerPage.toString()); - const data = await apiGet(`/servers?${params.toString()}`); + + // Fetch both paginated servers and all servers in parallel + const [paginatedData, allData] = await Promise.all([ + apiGet(`/servers?${params.toString()}`), + apiGet('/servers'), // Fetch all servers without pagination + ]); // 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); + // Handle paginated API response wrapper object, extract data field + if (paginatedData && paginatedData.success && Array.isArray(paginatedData.data)) { + setServers(paginatedData.data); // Update pagination info if available - if (data.pagination) { - setPagination(data.pagination); + if (paginatedData.pagination) { + setPagination(paginatedData.pagination); } else { setPagination(null); } - setIsInitialLoading(false); - // Initialization successful, start normal polling (skip immediate to avoid duplicate fetch) - startNormalPolling({ immediate: false }); - return true; - } else if (data && Array.isArray(data)) { + } else if (paginatedData && Array.isArray(paginatedData)) { // Compatibility handling, if API directly returns array - setServers(data); + setServers(paginatedData); setPagination(null); - 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); + console.error('Invalid server data format:', paginatedData); setServers([]); setPagination(null); - setIsInitialLoading(false); - // Initialization successful but data is empty, start normal polling (skip immediate) - startNormalPolling({ immediate: false }); - return true; } + + // Handle all servers response + if (allData && allData.success && Array.isArray(allData.data)) { + setAllServers(allData.data); + } else if (allData && Array.isArray(allData)) { + setAllServers(allData); + } else { + setAllServers([]); + } + + setIsInitialLoading(false); + // Initialization successful, start normal polling (skip immediate to avoid duplicate fetch) + startNormalPolling({ immediate: false }); + return true; } catch (err) { // Increment attempt count, use ref to avoid triggering effect rerun attemptsRef.current += 1; @@ -439,6 +464,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr const value: ServerContextType = { servers, + allServers, error, setError, isLoading: isInitialLoading, diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 5474fed..ec22c45 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -5,15 +5,15 @@ import { Server } from '@/types'; const DashboardPage: React.FC = () => { const { t } = useTranslation(); - const { servers, error, setError, isLoading } = useServerData({ refreshOnMount: true }); + const { allServers, error, setError, isLoading } = useServerData({ refreshOnMount: true }); - // Calculate server statistics + // Calculate server statistics using allServers (not paginated) const serverStats = { - total: servers.length, - online: servers.filter((server: Server) => server.status === 'connected').length, - offline: servers.filter((server: Server) => server.status === 'disconnected').length, - connecting: servers.filter((server: Server) => server.status === 'connecting').length, - oauthRequired: servers.filter((server: Server) => server.status === 'oauth_required').length, + total: allServers.length, + online: allServers.filter((server: Server) => server.status === 'connected').length, + offline: allServers.filter((server: Server) => server.status === 'disconnected').length, + connecting: allServers.filter((server: Server) => server.status === 'connecting').length, + oauthRequired: allServers.filter((server: Server) => server.status === 'oauth_required').length, }; // Map status to translation keys @@ -202,7 +202,7 @@ const DashboardPage: React.FC = () => { )} {/* Recent activity list */} - {servers.length > 0 && !isLoading && ( + {allServers.length > 0 && !isLoading && (

{t('pages.dashboard.recentServers')} @@ -244,7 +244,7 @@ const DashboardPage: React.FC = () => { - {servers.slice(0, 5).map((server, index) => ( + {allServers.slice(0, 5).map((server, index) => ( {server.name} diff --git a/frontend/src/pages/GroupsPage.tsx b/frontend/src/pages/GroupsPage.tsx index b1f6f01..5a29ad4 100644 --- a/frontend/src/pages/GroupsPage.tsx +++ b/frontend/src/pages/GroupsPage.tsx @@ -18,7 +18,7 @@ const GroupsPage: React.FC = () => { deleteGroup, triggerRefresh, } = useGroupData(); - const { servers } = useServerData({ refreshOnMount: true }); + const { allServers } = useServerData({ refreshOnMount: true }); const [editingGroup, setEditingGroup] = useState(null); const [showAddForm, setShowAddForm] = useState(false); @@ -140,7 +140,7 @@ const GroupsPage: React.FC = () => { diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index bec2369..c7acaeb 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -378,7 +378,7 @@ const SettingsPage: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); const { showToast } = useToast(); - const { servers } = useServerContext(); + const { allServers: servers } = useServerContext(); // Use allServers for settings (not paginated) const { groups } = useGroupData(); const [installConfig, setInstallConfig] = useState<{