1
0
mirror of https://github.com/samanhappy/mcphub.git synced 2026-01-11 09:07:01 -05:00

Enhance server data handling with allServers (#553)

This commit is contained in:
samanhappy
2026-01-07 22:10:44 +08:00
committed by GitHub
parent 3de56b30bd
commit 6b3a077a67
6 changed files with 151 additions and 125 deletions

View File

@@ -1,67 +1,67 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { useGroupData } from '@/hooks/useGroupData' import { useGroupData } from '@/hooks/useGroupData';
import { useServerData } from '@/hooks/useServerData' import { useServerData } from '@/hooks/useServerData';
import { GroupFormData, Server, IGroupServerConfig } from '@/types' import { GroupFormData, Server, IGroupServerConfig } from '@/types';
import { ServerToolConfig } from './ServerToolConfig' import { ServerToolConfig } from './ServerToolConfig';
interface AddGroupFormProps { interface AddGroupFormProps {
onAdd: () => void onAdd: () => void;
onCancel: () => void onCancel: () => void;
} }
const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => { const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
const { t } = useTranslation() const { t } = useTranslation();
const { createGroup } = useGroupData() const { createGroup } = useGroupData();
const { servers } = useServerData() const { allServers } = useServerData();
const [availableServers, setAvailableServers] = useState<Server[]>([]) const [availableServers, setAvailableServers] = useState<Server[]>([]);
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<GroupFormData>({ const [formData, setFormData] = useState<GroupFormData>({
name: '', name: '',
description: '', description: '',
servers: [] as IGroupServerConfig[] servers: [] as IGroupServerConfig[],
}) });
useEffect(() => { useEffect(() => {
// Filter available servers (enabled only) // Filter available servers (enabled only)
setAvailableServers(servers.filter(server => server.enabled !== false)) setAvailableServers(allServers.filter((server) => server.enabled !== false));
}, [servers]) }, [allServers]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target const { name, value } = e.target;
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value [name]: value,
})) }));
} };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
setIsSubmitting(true) setIsSubmitting(true);
setError(null) setError(null);
try { try {
if (!formData.name.trim()) { if (!formData.name.trim()) {
setError(t('groups.nameRequired')) setError(t('groups.nameRequired'));
setIsSubmitting(false) setIsSubmitting(false);
return 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) { if (!result || !result.success) {
setError(result?.message || t('groups.createError')) setError(result?.message || t('groups.createError'));
setIsSubmitting(false) setIsSubmitting(false);
return return;
} }
onAdd() onAdd();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : String(err)) setError(err instanceof Error ? err.message : String(err));
setIsSubmitting(false) setIsSubmitting(false);
}
} }
};
return ( return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
@@ -102,7 +102,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
<ServerToolConfig <ServerToolConfig
servers={availableServers} servers={availableServers}
value={formData.servers as IGroupServerConfig[]} value={formData.servers as IGroupServerConfig[]}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))} onChange={(servers) => setFormData((prev) => ({ ...prev, servers }))}
className="border border-gray-200 rounded-lg p-4 bg-gray-50" className="border border-gray-200 rounded-lg p-4 bg-gray-50"
/> />
</div> </div>
@@ -129,7 +129,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
</form> </form>
</div> </div>
</div> </div>
) );
} };
export default AddGroupForm export default AddGroupForm;

View File

