Some checks failed
CI / Lint & Typecheck (push) Has been cancelled
CI / Test (routes) (push) Has been cancelled
CI / Test (security) (push) Has been cancelled
CI / Test (services) (push) Has been cancelled
CI / Test (unit) (push) Has been cancelled
CI / Test (integration) (push) Has been cancelled
CI / Test Coverage (push) Has been cancelled
CI / Build (push) Has been cancelled
163 lines
5.7 KiB
TypeScript
163 lines
5.7 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { SERVER_STATS_CONFIG, type ServerResourceDataPoint } from '@tracearr/shared';
|
|
import { toast } from 'sonner';
|
|
import { api } from '@/lib/api';
|
|
import { useRef, useCallback } from 'react';
|
|
|
|
export function useServers() {
|
|
return useQuery({
|
|
queryKey: ['servers', 'list'],
|
|
queryFn: api.servers.list,
|
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
|
});
|
|
}
|
|
|
|
export function useCreateServer() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: { name: string; type: string; url: string; token: string }) =>
|
|
api.servers.create(data),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: ['servers', 'list'] });
|
|
toast.success('Server Added', { description: 'The server has been added successfully.' });
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error('Failed to Add Server', { description: error.message });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteServer() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) => api.servers.delete(id),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: ['servers', 'list'] });
|
|
toast.success('Server Removed', { description: 'The server has been removed successfully.' });
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error('Failed to Remove Server', { description: error.message });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useSyncServer() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: (id: string) => api.servers.sync(id),
|
|
onSuccess: (data) => {
|
|
void queryClient.invalidateQueries({ queryKey: ['servers', 'list'] });
|
|
void queryClient.invalidateQueries({ queryKey: ['users', 'list'] });
|
|
|
|
// Show detailed results
|
|
const parts: string[] = [];
|
|
if (data.usersAdded > 0) parts.push(`${data.usersAdded} users added`);
|
|
if (data.usersUpdated > 0) parts.push(`${data.usersUpdated} users updated`);
|
|
if (data.librariesSynced > 0) parts.push(`${data.librariesSynced} libraries`);
|
|
if (data.errors.length > 0) parts.push(`${data.errors.length} errors`);
|
|
|
|
const description = parts.length > 0
|
|
? parts.join(', ')
|
|
: 'No changes detected';
|
|
|
|
if (data.errors.length > 0) {
|
|
toast.warning(data.success ? 'Sync Completed with Errors' : 'Sync Completed with Errors', { description });
|
|
// Log errors to console for debugging
|
|
console.error('Sync errors:', data.errors);
|
|
} else {
|
|
toast.success('Server Synced', { description });
|
|
}
|
|
},
|
|
onError: (error: Error) => {
|
|
toast.error('Sync Failed', { description: error.message });
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook for fetching server resource statistics with fixed 2-minute window
|
|
* Polls every 10 seconds, displays last 2 minutes of data (12 points)
|
|
* X-axis is static (2m → NOW), data slides through as new points arrive
|
|
*
|
|
* @param serverId - Server ID to fetch stats for
|
|
* @param enabled - Whether polling is enabled (typically tied to component mount)
|
|
*/
|
|
export function useServerStatistics(serverId: string | undefined, enabled: boolean = true) {
|
|
// Accumulate data points across polls, keyed by timestamp for deduplication
|
|
const dataMapRef = useRef<Map<number, ServerResourceDataPoint>>(new Map());
|
|
|
|
// Merge new data with existing, keep most recent DATA_POINTS
|
|
const mergeData = useCallback((newData: ServerResourceDataPoint[]) => {
|
|
const map = dataMapRef.current;
|
|
|
|
// Add/update data points
|
|
for (const point of newData) {
|
|
map.set(point.at, point);
|
|
}
|
|
|
|
// Sort by timestamp descending (newest first), keep DATA_POINTS
|
|
const sorted = Array.from(map.values())
|
|
.sort((a, b) => b.at - a.at)
|
|
.slice(0, SERVER_STATS_CONFIG.DATA_POINTS);
|
|
|
|
// Rebuild map with only kept points
|
|
dataMapRef.current = new Map(sorted.map((p) => [p.at, p]));
|
|
|
|
// Return in ascending order (oldest first) for chart rendering
|
|
return sorted.reverse();
|
|
}, []);
|
|
|
|
const query = useQuery({
|
|
queryKey: ['servers', 'statistics', serverId],
|
|
queryFn: async () => {
|
|
if (!serverId) throw new Error('Server ID required');
|
|
const response = await api.servers.statistics(serverId);
|
|
// Merge with accumulated data
|
|
const mergedData = mergeData(response.data);
|
|
return {
|
|
...response,
|
|
data: mergedData,
|
|
};
|
|
},
|
|
enabled: enabled && !!serverId,
|
|
// Poll every 10 seconds
|
|
refetchInterval: SERVER_STATS_CONFIG.POLL_INTERVAL_SECONDS * 1000,
|
|
// Don't poll when tab is hidden
|
|
refetchIntervalInBackground: false,
|
|
// Don't refetch on window focus (we have interval polling)
|
|
refetchOnWindowFocus: false,
|
|
// Keep previous data while fetching new
|
|
placeholderData: (prev) => prev,
|
|
// Data is fresh until next poll
|
|
staleTime: (SERVER_STATS_CONFIG.POLL_INTERVAL_SECONDS * 1000) - 500,
|
|
});
|
|
|
|
// Calculate averages from windowed data
|
|
const dataPoints = query.data?.data as ServerResourceDataPoint[] | undefined;
|
|
const dataLength = dataPoints?.length ?? 0;
|
|
const averages = dataPoints && dataLength > 0
|
|
? {
|
|
hostCpu: Math.round(
|
|
dataPoints.reduce((sum: number, p) => sum + p.hostCpuUtilization, 0) / dataLength
|
|
),
|
|
processCpu: Math.round(
|
|
dataPoints.reduce((sum: number, p) => sum + p.processCpuUtilization, 0) / dataLength
|
|
),
|
|
hostMemory: Math.round(
|
|
dataPoints.reduce((sum: number, p) => sum + p.hostMemoryUtilization, 0) / dataLength
|
|
),
|
|
processMemory: Math.round(
|
|
dataPoints.reduce((sum: number, p) => sum + p.processMemoryUtilization, 0) / dataLength
|
|
),
|
|
}
|
|
: null;
|
|
|
|
return {
|
|
...query,
|
|
averages,
|
|
};
|
|
}
|