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 { 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<Server[]>([])
const [error, setError] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const { t } = useTranslation();
const { createGroup } = useGroupData();
const { allServers } = useServerData();
const [availableServers, setAvailableServers] = useState<Server[]>([]);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<GroupFormData>({
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<HTMLInputElement | HTMLTextAreaElement>) => {
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 (
<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
servers={availableServers}
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"
/>
</div>
@@ -129,7 +129,7 @@ const AddGroupForm = ({ onAdd, onCancel }: AddGroupFormProps) => {
</form>
</div>
</div>
)
}
);
};
export default AddGroupForm
export default AddGroupForm;

View File

@@ -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<Server[]>([])
const [error, setError] = useState<string | null>(null)
const [isSubmitting, setIsSubmitting] = useState(false)
const { t } = useTranslation();
const { updateGroup } = useGroupData();
const { allServers } = useServerData();
const [availableServers, setAvailableServers] = useState<Server[]>([]);
const [error, setError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [formData, setFormData] = useState<GroupFormData>({
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<HTMLInputElement | HTMLTextAreaElement>) => {
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 (
<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
servers={availableServers}
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"
/>
</div>
@@ -135,7 +135,7 @@ const EditGroupForm = ({ group, onEdit, onCancel }: EditGroupFormProps) => {
</form>
</div>
</div>
)
}
);
};
export default EditGroupForm
export default EditGroupForm;

View File

@@ -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<Server[]>([]);
const [allServers, setAllServers] = useState<Server[]>([]); // All servers without pagination
const [error, setError] = useState<string | null>(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,

View File

@@ -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 && (
<div className="mt-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
{t('pages.dashboard.recentServers')}
@@ -244,7 +244,7 @@ const DashboardPage: React.FC = () => {
</tr>
</thead>
<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}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{server.name}

View File

@@ -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<Group | null>(null);
const [showAddForm, setShowAddForm] = useState(false);
@@ -140,7 +140,7 @@ const GroupsPage: React.FC = () => {
<GroupCard
key={group.id}
group={group}
servers={servers}
servers={allServers}
onEdit={handleEditClick}
onDelete={handleDeleteGroup}
/>

View File

@@ -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<{