@@ -1,73 +1,73 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types' import { Group, GroupFormData, Server, IGroupServerConfig } from '@/types';
import { useGroupData } from '@/hooks/useGroupData' import { useGroupData } from '@/hooks/useGroupData';
import { useServerData } from '@/hooks/useServerData' import { useServerData } from '@/hooks/useServerData';
import { ServerToolConfig } from './ServerToolConfig' import { ServerToolConfig } from './ServerToolConfig';
interface EditGroupFormProps { interface EditGroupFormProps {
group: Group group: Group;
onEdit: () => void onEdit: () => void;
onCancel: () => void onCancel: () => void;
} }
const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => { const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
const { t } = useTranslation() const { t } = useTranslation();
const { updateGroup } = useGroupData() const { updateGroup } = useGroupData();
const { servers } = useServerData() const { allServers } = useServerData();
const [availableServers, setAvailableServers] = useState<Server[]>([]) const [availableServers, setAvailableServers] = useState<Server[]>([]);
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<GroupFormData>({ const [formData, setFormData] = useState<GroupFormData>({
name: group.name, name: group.name,
description: group.description || '', description: group.description || '',
servers: group.servers || [] servers: group.servers || [],
}) });
useEffect(() => { useEffect(() => {
// Filter available servers (enabled only) // Filter available servers (enabled only)
setAvailableServers(servers.filter(server => server.enabled !== false)) setAvailableServers(allServers.filter((server) => server.enabled !== false));
}, [servers]) }, [allServers]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target const { name, value } = e.target;
setFormData(prev => ({ setFormData((prev) => ({
...prev, ...prev,
[name]: value [name]: value,
})) }));
} };
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
setIsSubmitting(true) setIsSubmitting(true);
setError(null) setError(null);
try { try {
if (!formData.name.trim()) { if (!formData.name.trim()) {
setError(t('groups.nameRequired')) setError(t('groups.nameRequired'));
setIsSubmitting(false) setIsSubmitting(false);
return return;
} }
const result = await updateGroup(group.id, { const result = await updateGroup(group.id, {
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
servers: formData.servers servers: formData.servers,
}) });
if (!result || !result.success) { if (!result || !result.success) {
setError(result?.message || t('groups.updateError')) setError(result?.message || t('groups.updateError'));
setIsSubmitting(false) setIsSubmitting(false);
return return;
} }
onEdit() onEdit();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : String(err)) setError(err instanceof Error ? err.message : String(err));
setIsSubmitting(false) setIsSubmitting(false);
}
} }
};
return ( return (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
@@ -108,7 +108,7 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
<ServerToolConfig <ServerToolConfig
servers={availableServers} servers={availableServers}
value={formData.servers as IGroupServerConfig[]} value={formData.servers as IGroupServerConfig[]}
onChange={(servers) => setFormData(prev => ({ ...prev, servers }))} onChange={(servers) => setFormData((prev) => ({ ...prev, servers }))}
className="border border-gray-200 rounded-lg p-4 bg-gray-50" className="border border-gray-200 rounded-lg p-4 bg-gray-50"
/> />
</div> </div>
@@ -135,7 +135,7 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
</form> </form>
</div> </div>
</div> </div>
) );
} };
export default EditGroupForm export default EditGroupForm;

View File

