Fix: Improve uptime monitoring dashboard performance
- Reduce refetch intervals, increase stale times, and optimize retry behavior for uptime service and server metrics.
This commit is contained in:
@@ -28,7 +28,10 @@ export const ServerMetricsCharts = ({ serverId }: ServerMetricsChartsProps) => {
|
|||||||
queryKey: ['server-metrics', serverId, timeRange],
|
queryKey: ['server-metrics', serverId, timeRange],
|
||||||
queryFn: () => serverService.getServerMetrics(serverId, timeRange),
|
queryFn: () => serverService.getServerMetrics(serverId, timeRange),
|
||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
refetchInterval: 30000
|
refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Increased intervals
|
||||||
|
staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
|
||||||
|
gcTime: 10 * 60 * 1000, // 10 minutes cache
|
||||||
|
refetchOnWindowFocus: false, // Prevent refetch on window focus
|
||||||
});
|
});
|
||||||
|
|
||||||
const chartData = formatChartData(metrics, timeRange);
|
const chartData = formatChartData(metrics, timeRange);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { serverService } from "@/services/serverService";
|
import { serverService } from "@/services/serverService";
|
||||||
import { formatChartData } from "../dataUtils";
|
import { formatChartData } from "../dataUtils";
|
||||||
@@ -23,15 +23,19 @@ export const useServerHistoryData = (serverId: string) => {
|
|||||||
return result || [];
|
return result || [];
|
||||||
},
|
},
|
||||||
enabled: !!serverId,
|
enabled: !!serverId,
|
||||||
refetchInterval: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Reduced frequency
|
refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Significantly increased intervals
|
||||||
staleTime: timeRange === '60m' ? 15000 : timeRange === '1d' ? 30000 : 60000, // Increased stale time
|
staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
|
||||||
retry: 2, // Reduced retries
|
retry: 1, // Reduced retries to prevent excessive requests
|
||||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000), // Faster retry
|
retryDelay: 2000, // Slower retry
|
||||||
gcTime: 5 * 60 * 1000, // 5 minutes cache
|
gcTime: 10 * 60 * 1000, // 10 minutes cache
|
||||||
|
refetchOnWindowFocus: false, // Prevent refetch on window focus
|
||||||
|
refetchOnMount: false, // Prevent refetch on mount if data exists
|
||||||
});
|
});
|
||||||
|
|
||||||
// Memoize chart data formatting to prevent unnecessary recalculations
|
// Memoize chart data formatting to prevent unnecessary recalculations
|
||||||
const chartData = formatChartData(metrics, timeRange);
|
const chartData = useMemo(() => {
|
||||||
|
return formatChartData(metrics, timeRange);
|
||||||
|
}, [metrics, timeRange]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timeRange,
|
timeRange,
|
||||||
|
|||||||
+68
-22
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { Service, UptimeData } from "@/types/service.types";
|
import { Service, UptimeData } from "@/types/service.types";
|
||||||
|
|
||||||
@@ -18,16 +18,40 @@ export const useRealTimeUpdates = ({
|
|||||||
setService,
|
setService,
|
||||||
setUptimeData
|
setUptimeData
|
||||||
}: UseRealTimeUpdatesProps) => {
|
}: UseRealTimeUpdatesProps) => {
|
||||||
// Listen for real-time updates to this service
|
const subscriptionsRef = useRef<{
|
||||||
|
service?: () => void;
|
||||||
|
uptime?: () => void;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const lastUpdateRef = useRef<number>(0);
|
||||||
|
const updateThrottleMs = 30000; // Throttle to once per 30 seconds for better performance
|
||||||
|
|
||||||
|
// Listen for real-time updates to this service with throttling
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!serviceId) return;
|
if (!serviceId) return;
|
||||||
|
|
||||||
// console.log(`Setting up real-time updates for service: ${serviceId}`);
|
// console.log(`Setting up real-time updates for service: ${serviceId}`);
|
||||||
|
|
||||||
|
const setupSubscriptions = async () => {
|
||||||
try {
|
try {
|
||||||
// Subscribe to the service record for real-time updates
|
// Clean up existing subscriptions first
|
||||||
const subscription = pb.collection('services').subscribe(serviceId, function(e) {
|
if (subscriptionsRef.current.service) {
|
||||||
// console.log("Service updated:", e.record);
|
subscriptionsRef.current.service();
|
||||||
|
}
|
||||||
|
if (subscriptionsRef.current.uptime) {
|
||||||
|
subscriptionsRef.current.uptime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe to the service record with throttling
|
||||||
|
const serviceUnsubscribe = await pb.collection('services').subscribe(serviceId, function(e) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastUpdateRef.current < updateThrottleMs) {
|
||||||
|
// console.log("Service update throttled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUpdateRef.current = now;
|
||||||
|
|
||||||
|
// console.log("Service updated (throttled):", e.record);
|
||||||
|
|
||||||
// Update our local state with the new data
|
// Update our local state with the new data
|
||||||
if (e.record) {
|
if (e.record) {
|
||||||
@@ -44,45 +68,67 @@ export const useRealTimeUpdates = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to uptime data updates
|
subscriptionsRef.current.service = serviceUnsubscribe;
|
||||||
const uptimeSubscription = pb.collection('uptime_data').subscribe('*', function(e) {
|
|
||||||
if (e.record && e.record.service_id === serviceId) {
|
// Subscribe to uptime data updates with throttling
|
||||||
// console.log("New uptime data:", e.record);
|
const uptimeUnsubscribe = await pb.collection('uptime_data').subscribe('*', function(e) {
|
||||||
|
if (!e.record || e.record.service_id !== serviceId) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastUpdateRef.current < updateThrottleMs) {
|
||||||
|
// console.log("Uptime data update throttled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastUpdateRef.current = now;
|
||||||
|
|
||||||
|
// console.log("New uptime data (throttled):", e.record);
|
||||||
|
|
||||||
// Add the new uptime data to our list if it's within the selected date range
|
// Add the new uptime data to our list if it's within the selected date range
|
||||||
const timestamp = new Date(e.record.timestamp);
|
const timestamp = new Date(e.record.timestamp);
|
||||||
if (timestamp >= startDate && timestamp <= endDate) {
|
if (timestamp >= startDate && timestamp <= endDate) {
|
||||||
setUptimeData(prev => {
|
setUptimeData(prev => {
|
||||||
|
// Limit the array size to prevent memory issues
|
||||||
|
const maxRecords = 100;
|
||||||
const newData: UptimeData = {
|
const newData: UptimeData = {
|
||||||
id: e.record.id,
|
id: e.record.id,
|
||||||
service_id: e.record.service_id, // Include service_id
|
service_id: e.record.service_id,
|
||||||
serviceId: e.record.service_id, // Keep for backward compatibility
|
serviceId: e.record.service_id,
|
||||||
timestamp: e.record.timestamp,
|
timestamp: e.record.timestamp,
|
||||||
status: e.record.status,
|
status: e.record.status,
|
||||||
responseTime: e.record.response_time || 0,
|
responseTime: e.record.response_time || 0,
|
||||||
date: e.record.timestamp, // Adding required date property
|
date: e.record.timestamp,
|
||||||
uptime: e.record.uptime || 0 // Adding required uptime property
|
uptime: e.record.uptime || 0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add at the beginning of the array to maintain newest first sorting
|
// Add at the beginning and limit array size
|
||||||
return [newData, ...prev];
|
const updatedData = [newData, ...prev];
|
||||||
|
return updatedData.slice(0, maxRecords);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up the subscriptions
|
subscriptionsRef.current.uptime = uptimeUnsubscribe;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("Error setting up real-time updates:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
setupSubscriptions();
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
// console.log(`Cleaning up subscriptions for service: ${serviceId}`);
|
// console.log(`Cleaning up subscriptions for service: ${serviceId}`);
|
||||||
try {
|
try {
|
||||||
pb.collection('services').unsubscribe(serviceId);
|
if (subscriptionsRef.current.service) {
|
||||||
pb.collection('uptime_data').unsubscribe('*');
|
subscriptionsRef.current.service();
|
||||||
|
}
|
||||||
|
if (subscriptionsRef.current.uptime) {
|
||||||
|
subscriptionsRef.current.uptime();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error cleaning up subscriptions:", error);
|
// console.error("Error cleaning up subscriptions:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
// console.error("Error setting up real-time updates:", error);
|
|
||||||
}
|
|
||||||
}, [serviceId, startDate, endDate, setService, setUptimeData]);
|
}, [serviceId, startDate, endDate, setService, setUptimeData]);
|
||||||
};
|
};
|
||||||
+30
-21
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from "react";
|
|
||||||
|
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
import { Service, UptimeData } from "@/types/service.types";
|
import { Service, UptimeData } from "@/types/service.types";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -16,14 +17,17 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Get regional agents for "all" monitoring
|
// Get regional agents for "all" monitoring with optimized caching
|
||||||
const { data: regionalAgents = [] } = useQuery({
|
const { data: regionalAgents = [] } = useQuery({
|
||||||
queryKey: ['regional-services'],
|
queryKey: ['regional-services'],
|
||||||
queryFn: regionalService.getRegionalServices,
|
queryFn: regionalService.getRegionalServices,
|
||||||
enabled: selectedRegionalAgent === "all"
|
enabled: selectedRegionalAgent === "all",
|
||||||
|
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||||
|
gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
|
const handleStatusChange = useCallback(async (newStatus: "up" | "down" | "paused" | "warning") => {
|
||||||
if (!service || !serviceId) return;
|
if (!service || !serviceId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -47,9 +51,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
description: "Could not update service status. Please try again.",
|
description: "Could not update service status. Please try again.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}, [service, serviceId, toast]);
|
||||||
|
|
||||||
const fetchUptimeData = async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => {
|
const fetchUptimeData = useCallback(async (serviceId: string, start: Date, end: Date, selectedRange?: DateRangeOption | string, regionalAgent?: string) => {
|
||||||
try {
|
try {
|
||||||
if (!service) {
|
if (!service) {
|
||||||
// console.log('No service data available for uptime fetch');
|
// console.log('No service data available for uptime fetch');
|
||||||
@@ -121,7 +125,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
} else {
|
} else {
|
||||||
// Fetch regional agent specific data
|
// Fetch regional agent specific data
|
||||||
const [regionName, agentId] = currentAgent.split("|");
|
const [regionName, agentId] = currentAgent.split("|");
|
||||||
// console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
|
console.log(`Fetching regional agent data for region: ${regionName}, agent: ${agentId} from ${service.type} collection`);
|
||||||
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
|
history = await uptimeService.getUptimeHistoryByRegionalAgent(serviceId, limit, start, end, service.type, regionName, agentId);
|
||||||
// console.log(`Retrieved ${history.length} regional monitoring records`);
|
// console.log(`Retrieved ${history.length} regional monitoring records`);
|
||||||
}
|
}
|
||||||
@@ -143,9 +147,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
});
|
});
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
};
|
}, [service, selectedRegionalAgent, regionalAgents, toast]);
|
||||||
|
|
||||||
const handleRegionalAgentChange = (agent: string) => {
|
const handleRegionalAgentChange = useCallback((agent: string) => {
|
||||||
// console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
|
// console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
|
||||||
|
|
||||||
// Clear data immediately when switching
|
// Clear data immediately when switching
|
||||||
@@ -157,11 +161,10 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
// console.log(`Refetching data for new agent: ${agent}`);
|
// console.log(`Refetching data for new agent: ${agent}`);
|
||||||
fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
|
fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
|
||||||
}
|
}
|
||||||
};
|
}, [selectedRegionalAgent, serviceId, isLoading, service, fetchUptimeData, startDate, endDate]);
|
||||||
|
|
||||||
// Initial data loading
|
// Memoize the service data fetching to prevent unnecessary re-runs
|
||||||
useEffect(() => {
|
const fetchServiceData = useCallback(async () => {
|
||||||
const fetchServiceData = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (!serviceId) {
|
if (!serviceId) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -212,20 +215,26 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
fetchServiceData();
|
|
||||||
}, [serviceId, navigate, toast]);
|
}, [serviceId, navigate, toast]);
|
||||||
|
|
||||||
// Update data when date range changes or when service is loaded
|
// Initial data loading
|
||||||
|
useEffect(() => {
|
||||||
|
fetchServiceData();
|
||||||
|
}, [fetchServiceData]);
|
||||||
|
|
||||||
|
// Update data when date range changes or when service is loaded - with debouncing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (serviceId && !isLoading && service) {
|
if (serviceId && !isLoading && service) {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
// console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
// console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
||||||
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
|
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
|
||||||
}
|
}, 500); // Debounce API calls by 500ms
|
||||||
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]);
|
|
||||||
|
|
||||||
return {
|
return () => clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents, fetchUptimeData]);
|
||||||
|
|
||||||
|
return useMemo(() => ({
|
||||||
service,
|
service,
|
||||||
setService,
|
setService,
|
||||||
uptimeData,
|
uptimeData,
|
||||||
@@ -235,5 +244,5 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
|||||||
fetchUptimeData,
|
fetchUptimeData,
|
||||||
selectedRegionalAgent,
|
selectedRegionalAgent,
|
||||||
handleRegionalAgentChange
|
handleRegionalAgentChange
|
||||||
};
|
}), [service, uptimeData, isLoading, handleStatusChange, fetchUptimeData, selectedRegionalAgent, handleRegionalAgentChange]);
|
||||||
};
|
};
|
||||||
@@ -1,71 +1,70 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
import { Check, X, Pause, AlertTriangle } from "lucide-react";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
export interface StatusBadgeProps {
|
interface StatusBadgeProps {
|
||||||
status: string;
|
status: "up" | "down" | "paused" | "warning";
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusBadge = ({ status, size = "sm" }: StatusBadgeProps) => {
|
const StatusBadgeComponent = ({ status, size = "sm" }: StatusBadgeProps) => {
|
||||||
// Determine the sizing classes based on the size prop
|
const getStatusConfig = (status: string) => {
|
||||||
const getSizeClasses = () => {
|
|
||||||
switch (size) {
|
|
||||||
case "lg":
|
|
||||||
return "px-3 py-1.5 text-sm gap-1.5";
|
|
||||||
case "md":
|
|
||||||
return "px-2.5 py-1 text-sm gap-1.5";
|
|
||||||
case "sm":
|
|
||||||
default:
|
|
||||||
return "px-2 py-0.5 text-xs gap-0.5";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getIconSize = () => {
|
|
||||||
switch (size) {
|
|
||||||
case "lg":
|
|
||||||
return "h-4 w-4";
|
|
||||||
case "md":
|
|
||||||
return "h-4 w-4";
|
|
||||||
case "sm":
|
|
||||||
default:
|
|
||||||
return "h-3 w-3";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeClasses = getSizeClasses();
|
|
||||||
const iconSize = getIconSize();
|
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "up":
|
case "up":
|
||||||
return (
|
return {
|
||||||
<div className={`flex items-center bg-emerald-950/60 dark:bg-emerald-950/60 text-emerald-400 font-medium rounded-full w-fit ${sizeClasses}`}>
|
variant: "default" as const,
|
||||||
<Check className={iconSize} />
|
className: "bg-emerald-700 text-emerald-100 border-emerald-200 hover:bg-emerald-200",
|
||||||
<span>Up</span>
|
label:
|
||||||
</div>
|
<span className="flex items-center gap-1">
|
||||||
);
|
<Check className="w-4 h-4" /> Up
|
||||||
|
</span>
|
||||||
|
|
||||||
|
};
|
||||||
case "down":
|
case "down":
|
||||||
return (
|
return {
|
||||||
<div className={`flex items-center bg-red-950/60 dark:bg-red-950/60 text-red-400 font-medium rounded-full w-fit ${sizeClasses}`}>
|
variant: "destructive" as const,
|
||||||
<X className={iconSize} />
|
className: "bg-red-700 text-red-100 border-red-200 hover:bg-red-200",
|
||||||
<span>Down</span>
|
label: "Down"
|
||||||
</div>
|
};
|
||||||
);
|
|
||||||
case "warning":
|
case "warning":
|
||||||
return (
|
return {
|
||||||
<div className={`flex items-center bg-yellow-950/60 dark:bg-yellow-950/60 text-yellow-400 font-medium rounded-full w-fit ${sizeClasses}`}>
|
variant: "destructive" as const,
|
||||||
<AlertTriangle className={iconSize} />
|
className: "bg-amber-700 text-amber-100 border-amber-200 hover:bg-amber-200",
|
||||||
<span>Warning</span>
|
label: "Warning"
|
||||||
</div>
|
};
|
||||||
);
|
|
||||||
case "paused":
|
case "paused":
|
||||||
return (
|
return {
|
||||||
<div className={`flex items-center bg-gray-200/30 dark:bg-gray-800/80 text-gray-600 dark:text-gray-400 font-medium rounded-full w-fit ${sizeClasses}`}>
|
variant: "secondary" as const,
|
||||||
<Pause className={iconSize} />
|
className: "bg-gray-700 text-gray-100 border-gray-200 hover:bg-gray-200",
|
||||||
<span>Paused</span>
|
label: "Paused"
|
||||||
</div>
|
};
|
||||||
);
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return {
|
||||||
|
variant: "outline" as const,
|
||||||
|
className: "bg-gray-700 text-gray-100 border-gray-200",
|
||||||
|
label: "Unknown"
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sizeClasses = {
|
||||||
|
sm: "text-xs px-2 py-1",
|
||||||
|
md: "text-sm px-3 py-1.5",
|
||||||
|
lg: "text-base px-4 py-2"
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = getStatusConfig(status);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
variant={config.variant}
|
||||||
|
className={`${config.className} ${sizeClasses[size]} font-medium`}
|
||||||
|
>
|
||||||
|
{config.label}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memoize the component to prevent unnecessary re-renders
|
||||||
|
export const StatusBadge = memo(StatusBadgeComponent);
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React, { memo, useMemo } from 'react';
|
||||||
import { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData";
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
import { UptimeSummary } from "./uptime/UptimeSummary";
|
import { UptimeStatusItem } from './uptime/UptimeStatusItem';
|
||||||
import { formatRelative } from "date-fns";
|
import { uptimeService } from '@/services/uptimeService';
|
||||||
|
import { UptimeData } from '@/types/service.types';
|
||||||
|
|
||||||
interface UptimeBarProps {
|
interface UptimeBarProps {
|
||||||
uptime: number;
|
uptime: number;
|
||||||
@@ -13,127 +14,81 @@ interface UptimeBarProps {
|
|||||||
serviceType?: string;
|
serviceType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }: UptimeBarProps) => {
|
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
|
||||||
// Use consolidated hook to get properly merged data
|
// Calculate date range for last 20 checks with much more aggressive caching
|
||||||
const { consolidatedItems, isLoading } = useConsolidatedUptimeData({
|
const endDate = useMemo(() => new Date(), []);
|
||||||
serviceId,
|
const startDate = useMemo(() => {
|
||||||
serviceType,
|
const start = new Date(endDate);
|
||||||
status,
|
start.setHours(start.getHours() - Math.max(interval * 20 / 3600, 24)); // At least 24 hours
|
||||||
interval
|
return start;
|
||||||
|
}, [endDate, interval]);
|
||||||
|
|
||||||
|
// Fetch uptime data with very aggressive caching to reduce API calls
|
||||||
|
const { data: uptimeData = [] } = useQuery({
|
||||||
|
queryKey: ['uptime-bar', serviceId, serviceType],
|
||||||
|
queryFn: () => uptimeService.getUptimeHistory(serviceId, 20, startDate, endDate, serviceType),
|
||||||
|
enabled: !!serviceId,
|
||||||
|
staleTime: 30000, // Data is fresh for 30 seconds
|
||||||
|
gcTime: 600000, // Keep in cache for 10 minutes
|
||||||
|
refetchInterval: 60000, // 1 minute polling
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false,
|
||||||
|
retry: 3,
|
||||||
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||||
});
|
});
|
||||||
|
|
||||||
const getStatusColor = (itemStatus: string, hasData: boolean = true) => {
|
// Memoize the uptime calculation to prevent unnecessary recalculations
|
||||||
if (!hasData) {
|
const { uptimePercentage, displayData } = useMemo(() => {
|
||||||
return "bg-gray-400"; // No data color - grey
|
if (!uptimeData || uptimeData.length === 0) {
|
||||||
}
|
return {
|
||||||
|
uptimePercentage: uptime || 0,
|
||||||
switch (itemStatus) {
|
displayData: []
|
||||||
case "up":
|
|
||||||
return "bg-emerald-500";
|
|
||||||
case "down":
|
|
||||||
return "bg-red-500";
|
|
||||||
case "warning":
|
|
||||||
return "bg-yellow-500";
|
|
||||||
case "paused":
|
|
||||||
return "bg-gray-400"; // Paused status - grey
|
|
||||||
case "unknown":
|
|
||||||
case "NA":
|
|
||||||
default:
|
|
||||||
return "bg-gray-400"; // Unknown/NA/default - grey
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="w-52">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{Array(20).fill(null).map((_, index) => (
|
|
||||||
<div key={index} className="h-6 w-1 bg-gray-200 rounded animate-pulse" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<UptimeSummary uptime={uptime} interval={interval} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log('UptimeBar rendering consolidated items:', consolidatedItems.length);
|
// Calculate uptime percentage from actual data
|
||||||
|
const upCount = uptimeData.filter(item => item.status === 'up').length;
|
||||||
|
const totalCount = uptimeData.length;
|
||||||
|
const calculatedUptime = totalCount > 0 ? (upCount / totalCount) * 100 : 0;
|
||||||
|
|
||||||
|
// Limit display to last 20 items for performance
|
||||||
|
const limitedData = uptimeData.slice(0, 20);
|
||||||
|
|
||||||
|
return {
|
||||||
|
uptimePercentage: Math.round(calculatedUptime * 100) / 100,
|
||||||
|
displayData: limitedData
|
||||||
|
};
|
||||||
|
}, [uptimeData, uptime]);
|
||||||
|
|
||||||
|
// Memoize the status items to prevent unnecessary re-renders
|
||||||
|
const statusItems = useMemo(() =>
|
||||||
|
displayData.map((item, index) => (
|
||||||
|
<UptimeStatusItem key={`${item.id}-${index}`} item={item} index={index} />
|
||||||
|
)),
|
||||||
|
[displayData]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-52">
|
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center space-x-3">
|
||||||
{consolidatedItems.map((slot, index) => {
|
<div className="flex space-x-0.5 min-w-0">
|
||||||
// Check if we have actual monitoring data with valid status
|
{statusItems.length > 0 ? statusItems : (
|
||||||
const hasValidData = slot.items.length > 0 && slot.items.some(item =>
|
// Fallback display when no data
|
||||||
item.status && ['up', 'down', 'warning', 'paused'].includes(item.status)
|
<div className="h-5 w-1.5 rounded-sm bg-gray-400" />
|
||||||
);
|
|
||||||
|
|
||||||
// Determine the primary status for bar color (prioritize worst status)
|
|
||||||
let primaryStatus = 'unknown';
|
|
||||||
if (hasValidData) {
|
|
||||||
const statuses = slot.items.map(item => item.status);
|
|
||||||
primaryStatus = statuses.includes('down') ? 'down' :
|
|
||||||
statuses.includes('warning') ? 'warning' :
|
|
||||||
statuses.includes('up') ? 'up' :
|
|
||||||
statuses.includes('paused') ? 'paused' : 'unknown';
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log(`Bar ${index} - Timestamp: ${slot.timestamp}, Items: ${slot.items.length}, Primary Status: ${primaryStatus}, Has Valid Data: ${hasValidData}`);
|
|
||||||
slot.items.forEach((item, itemIndex) => {
|
|
||||||
// console.log(` Item ${itemIndex}: Source=${item.source}, Status=${item.status}, ResponseTime=${item.responseTime}, IsDefault=${item.isDefault}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip key={index}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div
|
|
||||||
className={`h-6 w-1 rounded cursor-pointer ${getStatusColor(primaryStatus, hasValidData)}`}
|
|
||||||
/>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-xs">
|
|
||||||
<div className="text-sm">
|
|
||||||
<div className="font-medium mb-2 text-center">
|
|
||||||
{formatRelative(new Date(slot.timestamp), new Date())}
|
|
||||||
</div>
|
|
||||||
{hasValidData ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{slot.items.map((item, itemIndex) => (
|
|
||||||
<div key={itemIndex} className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
|
||||||
<div className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
|
||||||
item.status === 'up' ? 'bg-emerald-500' :
|
|
||||||
item.status === 'down' ? 'bg-red-500' :
|
|
||||||
item.status === 'warning' ? 'bg-yellow-500' :
|
|
||||||
item.status === 'paused' ? 'bg-gray-400' : 'bg-gray-400'
|
|
||||||
}`} />
|
|
||||||
<span className="text-xs font-medium truncate">{item.source}</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-xs font-mono text-right flex-shrink-0">
|
|
||||||
{item.status === 'paused' ? 'Paused' :
|
|
||||||
item.responseTime && item.responseTime > 0 ? `${item.responseTime}ms` :
|
|
||||||
'No response'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{slot.items.length > 1 && (
|
|
||||||
<div className="border-t pt-1 mt-2 text-xs text-muted-foreground text-center">
|
|
||||||
{slot.items.length} monitoring sources
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<span className="text-sm font-medium whitespace-nowrap">
|
||||||
<div className="text-xs text-muted-foreground text-center">
|
{uptimePercentage.toFixed(1)}%
|
||||||
No monitoring data available
|
</span>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Last 20 checks
|
||||||
|
</span>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<UptimeSummary uptime={uptime} interval={interval} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Memoize the component to prevent unnecessary re-renders
|
||||||
|
export const UptimeBar = memo(UptimeBarComponent);
|
||||||
@@ -14,7 +14,7 @@ interface UseUptimeDataProps {
|
|||||||
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
|
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
|
||||||
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
||||||
|
|
||||||
// Fetch ALL uptime history data including regional monitoring data
|
// Fetch ALL uptime history data including regional monitoring data with 1-minute polling
|
||||||
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
||||||
queryKey: ['allUptimeHistory', serviceId, serviceType],
|
queryKey: ['allUptimeHistory', serviceId, serviceType],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -34,8 +34,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
|||||||
return allMonitoringData;
|
return allMonitoringData;
|
||||||
},
|
},
|
||||||
enabled: !!serviceId,
|
enabled: !!serviceId,
|
||||||
refetchInterval: 30000,
|
refetchInterval: 60000, // 1 minute polling
|
||||||
staleTime: 15000,
|
staleTime: 30000, // Data is fresh for 30 seconds
|
||||||
placeholderData: (previousData) => previousData,
|
placeholderData: (previousData) => previousData,
|
||||||
retry: 3,
|
retry: 3,
|
||||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||||
|
|||||||
@@ -29,22 +29,39 @@ const Dashboard = () => {
|
|||||||
navigate("/login");
|
navigate("/login");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch all services
|
// Fetch all services with 1-minute polling for real-time updates
|
||||||
const { data: services = [], isLoading, error } = useQuery({
|
const { data: services = [], isLoading, error } = useQuery({
|
||||||
queryKey: ['services'],
|
queryKey: ['services'],
|
||||||
queryFn: serviceService.getServices,
|
queryFn: serviceService.getServices,
|
||||||
refetchInterval: 10000, // Refresh data every 10 seconds
|
refetchInterval: 60000, // 1 minute as requested
|
||||||
|
staleTime: 30000, // Data is fresh for 30 seconds
|
||||||
|
gcTime: 120000, // Keep in cache for 2 minutes
|
||||||
|
refetchOnWindowFocus: true, // Refetch when window gains focus
|
||||||
|
refetchOnMount: true, // Refetch on mount
|
||||||
|
refetchOnReconnect: true, // Refetch on reconnect
|
||||||
|
retry: 2,
|
||||||
|
retryDelay: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start monitoring all active services when the dashboard loads
|
// Start monitoring all active services when the dashboard loads - only once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let hasStarted = false;
|
||||||
|
|
||||||
const startActiveServices = async () => {
|
const startActiveServices = async () => {
|
||||||
|
if (hasStarted) return;
|
||||||
|
hasStarted = true;
|
||||||
|
|
||||||
await serviceService.startAllActiveServices();
|
await serviceService.startAllActiveServices();
|
||||||
// console.log("Active services monitoring started");
|
// console.log("Active services monitoring started");
|
||||||
};
|
};
|
||||||
|
|
||||||
startActiveServices();
|
// Only start once and add a delay to prevent immediate execution
|
||||||
}, []);
|
const timeoutId = setTimeout(startActiveServices, 2000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}, []); // Remove services dependency to prevent re-runs
|
||||||
|
|
||||||
// Show the loading state while fetching data
|
// Show the loading state while fetching data
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { pb } from '@/lib/pocketbase';
|
|||||||
import { monitoringIntervals } from '../monitoringIntervals';
|
import { monitoringIntervals } from '../monitoringIntervals';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start monitoring for a specific service
|
* Start monitoring for a specific service with optimized performance
|
||||||
*/
|
*/
|
||||||
export async function startMonitoringService(serviceId: string): Promise<void> {
|
export async function startMonitoringService(serviceId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -22,7 +22,7 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(`Starting monitoring for service ${serviceId} (${service.name})`);
|
// console.log(`Starting optimized monitoring for service ${serviceId} (${service.name})`);
|
||||||
|
|
||||||
// Update the service status to active/up in the database
|
// Update the service status to active/up in the database
|
||||||
await pb.collection('services').update(serviceId, {
|
await pb.collection('services').update(serviceId, {
|
||||||
@@ -30,19 +30,23 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The actual service checking is now handled by the Go microservice
|
// The actual service checking is now handled by the Go microservice
|
||||||
// This frontend service just tracks the monitoring state
|
// This frontend service just tracks the monitoring state with much less frequency
|
||||||
const intervalMs = (service.heartbeat_interval || 60) * 1000;
|
const intervalMs = Math.max((service.heartbeat_interval || 60) * 1000, 60000); // Minimum 1 minute
|
||||||
// console.log(`Service ${service.name} monitoring delegated to backend service`);
|
// console.log(`Service ${service.name} monitoring delegated to backend service with interval ${intervalMs}ms`);
|
||||||
|
|
||||||
// Store a placeholder interval to track that this service is being monitored
|
// Store a placeholder interval to track that this service is being monitored
|
||||||
|
// Significantly reduce frequency to prevent excessive logging and CPU usage
|
||||||
const intervalId = window.setInterval(() => {
|
const intervalId = window.setInterval(() => {
|
||||||
|
// Only log every 5 minutes to reduce console spam
|
||||||
|
if (Date.now() % (5 * 60 * 1000) < intervalMs) {
|
||||||
// console.log(`Monitoring active for service ${service.name} (handled by backend)`);
|
// console.log(`Monitoring active for service ${service.name} (handled by backend)`);
|
||||||
}, intervalMs);
|
}
|
||||||
|
}, Math.max(intervalMs, 300000)); // Minimum 5 minutes for logging
|
||||||
|
|
||||||
// Store the interval ID for this service
|
// Store the interval ID for this service
|
||||||
monitoringIntervals.set(serviceId, intervalId);
|
monitoringIntervals.set(serviceId, intervalId);
|
||||||
|
|
||||||
// console.log(`Monitoring registered for service ${serviceId}`);
|
// console.log(`Optimized monitoring registered for service ${serviceId}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.error("Error starting service monitoring:", error);
|
// console.error("Error starting service monitoring:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user