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],
|
||||
queryFn: () => serverService.getServerMetrics(serverId, timeRange),
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { serverService } from "@/services/serverService";
|
||||
import { formatChartData } from "../dataUtils";
|
||||
@@ -23,15 +23,19 @@ export const useServerHistoryData = (serverId: string) => {
|
||||
return result || [];
|
||||
},
|
||||
enabled: !!serverId,
|
||||
refetchInterval: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Reduced frequency
|
||||
staleTime: timeRange === '60m' ? 15000 : timeRange === '1d' ? 30000 : 60000, // Increased stale time
|
||||
retry: 2, // Reduced retries
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 3000), // Faster retry
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes cache
|
||||
refetchInterval: timeRange === '60m' ? 60000 : timeRange === '1d' ? 120000 : 300000, // Significantly increased intervals
|
||||
staleTime: timeRange === '60m' ? 30000 : timeRange === '1d' ? 60000 : 120000, // Increased stale time
|
||||
retry: 1, // Reduced retries to prevent excessive requests
|
||||
retryDelay: 2000, // Slower retry
|
||||
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
|
||||
const chartData = formatChartData(metrics, timeRange);
|
||||
const chartData = useMemo(() => {
|
||||
return formatChartData(metrics, timeRange);
|
||||
}, [metrics, timeRange]);
|
||||
|
||||
return {
|
||||
timeRange,
|
||||
|
||||
+91
-45
@@ -1,5 +1,5 @@
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Service, UptimeData } from "@/types/service.types";
|
||||
|
||||
@@ -18,71 +18,117 @@ export const useRealTimeUpdates = ({
|
||||
setService,
|
||||
setUptimeData
|
||||
}: 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(() => {
|
||||
if (!serviceId) return;
|
||||
|
||||
// console.log(`Setting up real-time updates for service: ${serviceId}`);
|
||||
// console.log(`Setting up real-time updates for service: ${serviceId}`);
|
||||
|
||||
try {
|
||||
// Subscribe to the service record for real-time updates
|
||||
const subscription = pb.collection('services').subscribe(serviceId, function(e) {
|
||||
// console.log("Service updated:", e.record);
|
||||
|
||||
// Update our local state with the new data
|
||||
if (e.record) {
|
||||
setService(prev => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
status: e.record.status || prev.status,
|
||||
responseTime: e.record.response_time || e.record.responseTime || prev.responseTime,
|
||||
uptime: e.record.uptime || prev.uptime,
|
||||
lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked,
|
||||
};
|
||||
});
|
||||
const setupSubscriptions = async () => {
|
||||
try {
|
||||
// Clean up existing subscriptions first
|
||||
if (subscriptionsRef.current.service) {
|
||||
subscriptionsRef.current.service();
|
||||
}
|
||||
if (subscriptionsRef.current.uptime) {
|
||||
subscriptionsRef.current.uptime();
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe to uptime data updates
|
||||
const uptimeSubscription = pb.collection('uptime_data').subscribe('*', function(e) {
|
||||
if (e.record && e.record.service_id === serviceId) {
|
||||
// console.log("New uptime data:", e.record);
|
||||
// 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
|
||||
if (e.record) {
|
||||
setService(prev => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
status: e.record.status || prev.status,
|
||||
responseTime: e.record.response_time || e.record.responseTime || prev.responseTime,
|
||||
uptime: e.record.uptime || prev.uptime,
|
||||
lastChecked: e.record.last_checked || e.record.lastChecked || prev.lastChecked,
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
subscriptionsRef.current.service = serviceUnsubscribe;
|
||||
|
||||
// Subscribe to uptime data updates with throttling
|
||||
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
|
||||
const timestamp = new Date(e.record.timestamp);
|
||||
if (timestamp >= startDate && timestamp <= endDate) {
|
||||
setUptimeData(prev => {
|
||||
// Limit the array size to prevent memory issues
|
||||
const maxRecords = 100;
|
||||
const newData: UptimeData = {
|
||||
id: e.record.id,
|
||||
service_id: e.record.service_id, // Include service_id
|
||||
serviceId: e.record.service_id, // Keep for backward compatibility
|
||||
service_id: e.record.service_id,
|
||||
serviceId: e.record.service_id,
|
||||
timestamp: e.record.timestamp,
|
||||
status: e.record.status,
|
||||
responseTime: e.record.response_time || 0,
|
||||
date: e.record.timestamp, // Adding required date property
|
||||
uptime: e.record.uptime || 0 // Adding required uptime property
|
||||
date: e.record.timestamp,
|
||||
uptime: e.record.uptime || 0
|
||||
};
|
||||
|
||||
// Add at the beginning of the array to maintain newest first sorting
|
||||
return [newData, ...prev];
|
||||
// Add at the beginning and limit array size
|
||||
const updatedData = [newData, ...prev];
|
||||
return updatedData.slice(0, maxRecords);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up the subscriptions
|
||||
return () => {
|
||||
// console.log(`Cleaning up subscriptions for service: ${serviceId}`);
|
||||
try {
|
||||
pb.collection('services').unsubscribe(serviceId);
|
||||
pb.collection('uptime_data').unsubscribe('*');
|
||||
} catch (error) {
|
||||
// console.error("Error cleaning up subscriptions:", error);
|
||||
subscriptionsRef.current.uptime = uptimeUnsubscribe;
|
||||
|
||||
} catch (error) {
|
||||
// console.error("Error setting up real-time updates:", error);
|
||||
}
|
||||
};
|
||||
|
||||
setupSubscriptions();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
// console.log(`Cleaning up subscriptions for service: ${serviceId}`);
|
||||
try {
|
||||
if (subscriptionsRef.current.service) {
|
||||
subscriptionsRef.current.service();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// console.error("Error setting up real-time updates:", error);
|
||||
}
|
||||
if (subscriptionsRef.current.uptime) {
|
||||
subscriptionsRef.current.uptime();
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("Error cleaning up subscriptions:", error);
|
||||
}
|
||||
};
|
||||
}, [serviceId, startDate, endDate, setService, setUptimeData]);
|
||||
};
|
||||
+88
-79
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Service, UptimeData } from "@/types/service.types";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -16,14 +17,17 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Get regional agents for "all" monitoring
|
||||
// Get regional agents for "all" monitoring with optimized caching
|
||||
const { data: regionalAgents = [] } = useQuery({
|
||||
queryKey: ['regional-services'],
|
||||
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;
|
||||
|
||||
try {
|
||||
@@ -38,7 +42,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
description: `Service status changed to ${newStatus}`,
|
||||
});
|
||||
} catch (error) {
|
||||
// console.error("Failed to update service status:", error);
|
||||
// console.error("Failed to update service status:", error);
|
||||
setService(prevService => prevService);
|
||||
|
||||
toast({
|
||||
@@ -47,9 +51,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
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 {
|
||||
if (!service) {
|
||||
// console.log('No service data available for uptime fetch');
|
||||
@@ -80,7 +84,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
|
||||
// Fetch default monitoring data
|
||||
const defaultData = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
|
||||
// console.log(`Retrieved ${defaultData.length} default monitoring records`);
|
||||
// console.log(`Retrieved ${defaultData.length} default monitoring records`);
|
||||
|
||||
// Mark default data with source identifier
|
||||
const markedDefaultData = defaultData.map(record => ({
|
||||
@@ -100,7 +104,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
const regionalData = await uptimeService.getUptimeHistoryByRegionalAgent(
|
||||
serviceId, limit, start, end, service.type, agent.region_name, agent.agent_id
|
||||
);
|
||||
// console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`);
|
||||
// console.log(`Retrieved ${regionalData.length} records from ${agent.region_name}`);
|
||||
|
||||
// Mark regional data with source identifier
|
||||
const markedRegionalData = regionalData.map(record => ({
|
||||
@@ -112,18 +116,18 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
|
||||
history = [...history, ...markedRegionalData];
|
||||
} catch (error) {
|
||||
// console.error(`Error fetching data from ${agent.region_name}:`, error);
|
||||
// console.error(`Error fetching data from ${agent.region_name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(`Total combined records: ${history.length}`);
|
||||
// console.log(`Total combined records: ${history.length}`);
|
||||
|
||||
} else {
|
||||
// Fetch regional agent specific data
|
||||
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);
|
||||
// console.log(`Retrieved ${history.length} regional monitoring records`);
|
||||
// console.log(`Retrieved ${history.length} regional monitoring records`);
|
||||
}
|
||||
|
||||
// Sort by timestamp (newest first)
|
||||
@@ -131,7 +135,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
|
||||
//console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`);
|
||||
// console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`);
|
||||
setUptimeData(filteredHistory);
|
||||
return filteredHistory;
|
||||
} catch (error) {
|
||||
@@ -143,9 +147,9 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
});
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}, [service, selectedRegionalAgent, regionalAgents, toast]);
|
||||
|
||||
const handleRegionalAgentChange = (agent: string) => {
|
||||
const handleRegionalAgentChange = useCallback((agent: string) => {
|
||||
// console.log(`Regional agent changed from ${selectedRegionalAgent} to: ${agent}`);
|
||||
|
||||
// Clear data immediately when switching
|
||||
@@ -154,78 +158,83 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
|
||||
// Refetch data with new agent selection
|
||||
if (serviceId && !isLoading && service) {
|
||||
// console.log(`Refetching data for new agent: ${agent}`);
|
||||
// console.log(`Refetching data for new agent: ${agent}`);
|
||||
fetchUptimeData(serviceId, startDate, endDate, '24h', agent);
|
||||
}
|
||||
};
|
||||
}, [selectedRegionalAgent, serviceId, isLoading, service, fetchUptimeData, startDate, endDate]);
|
||||
|
||||
// Memoize the service data fetching to prevent unnecessary re-runs
|
||||
const fetchServiceData = useCallback(async () => {
|
||||
try {
|
||||
if (!serviceId) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Request timed out")), 10000);
|
||||
});
|
||||
|
||||
const fetchPromise = pb.collection('services').getOne(serviceId);
|
||||
const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any;
|
||||
|
||||
const formattedService: Service = {
|
||||
id: serviceData.id,
|
||||
name: serviceData.name,
|
||||
url: serviceData.url || "",
|
||||
host: serviceData.host || "",
|
||||
port: serviceData.port || undefined,
|
||||
domain: serviceData.domain || "",
|
||||
type: serviceData.service_type || serviceData.type || "HTTP",
|
||||
status: serviceData.status || "paused",
|
||||
responseTime: serviceData.response_time || serviceData.responseTime || 0,
|
||||
uptime: serviceData.uptime || 0,
|
||||
lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(),
|
||||
interval: serviceData.heartbeat_interval || serviceData.interval || 60,
|
||||
retries: serviceData.max_retries || serviceData.retries || 3,
|
||||
notificationChannel: serviceData.notification_id,
|
||||
alertTemplate: serviceData.template_id,
|
||||
alerts: serviceData.alerts || "unmuted"
|
||||
};
|
||||
|
||||
// console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
|
||||
setService(formattedService);
|
||||
|
||||
// Small delay to ensure state is updated before fetching uptime data
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
// console.error("Error fetching service:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load service data. Please try again.",
|
||||
});
|
||||
navigate("/dashboard");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [serviceId, navigate, toast]);
|
||||
|
||||
// Initial data loading
|
||||
useEffect(() => {
|
||||
const fetchServiceData = async () => {
|
||||
try {
|
||||
if (!serviceId) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Request timed out")), 10000);
|
||||
});
|
||||
|
||||
const fetchPromise = pb.collection('services').getOne(serviceId);
|
||||
const serviceData = await Promise.race([fetchPromise, timeoutPromise]) as any;
|
||||
|
||||
const formattedService: Service = {
|
||||
id: serviceData.id,
|
||||
name: serviceData.name,
|
||||
url: serviceData.url || "",
|
||||
host: serviceData.host || "",
|
||||
port: serviceData.port || undefined,
|
||||
domain: serviceData.domain || "",
|
||||
type: serviceData.service_type || serviceData.type || "HTTP",
|
||||
status: serviceData.status || "paused",
|
||||
responseTime: serviceData.response_time || serviceData.responseTime || 0,
|
||||
uptime: serviceData.uptime || 0,
|
||||
lastChecked: serviceData.last_checked || serviceData.lastChecked || new Date().toLocaleString(),
|
||||
interval: serviceData.heartbeat_interval || serviceData.interval || 60,
|
||||
retries: serviceData.max_retries || serviceData.retries || 3,
|
||||
notificationChannel: serviceData.notification_id,
|
||||
alertTemplate: serviceData.template_id,
|
||||
alerts: serviceData.alerts || "unmuted"
|
||||
};
|
||||
|
||||
// console.log(`Loaded service: ${formattedService.name} (${formattedService.type})`);
|
||||
setService(formattedService);
|
||||
|
||||
// Small delay to ensure state is updated before fetching uptime data
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} catch (error) {
|
||||
// console.error("Error fetching service:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load service data. Please try again.",
|
||||
});
|
||||
navigate("/dashboard");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchServiceData();
|
||||
}, [serviceId, navigate, toast]);
|
||||
}, [fetchServiceData]);
|
||||
|
||||
// Update data when date range changes or when service is loaded
|
||||
// Update data when date range changes or when service is loaded - with debouncing
|
||||
useEffect(() => {
|
||||
if (serviceId && !isLoading && service) {
|
||||
// console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
||||
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
|
||||
}
|
||||
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]);
|
||||
const timeoutId = setTimeout(() => {
|
||||
// console.log(`Date range changed or service loaded, refetching data for ${serviceId}: ${startDate.toISOString()} to ${endDate.toISOString()}`);
|
||||
fetchUptimeData(serviceId, startDate, endDate, '24h', selectedRegionalAgent);
|
||||
}, 500); // Debounce API calls by 500ms
|
||||
|
||||
return {
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents, fetchUptimeData]);
|
||||
|
||||
return useMemo(() => ({
|
||||
service,
|
||||
setService,
|
||||
uptimeData,
|
||||
@@ -235,5 +244,5 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
fetchUptimeData,
|
||||
selectedRegionalAgent,
|
||||
handleRegionalAgentChange
|
||||
};
|
||||
}), [service, uptimeData, isLoading, handleStatusChange, fetchUptimeData, selectedRegionalAgent, handleRegionalAgentChange]);
|
||||
};
|
||||
@@ -1,71 +1,70 @@
|
||||
|
||||
import React from "react";
|
||||
import { Check, X, Pause, AlertTriangle } from "lucide-react";
|
||||
import React, { memo } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
export interface StatusBadgeProps {
|
||||
status: string;
|
||||
interface StatusBadgeProps {
|
||||
status: "up" | "down" | "paused" | "warning";
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const StatusBadge = ({ status, size = "sm" }: StatusBadgeProps) => {
|
||||
// Determine the sizing classes based on the size prop
|
||||
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":
|
||||
const StatusBadgeComponent = ({ status, size = "sm" }: StatusBadgeProps) => {
|
||||
const getStatusConfig = (status: string) => {
|
||||
switch (status) {
|
||||
case "up":
|
||||
return {
|
||||
variant: "default" as const,
|
||||
className: "bg-emerald-700 text-emerald-100 border-emerald-200 hover:bg-emerald-200",
|
||||
label:
|
||||
<span className="flex items-center gap-1">
|
||||
<Check className="w-4 h-4" /> Up
|
||||
</span>
|
||||
|
||||
};
|
||||
case "down":
|
||||
return {
|
||||
variant: "destructive" as const,
|
||||
className: "bg-red-700 text-red-100 border-red-200 hover:bg-red-200",
|
||||
label: "Down"
|
||||
};
|
||||
case "warning":
|
||||
return {
|
||||
variant: "destructive" as const,
|
||||
className: "bg-amber-700 text-amber-100 border-amber-200 hover:bg-amber-200",
|
||||
label: "Warning"
|
||||
};
|
||||
case "paused":
|
||||
return {
|
||||
variant: "secondary" as const,
|
||||
className: "bg-gray-700 text-gray-100 border-gray-200 hover:bg-gray-200",
|
||||
label: "Paused"
|
||||
};
|
||||
default:
|
||||
return "px-2 py-0.5 text-xs gap-0.5";
|
||||
return {
|
||||
variant: "outline" as const,
|
||||
className: "bg-gray-700 text-gray-100 border-gray-200",
|
||||
label: "Unknown"
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
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 = {
|
||||
sm: "text-xs px-2 py-1",
|
||||
md: "text-sm px-3 py-1.5",
|
||||
lg: "text-base px-4 py-2"
|
||||
};
|
||||
|
||||
const sizeClasses = getSizeClasses();
|
||||
const iconSize = getIconSize();
|
||||
const config = getStatusConfig(status);
|
||||
|
||||
switch (status) {
|
||||
case "up":
|
||||
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}`}>
|
||||
<Check className={iconSize} />
|
||||
<span>Up</span>
|
||||
</div>
|
||||
);
|
||||
case "down":
|
||||
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}`}>
|
||||
<X className={iconSize} />
|
||||
<span>Down</span>
|
||||
</div>
|
||||
);
|
||||
case "warning":
|
||||
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}`}>
|
||||
<AlertTriangle className={iconSize} />
|
||||
<span>Warning</span>
|
||||
</div>
|
||||
);
|
||||
case "paused":
|
||||
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}`}>
|
||||
<Pause className={iconSize} />
|
||||
<span>Paused</span>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
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 { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData";
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { UptimeSummary } from "./uptime/UptimeSummary";
|
||||
import { formatRelative } from "date-fns";
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { UptimeStatusItem } from './uptime/UptimeStatusItem';
|
||||
import { uptimeService } from '@/services/uptimeService';
|
||||
import { UptimeData } from '@/types/service.types';
|
||||
|
||||
interface UptimeBarProps {
|
||||
uptime: number;
|
||||
@@ -13,127 +14,81 @@ interface UptimeBarProps {
|
||||
serviceType?: string;
|
||||
}
|
||||
|
||||
export const UptimeBar = ({ uptime, status, serviceId, interval, serviceType }: UptimeBarProps) => {
|
||||
// Use consolidated hook to get properly merged data
|
||||
const { consolidatedItems, isLoading } = useConsolidatedUptimeData({
|
||||
serviceId,
|
||||
serviceType,
|
||||
status,
|
||||
interval
|
||||
const UptimeBarComponent = ({ uptime, status, serviceId, interval, serviceType = "HTTP" }: UptimeBarProps) => {
|
||||
// Calculate date range for last 20 checks with much more aggressive caching
|
||||
const endDate = useMemo(() => new Date(), []);
|
||||
const startDate = useMemo(() => {
|
||||
const start = new Date(endDate);
|
||||
start.setHours(start.getHours() - Math.max(interval * 20 / 3600, 24)); // At least 24 hours
|
||||
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) => {
|
||||
if (!hasData) {
|
||||
return "bg-gray-400"; // No data color - grey
|
||||
// Memoize the uptime calculation to prevent unnecessary recalculations
|
||||
const { uptimePercentage, displayData } = useMemo(() => {
|
||||
if (!uptimeData || uptimeData.length === 0) {
|
||||
return {
|
||||
uptimePercentage: uptime || 0,
|
||||
displayData: []
|
||||
};
|
||||
}
|
||||
|
||||
switch (itemStatus) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
// 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;
|
||||
|
||||
// console.log('UptimeBar rendering consolidated items:', consolidatedItems.length);
|
||||
// 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 (
|
||||
<div className="w-52">
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-1">
|
||||
{consolidatedItems.map((slot, index) => {
|
||||
// Check if we have actual monitoring data with valid status
|
||||
const hasValidData = slot.items.length > 0 && slot.items.some(item =>
|
||||
item.status && ['up', 'down', 'warning', 'paused'].includes(item.status)
|
||||
);
|
||||
|
||||
// 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 className="text-xs text-muted-foreground text-center">
|
||||
No monitoring data available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex space-x-0.5 min-w-0">
|
||||
{statusItems.length > 0 ? statusItems : (
|
||||
// Fallback display when no data
|
||||
<div className="h-5 w-1.5 rounded-sm bg-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<UptimeSummary uptime={uptime} interval={interval} />
|
||||
</div>
|
||||
<span className="text-sm font-medium whitespace-nowrap">
|
||||
{uptimePercentage.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Last 20 checks
|
||||
</span>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
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({
|
||||
queryKey: ['allUptimeHistory', serviceId, serviceType],
|
||||
queryFn: async () => {
|
||||
@@ -34,8 +34,8 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
||||
return allMonitoringData;
|
||||
},
|
||||
enabled: !!serviceId,
|
||||
refetchInterval: 30000,
|
||||
staleTime: 15000,
|
||||
refetchInterval: 60000, // 1 minute polling
|
||||
staleTime: 30000, // Data is fresh for 30 seconds
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
|
||||
@@ -20,7 +20,7 @@ const Dashboard = () => {
|
||||
|
||||
// For debugging user data
|
||||
useEffect(() => {
|
||||
// console.log("Current user data:", currentUser);
|
||||
// console.log("Current user data:", currentUser);
|
||||
}, [currentUser]);
|
||||
|
||||
// Handle logout
|
||||
@@ -29,22 +29,39 @@ const Dashboard = () => {
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
// Fetch all services
|
||||
// Fetch all services with 1-minute polling for real-time updates
|
||||
const { data: services = [], isLoading, error } = useQuery({
|
||||
queryKey: ['services'],
|
||||
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(() => {
|
||||
let hasStarted = false;
|
||||
|
||||
const startActiveServices = async () => {
|
||||
if (hasStarted) return;
|
||||
hasStarted = true;
|
||||
|
||||
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
|
||||
if (isLoading) {
|
||||
|
||||
@@ -3,13 +3,13 @@ import { pb } from '@/lib/pocketbase';
|
||||
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> {
|
||||
try {
|
||||
// First check if the service is already being monitored
|
||||
if (monitoringIntervals.has(serviceId)) {
|
||||
// console.log(`Service ${serviceId} is already being monitored`);
|
||||
// console.log(`Service ${serviceId} is already being monitored`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
|
||||
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
|
||||
await pb.collection('services').update(serviceId, {
|
||||
@@ -30,20 +30,24 @@ export async function startMonitoringService(serviceId: string): Promise<void> {
|
||||
});
|
||||
|
||||
// The actual service checking is now handled by the Go microservice
|
||||
// This frontend service just tracks the monitoring state
|
||||
const intervalMs = (service.heartbeat_interval || 60) * 1000;
|
||||
// console.log(`Service ${service.name} monitoring delegated to backend service`);
|
||||
// This frontend service just tracks the monitoring state with much less frequency
|
||||
const intervalMs = Math.max((service.heartbeat_interval || 60) * 1000, 60000); // Minimum 1 minute
|
||||
// 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
|
||||
// Significantly reduce frequency to prevent excessive logging and CPU usage
|
||||
const intervalId = window.setInterval(() => {
|
||||
// console.log(`Monitoring active for service ${service.name} (handled by backend)`);
|
||||
}, intervalMs);
|
||||
// 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)`);
|
||||
}
|
||||
}, Math.max(intervalMs, 300000)); // Minimum 5 minutes for logging
|
||||
|
||||
// Store the interval ID for this service
|
||||
monitoringIntervals.set(serviceId, intervalId);
|
||||
|
||||
// console.log(`Monitoring registered for service ${serviceId}`);
|
||||
// console.log(`Optimized monitoring registered for service ${serviceId}`);
|
||||
} catch (error) {
|
||||
// console.error("Error starting service monitoring:", error);
|
||||
// console.error("Error starting service monitoring:", error);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user