@@ -30,6 +30,7 @@ interface PaginationInfo {
// Context type definition // Context type definition
interface ServerContextType { interface ServerContextType {
servers: Server[]; servers: Server[];
allServers: Server[]; // All servers without pagination, for Dashboard, Groups, Settings
error: string | null; error: string | null;
setError: (error: string | null) => void; setError: (error: string | null) => void;
isLoading: boolean; isLoading: boolean;
@@ -56,6 +57,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const { t } = useTranslation(); const { t } = useTranslation();
const { auth } = useAuth(); const { auth } = useAuth();
const [servers, setServers] = useState<Server[]>([]); const [servers, setServers] = useState<Server[]>([]);
const [allServers, setAllServers] = useState<Server[]>([]); // All servers without pagination
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
const [isInitialLoading, setIsInitialLoading] = useState(true); const [isInitialLoading, setIsInitialLoading] = useState(true);
@@ -95,29 +97,44 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('page', currentPage.toString()); params.append('page', currentPage.toString());
params.append('limit', serversPerPage.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 // Update last fetch time
lastFetchTimeRef.current = Date.now(); lastFetchTimeRef.current = Date.now();
if (data && data.success && Array.isArray(data.data)) { // Handle paginated response
setServers(data.data); if (paginatedData && paginatedData.success && Array.isArray(paginatedData.data)) {
setServers(paginatedData.data);
// Update pagination info if available // Update pagination info if available
if (data.pagination) { if (paginatedData.pagination) {
setPagination(data.pagination); setPagination(paginatedData.pagination);
} else { } else {
setPagination(null); setPagination(null);
} }
} else if (data && Array.isArray(data)) { } else if (paginatedData && Array.isArray(paginatedData)) {
// Compatibility handling for non-paginated responses // Compatibility handling for non-paginated responses
setServers(data); setServers(paginatedData);
setPagination(null); setPagination(null);
} else { } else {
console.error('Invalid server data format:', data); console.error('Invalid server data format:', paginatedData);
setServers([]); setServers([]);
setPagination(null); 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 // Reset error state
setError(null); setError(null);
} catch (err) { } catch (err) {
@@ -159,6 +176,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
// When user logs out, clear data and stop polling // When user logs out, clear data and stop polling
clearTimer(); clearTimer();
setServers([]); setServers([]);
setAllServers([]);
setIsInitialLoading(false); setIsInitialLoading(false);
setError(null); setError(null);
} }
@@ -185,42 +203,49 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const params = new URLSearchParams(); const params = new URLSearchParams();
params.append('page', currentPage.toString()); params.append('page', currentPage.toString());
params.append('limit', serversPerPage.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 // Update last fetch time
lastFetchTimeRef.current = Date.now(); lastFetchTimeRef.current = Date.now();
// Handle API response wrapper object, extract data field // Handle paginated API response wrapper object, extract data field
if (data && data.success && Array.isArray(data.data)) { if (paginatedData && paginatedData.success && Array.isArray(paginatedData.data)) {
setServers(data.data); setServers(paginatedData.data);
// Update pagination info if available // Update pagination info if available
if (data.pagination) { if (paginatedData.pagination) {
setPagination(data.pagination); setPagination(paginatedData.pagination);
} else { } else {
setPagination(null); setPagination(null);
} }
setIsInitialLoading(false); } else if (paginatedData && Array.isArray(paginatedData)) {
// 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 // Compatibility handling, if API directly returns array
setServers(data); setServers(paginatedData);
setPagination(null); setPagination(null);
setIsInitialLoading(false);
// Initialization successful, start normal polling (skip immediate to avoid duplicate fetch)
startNormalPolling({ immediate: false });
return true;
} else { } else {
// If data format is not as expected, set to empty array // 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([]); setServers([]);
setPagination(null); 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([]);
}
setIsInitialLoading(false); setIsInitialLoading(false);
// Initialization successful but data is empty, start normal polling (skip immediate) // Initialization successful, start normal polling (skip immediate to avoid duplicate fetch)
startNormalPolling({ immediate: false }); startNormalPolling({ immediate: false });
return true; return true;
}
} catch (err) { } catch (err) {
// Increment attempt count, use ref to avoid triggering effect rerun // Increment attempt count, use ref to avoid triggering effect rerun
attemptsRef.current += 1; attemptsRef.current += 1;
@@ -439,6 +464,7 @@ export const ServerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
const value: ServerContextType = { const value: ServerContextType = {
servers, servers,
allServers,
error, error,
setError, setError,
isLoading: isInitialLoading, isLoading: isInitialLoading,

View File

@@ -5,15 +5,15 @@ import { Server } from '@/types';
const DashboardPage: React.FC = () => { const DashboardPage: React.FC = () => {
const { t } = useTranslation(); 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 = { const serverStats = {
total: servers.length, total: allServers.length,
online: servers.filter((server: Server) => server.status === 'connected').length, online: allServers.filter((server: Server) => server.status === 'connected').length,
offline: servers.filter((server: Server) => server.status === 'disconnected').length, offline: allServers.filter((server: Server) => server.status === 'disconnected').length,
connecting: servers.filter((server: Server) => server.status === 'connecting').length, connecting: allServers.filter((server: Server) => server.status === 'connecting').length,
oauthRequired: servers.filter((server: Server) => server.status === 'oauth_required').length, oauthRequired: allServers.filter((server: Server) => server.status === 'oauth_required').length,
}; };
// Map status to translation keys // Map status to translation keys
@@ -202,7 +202,7 @@ const DashboardPage: React.FC = () => {
)} )}
{/* Recent activity list */} {/* Recent activity list */}
{servers.length > 0 && !isLoading && ( {allServers.length > 0 && !isLoading && (
<div className="mt-8"> <div className="mt-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4"> <h2 className="text-xl font-semibold text-gray-900 mb-4">
{t('pages.dashboard.recentServers')} {t('pages.dashboard.recentServers')}
@@ -244,7 +244,7 @@ const DashboardPage: React.FC = () => {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{servers.slice(0, 5).map((server, index) => ( {allServers.slice(0, 5).map((server, index) => (
<tr key={index}> <tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{server.name} {server.name}

View File

@@ -18,7 +18,7 @@ const GroupsPage: React.FC = () => {
deleteGroup, deleteGroup,
triggerRefresh, triggerRefresh,
} = useGroupData(); } = useGroupData();
const { servers } = useServerData({ refreshOnMount: true }); const { allServers } = 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);
@@ -140,7 +140,7 @@ const GroupsPage: React.FC = () => {
<GroupCard <GroupCard
key={group.id} key={group.id}
group={group} group={group}
servers={servers} servers={allServers}
onEdit={handleEditClick} onEdit={handleEditClick}
onDelete={handleDeleteGroup} onDelete={handleDeleteGroup}
/> />

View File

@@ -378,7 +378,7 @@ const SettingsPage: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { showToast } = useToast(); const { showToast } = useToast();
const { servers } = useServerContext(); const { allServers: servers } = useServerContext(); // Use allServers for settings (not paginated)
const { groups } = useGroupData(); const { groups } = useGroupData();
const [installConfig, setInstallConfig] = useState<{ const [installConfig, setInstallConfig] = useState<{