Initial Upload
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
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
This commit is contained in:
162
apps/web/src/hooks/queries/useServers.ts
Normal file
162
apps/web/src/hooks/queries/useServers.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user