Update the service detail page to display all monitoring data (default and regional) in separate lines on the graph when "All Monitoring" is selected. Preserve the original functionality of other monitoring source options.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { regionalService } from "@/services/regionalService";
|
||||
import { MapPin, Loader2, Globe } from "lucide-react";
|
||||
import { MapPin, Loader2, BarChart3 } from "lucide-react";
|
||||
|
||||
interface RegionalAgentFilterProps {
|
||||
selectedAgent: string;
|
||||
@@ -19,8 +19,8 @@ export function RegionalAgentFilter({ selectedAgent, onAgentChange }: RegionalAg
|
||||
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
|
||||
|
||||
const getCurrentAgentDisplay = () => {
|
||||
if (!selectedAgent || selectedAgent === "default") {
|
||||
return "Default Monitoring";
|
||||
if (!selectedAgent || selectedAgent === "all") {
|
||||
return "All Monitoring";
|
||||
}
|
||||
|
||||
const [regionName] = selectedAgent.split("|");
|
||||
@@ -32,7 +32,7 @@ export function RegionalAgentFilter({ selectedAgent, onAgentChange }: RegionalAg
|
||||
return `${agent.region_name} (${agent.agent_ip_address})`;
|
||||
}
|
||||
|
||||
return regionName || "Default Monitoring";
|
||||
return regionName || "All Monitoring";
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -43,7 +43,7 @@ export function RegionalAgentFilter({ selectedAgent, onAgentChange }: RegionalAg
|
||||
</label>
|
||||
<Select
|
||||
onValueChange={onAgentChange}
|
||||
value={selectedAgent || "default"}
|
||||
value={selectedAgent || "all"}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -63,10 +63,10 @@ export function RegionalAgentFilter({ selectedAgent, onAgentChange }: RegionalAg
|
||||
</SelectItem>
|
||||
) : (
|
||||
<>
|
||||
<SelectItem value="default">
|
||||
<SelectItem value="all">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="h-4 w-4 text-blue-500" />
|
||||
<span className="font-medium">Default Monitoring</span>
|
||||
<BarChart3 className="h-4 w-4 text-purple-500" />
|
||||
<span className="font-medium">All Monitoring</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
{onlineAgents.length > 0 && onlineAgents.map((agent) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine } from "recharts";
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line } from "recharts";
|
||||
import { UptimeData } from "@/types/service.types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useTheme } from "@/contexts/ThemeContext";
|
||||
@@ -13,94 +13,290 @@ interface ResponseTimeChartProps {
|
||||
export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
// Modern color palette for different chart lines (only for All Monitoring)
|
||||
const modernColors = [
|
||||
{ stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.1)' }, // Blue
|
||||
{ stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.1)' }, // Emerald
|
||||
{ stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.1)' }, // Amber
|
||||
{ stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.1)' }, // Red
|
||||
{ stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.1)' }, // Violet
|
||||
{ stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.1)' }, // Cyan
|
||||
{ stroke: '#f97316', fill: 'rgba(249, 115, 22, 0.1)' }, // Orange
|
||||
{ stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.1)' }, // Lime
|
||||
];
|
||||
|
||||
// Check if we have data from multiple sources
|
||||
const hasMultipleSources = useMemo(() => {
|
||||
const sources = new Set();
|
||||
uptimeData.forEach(data => {
|
||||
const source = data.source || 'default';
|
||||
sources.add(source);
|
||||
});
|
||||
return sources.size > 1;
|
||||
}, [uptimeData]);
|
||||
|
||||
// Format data for the chart with enhanced time formatting
|
||||
const chartData = useMemo(() => {
|
||||
if (!uptimeData || uptimeData.length === 0) return [];
|
||||
|
||||
// Sort by timestamp ascending (oldest to newest)
|
||||
const sortedData = [...uptimeData].sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
// Get time format based on data density
|
||||
const isShortTimeRange = uptimeData.length > 0 &&
|
||||
(new Date(uptimeData[0].timestamp).getTime() - new Date(uptimeData[uptimeData.length - 1].timestamp).getTime()) < 2 * 60 * 60 * 1000;
|
||||
|
||||
return sortedData.map(data => {
|
||||
const timestamp = new Date(data.timestamp);
|
||||
if (hasMultipleSources) {
|
||||
// Group data by timestamp for multi-source display
|
||||
const timeGroups = new Map();
|
||||
|
||||
return {
|
||||
time: isShortTimeRange
|
||||
? format(timestamp, 'HH:mm:ss') // Include seconds for short time ranges like 60min
|
||||
: format(timestamp, 'HH:mm'),
|
||||
rawTime: timestamp.getTime(),
|
||||
date: format(timestamp, 'MMM dd, yyyy'),
|
||||
value: data.status === "paused" ? null : data.responseTime,
|
||||
status: data.status,
|
||||
// Separate values for different statuses with proper positioning
|
||||
upValue: data.status === "up" ? data.responseTime : null,
|
||||
downValue: data.status === "down" ? data.responseTime : null,
|
||||
warningValue: data.status === "warning" ? data.responseTime : null,
|
||||
};
|
||||
});
|
||||
}, [uptimeData]);
|
||||
uptimeData.forEach(data => {
|
||||
const timestamp = new Date(data.timestamp);
|
||||
// Round to nearest minute for better grouping
|
||||
const roundedTime = new Date(timestamp.getFullYear(), timestamp.getMonth(), timestamp.getDate(),
|
||||
timestamp.getHours(), timestamp.getMinutes());
|
||||
const timeKey = roundedTime.getTime();
|
||||
const source = data.source || 'default';
|
||||
|
||||
if (!timeGroups.has(timeKey)) {
|
||||
timeGroups.set(timeKey, {
|
||||
time: format(roundedTime, 'HH:mm'),
|
||||
rawTime: timeKey,
|
||||
date: format(roundedTime, 'MMM dd, yyyy'),
|
||||
timestamp: data.timestamp
|
||||
});
|
||||
}
|
||||
|
||||
const group = timeGroups.get(timeKey);
|
||||
|
||||
// Handle different data sources properly
|
||||
if (source === 'default') {
|
||||
// Only add pure default if we don't already have regional data with agent_id 1
|
||||
const hasRegionalDefault = uptimeData.some(d =>
|
||||
d.region_name === 'Default' && (d.agent_id === '1' || d.agent_id === 1)
|
||||
);
|
||||
|
||||
if (!hasRegionalDefault) {
|
||||
group.defaultValue = data.status === "paused" ? null : data.responseTime;
|
||||
group.defaultStatus = data.status;
|
||||
}
|
||||
} else {
|
||||
// Regional monitoring data - distinguish by region and agent
|
||||
const sourceKey = `regional_${source.replace('|', '_')}`;
|
||||
|
||||
// Group by the same timestamp more accurately
|
||||
const currentValue = group[`${sourceKey}_value`];
|
||||
const newValue = data.status === "paused" ? null : data.responseTime;
|
||||
|
||||
if (currentValue !== undefined && newValue !== null) {
|
||||
// Average multiple values for the same time slot
|
||||
const count = group[`${sourceKey}_count`] || 1;
|
||||
group[`${sourceKey}_value`] = ((currentValue * count) + newValue) / (count + 1);
|
||||
group[`${sourceKey}_count`] = count + 1;
|
||||
} else if (newValue !== null) {
|
||||
group[`${sourceKey}_value`] = newValue;
|
||||
group[`${sourceKey}_count`] = 1;
|
||||
}
|
||||
|
||||
group[`${sourceKey}_status`] = data.status;
|
||||
group[`${sourceKey}_label`] = data.region_name || source;
|
||||
group[`${sourceKey}_agent_id`] = data.agent_id;
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(timeGroups.values())
|
||||
.sort((a, b) => a.rawTime - b.rawTime)
|
||||
.slice(-50); // Show last 50 data points for better performance
|
||||
} else {
|
||||
// Single source display - use original styling
|
||||
const sortedData = [...uptimeData].sort((a, b) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
|
||||
const isShortTimeRange = uptimeData.length > 0 &&
|
||||
(new Date(uptimeData[0].timestamp).getTime() - new Date(uptimeData[uptimeData.length - 1].timestamp).getTime()) < 2 * 60 * 60 * 1000;
|
||||
|
||||
return sortedData.map(data => {
|
||||
const timestamp = new Date(data.timestamp);
|
||||
|
||||
return {
|
||||
time: isShortTimeRange
|
||||
? format(timestamp, 'HH:mm:ss')
|
||||
: format(timestamp, 'HH:mm'),
|
||||
rawTime: timestamp.getTime(),
|
||||
date: format(timestamp, 'MMM dd, yyyy'),
|
||||
value: data.status === "paused" ? null : data.responseTime,
|
||||
status: data.status,
|
||||
upValue: data.status === "up" ? data.responseTime : null,
|
||||
downValue: data.status === "down" ? data.responseTime : null,
|
||||
warningValue: data.status === "warning" ? data.responseTime : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [uptimeData, hasMultipleSources]);
|
||||
|
||||
// Calculate Y-axis domain for better positioning
|
||||
const yAxisDomain = useMemo(() => {
|
||||
if (!chartData.length) return ['dataMin - 10', 'dataMax + 10'];
|
||||
if (!chartData.length) return [0, 100];
|
||||
|
||||
const allValues = chartData
|
||||
.filter(d => d.value !== null && d.status !== 'paused')
|
||||
.map(d => d.value);
|
||||
let allValues: number[] = [];
|
||||
|
||||
if (hasMultipleSources) {
|
||||
chartData.forEach(d => {
|
||||
Object.keys(d).forEach(key => {
|
||||
if (key.endsWith('_value') || key === 'defaultValue') {
|
||||
const value = d[key as keyof typeof d];
|
||||
if (value !== null && value !== undefined && typeof value === 'number') {
|
||||
allValues.push(value);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
allValues = chartData
|
||||
.filter(d => d.value !== null && d.status !== 'paused')
|
||||
.map(d => d.value as number)
|
||||
.filter(v => typeof v === 'number');
|
||||
}
|
||||
|
||||
if (allValues.length === 0) return [0, 100];
|
||||
|
||||
const minValue = Math.min(...allValues);
|
||||
const maxValue = Math.max(...allValues);
|
||||
const padding = (maxValue - minValue) * 0.1 || 10;
|
||||
const padding = Math.max((maxValue - minValue) * 0.1, 10);
|
||||
|
||||
return [Math.max(0, minValue - padding), maxValue + padding];
|
||||
}, [chartData]);
|
||||
}, [chartData, hasMultipleSources]);
|
||||
|
||||
// Get unique sources for legend with proper labeling and colors
|
||||
const sources = useMemo(() => {
|
||||
if (!hasMultipleSources) return [];
|
||||
|
||||
const sourceSet = new Set<string>();
|
||||
const sourceInfo = new Map<string, any>();
|
||||
|
||||
uptimeData.forEach(data => {
|
||||
const source = data.source || 'default';
|
||||
if (source !== 'default') {
|
||||
sourceSet.add(source);
|
||||
sourceInfo.set(source, data);
|
||||
}
|
||||
});
|
||||
|
||||
const regionalSources = Array.from(sourceSet).map((source, index) => {
|
||||
const data = sourceInfo.get(source);
|
||||
const [regionName, agentId] = source.split('|');
|
||||
|
||||
// Create proper label with region and agent info
|
||||
let label = regionName;
|
||||
if (data?.agent_id && data.agent_id !== '1') {
|
||||
label = `${regionName} (${data.agent_id})`;
|
||||
} else if (regionName === 'Default' && data?.agent_id === '1') {
|
||||
label = `Default (Agent 1)`;
|
||||
}
|
||||
|
||||
const colorIndex = index % modernColors.length;
|
||||
|
||||
return {
|
||||
key: `regional_${source.replace('|', '_')}`,
|
||||
label: label,
|
||||
stroke: modernColors[colorIndex].stroke,
|
||||
fill: modernColors[colorIndex].fill
|
||||
};
|
||||
});
|
||||
|
||||
// Only add pure default monitoring if we have it AND no regional Default with agent_id 1
|
||||
const hasRegionalDefault = uptimeData.some(data =>
|
||||
data.region_name === 'Default' && (data.agent_id === '1' || data.agent_id === 1)
|
||||
);
|
||||
const hasPureDefault = uptimeData.some(data => data.source === 'default');
|
||||
|
||||
const defaultSources = (hasPureDefault && !hasRegionalDefault) ? [{
|
||||
key: 'default',
|
||||
label: 'Default',
|
||||
stroke: modernColors[regionalSources.length % modernColors.length].stroke,
|
||||
fill: modernColors[regionalSources.length % modernColors.length].fill
|
||||
}] : [];
|
||||
|
||||
return [...defaultSources, ...regionalSources];
|
||||
}, [uptimeData, hasMultipleSources]);
|
||||
|
||||
// Create a custom tooltip for the chart
|
||||
const CustomTooltip = ({ active, payload, label }: any) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload;
|
||||
|
||||
// Set background color based on status
|
||||
let statusColor = "bg-emerald-800";
|
||||
let statusText = "Up";
|
||||
|
||||
if (data.status === "down") {
|
||||
statusColor = "bg-red-800";
|
||||
statusText = "Down";
|
||||
} else if (data.status === "warning") {
|
||||
statusColor = "bg-yellow-800";
|
||||
statusText = "Warning";
|
||||
} else if (data.status === "paused") {
|
||||
statusColor = "bg-gray-800";
|
||||
statusText = "Paused";
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} p-2 border ${theme === 'dark' ? 'border-gray-700' : 'border-gray-200'} rounded shadow-md`}>
|
||||
<p className="text-sm font-medium">{label}</p>
|
||||
<p className="text-xs text-muted-foreground">{data.date}</p>
|
||||
<div className={`flex items-center gap-2 mt-1 ${data.status === "paused" ? "opacity-70" : ""}`}>
|
||||
<div className={`w-3 h-3 rounded-full ${statusColor}`}></div>
|
||||
<span>{statusText}</span>
|
||||
</div>
|
||||
<p className="mt-1 font-mono text-sm">
|
||||
{data.status === "paused" ? "Monitoring paused" :
|
||||
data.value !== null ? `${data.value} ms` : "No data"}
|
||||
</p>
|
||||
<div className={`${theme === 'dark' ? 'bg-gray-900' : 'bg-white'} p-3 border ${theme === 'dark' ? 'border-gray-700' : 'border-gray-200'} rounded shadow-md min-w-48`}>
|
||||
<p className="text-sm font-medium">{String(label)}</p>
|
||||
<p className="text-xs text-muted-foreground mb-2">{String(data.date)}</p>
|
||||
|
||||
{hasMultipleSources ? (
|
||||
// Multi-source tooltip
|
||||
<div className="space-y-2">
|
||||
{sources.map(source => {
|
||||
const valueKey = source.key === 'default' ? 'defaultValue' : `${source.key}_value`;
|
||||
const statusKey = source.key === 'default' ? 'defaultStatus' : `${source.key}_status`;
|
||||
const value = data[valueKey];
|
||||
const status = data[statusKey];
|
||||
|
||||
if (value === undefined && status === undefined) return null;
|
||||
|
||||
let statusColor = "bg-gray-800";
|
||||
let statusText = "No Data";
|
||||
|
||||
if (status === "up") {
|
||||
statusColor = "bg-emerald-800";
|
||||
statusText = "Up";
|
||||
} else if (status === "down") {
|
||||
statusColor = "bg-red-800";
|
||||
statusText = "Down";
|
||||
} else if (status === "warning") {
|
||||
statusColor = "bg-yellow-800";
|
||||
statusText = "Warning";
|
||||
} else if (status === "paused") {
|
||||
statusColor = "bg-gray-800";
|
||||
statusText = "Paused";
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={source.key} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: source.stroke }}
|
||||
></div>
|
||||
<span className="text-xs">{String(source.label)}</span>
|
||||
</div>
|
||||
<div className="text-xs font-mono">
|
||||
{status === "paused" ? "Paused" :
|
||||
value !== null && value !== undefined ? `${value} ms` : "No data"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
// Single source tooltip - showing status with color
|
||||
<>
|
||||
<div className={`flex items-center gap-2 mt-1 ${data.status === "paused" ? "opacity-70" : ""}`}>
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
data.status === "up" ? "bg-emerald-800" :
|
||||
data.status === "down" ? "bg-red-800" :
|
||||
data.status === "warning" ? "bg-yellow-800" : "bg-gray-800"
|
||||
}`}></div>
|
||||
<span>{
|
||||
data.status === "up" ? "Up" :
|
||||
data.status === "down" ? "Down" :
|
||||
data.status === "warning" ? "Warning" : "Paused"
|
||||
}</span>
|
||||
</div>
|
||||
<p className="mt-1 font-mono text-sm">
|
||||
{data.status === "paused" ? "Monitoring paused" :
|
||||
data.value !== null ? `${data.value} ms` : "No data"}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Check if we have any data to display - be more lenient by checking raw uptimeData
|
||||
// Check if we have any data to display
|
||||
const hasData = uptimeData.length > 0;
|
||||
|
||||
// Get date range for display
|
||||
@@ -130,6 +326,19 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
{hasMultipleSources && (
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
{sources.map(source => (
|
||||
<div key={source.key} className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: source.stroke }}
|
||||
></div>
|
||||
<span>{String(source.label)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!hasData ? (
|
||||
@@ -139,82 +348,140 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) {
|
||||
) : (
|
||||
<div className="h-80">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorUp" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.8}/>
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0.2}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorDown" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.8}/>
|
||||
<stop offset="95%" stopColor="#ef4444" stopOpacity={0.2}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorWarning" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.8}/>
|
||||
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0.2}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
tick={{ fontSize: 10 }}
|
||||
height={60}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={5}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||
allowDecimals={false}
|
||||
domain={yAxisDomain}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
|
||||
{/* Separate area charts for each status - positioned closer together */}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="upValue"
|
||||
stroke="#10b981"
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.4}
|
||||
fill="url(#colorUp)"
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="downValue"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.4}
|
||||
fill="url(#colorDown)"
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="warningValue"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
fillOpacity={0.4}
|
||||
fill="url(#colorWarning)"
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
{/* Add reference lines for paused periods */}
|
||||
{chartData.map((entry, index) =>
|
||||
entry.status === 'paused' ? (
|
||||
<ReferenceLine
|
||||
key={`ref-${index}`}
|
||||
x={entry.time}
|
||||
stroke="#9ca3af"
|
||||
strokeDasharray="3 3"
|
||||
{hasMultipleSources ? (
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
|
||||
<defs>
|
||||
{sources.map(source => (
|
||||
<linearGradient key={`gradient-${source.key}`} id={`gradient-${source.key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={source.stroke} stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor={source.stroke} stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
tick={{ fontSize: 10 }}
|
||||
height={60}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={5}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||
allowDecimals={false}
|
||||
domain={yAxisDomain}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
|
||||
{/* Default monitoring area with modern background - only if exists and no regional default */}
|
||||
{sources.find(s => s.key === 'default') && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="defaultValue"
|
||||
stroke={sources.find(s => s.key === 'default')?.stroke}
|
||||
fill={`url(#gradient-default)`}
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</AreaChart>
|
||||
)}
|
||||
|
||||
{/* Regional monitoring areas with modern backgrounds */}
|
||||
{sources.filter(s => s.key !== 'default').map((source) => (
|
||||
<Area
|
||||
key={source.key}
|
||||
type="monotone"
|
||||
dataKey={`${source.key}_value`}
|
||||
stroke={source.stroke}
|
||||
fill={`url(#gradient-${source.key})`}
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
) : (
|
||||
// For single regional agent or default monitoring only - use original AreaChart styling
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 30 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorUp" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#10b981" stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorDown" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ef4444" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#ef4444" stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
<linearGradient id="colorWarning" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#f59e0b" stopOpacity={0.3}/>
|
||||
<stop offset="95%" stopColor="#f59e0b" stopOpacity={0.05}/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke={theme === 'dark' ? '#333' : '#e5e7eb'} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||
angle={-45}
|
||||
textAnchor="end"
|
||||
tick={{ fontSize: 10 }}
|
||||
height={60}
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={5}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={theme === 'dark' ? '#666' : '#9ca3af'}
|
||||
allowDecimals={false}
|
||||
domain={yAxisDomain}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
|
||||
{/* Original area charts for different statuses with background colors */}
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="upValue"
|
||||
stroke="#10b981"
|
||||
fill="url(#colorUp)"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="downValue"
|
||||
stroke="#ef4444"
|
||||
fill="url(#colorDown)"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="warningValue"
|
||||
stroke="#f59e0b"
|
||||
fill="url(#colorWarning)"
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
connectNulls={false}
|
||||
/>
|
||||
|
||||
{/* Reference lines for paused status */}
|
||||
{chartData.map((entry, index) =>
|
||||
entry.status === 'paused' ? (
|
||||
<ReferenceLine
|
||||
key={`ref-${index}`}
|
||||
x={entry.time}
|
||||
stroke="#9ca3af"
|
||||
strokeDasharray="3 3"
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</AreaChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+56
-9
@@ -1,4 +1,3 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { Service, UptimeData } from "@/types/service.types";
|
||||
@@ -6,15 +5,24 @@ import { useToast } from "@/hooks/use-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { uptimeService } from "@/services/uptimeService";
|
||||
import { DateRangeOption } from "../../DateRangeFilter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { regionalService } from "@/services/regionalService";
|
||||
|
||||
export const useServiceData = (serviceId: string | undefined, startDate: Date, endDate: Date) => {
|
||||
const [service, setService] = useState<Service | null>(null);
|
||||
const [uptimeData, setUptimeData] = useState<UptimeData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedRegionalAgent, setSelectedRegionalAgent] = useState<string>("default");
|
||||
const [selectedRegionalAgent, setSelectedRegionalAgent] = useState<string>("all");
|
||||
const { toast } = useToast();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Get regional agents for "all" monitoring
|
||||
const { data: regionalAgents = [] } = useQuery({
|
||||
queryKey: ['regional-services'],
|
||||
queryFn: regionalService.getRegionalServices,
|
||||
enabled: selectedRegionalAgent === "all"
|
||||
});
|
||||
|
||||
const handleStatusChange = async (newStatus: "up" | "down" | "paused" | "warning") => {
|
||||
if (!service || !serviceId) return;
|
||||
|
||||
@@ -66,11 +74,50 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
|
||||
let history: UptimeData[] = [];
|
||||
|
||||
if (currentAgent === "default") {
|
||||
// Fetch default monitoring data - this will automatically filter out regional records
|
||||
console.log(`Fetching default monitoring data from ${service.type} collection`);
|
||||
history = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
|
||||
console.log(`Retrieved ${history.length} default monitoring records`);
|
||||
if (currentAgent === "all") {
|
||||
// Fetch data from all sources (default + all online regional agents)
|
||||
console.log(`Fetching data from all monitoring sources`);
|
||||
|
||||
// Fetch default monitoring data
|
||||
const defaultData = await uptimeService.getUptimeHistory(serviceId, limit, start, end, service.type);
|
||||
console.log(`Retrieved ${defaultData.length} default monitoring records`);
|
||||
|
||||
// Mark default data with source identifier
|
||||
const markedDefaultData = defaultData.map(record => ({
|
||||
...record,
|
||||
source: 'default' as const,
|
||||
region_name: undefined,
|
||||
agent_id: undefined
|
||||
}));
|
||||
|
||||
history = [...markedDefaultData];
|
||||
|
||||
// Fetch regional monitoring data from all online agents
|
||||
const onlineAgents = regionalAgents.filter(agent => agent.connection === 'online');
|
||||
|
||||
for (const agent of onlineAgents) {
|
||||
try {
|
||||
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}`);
|
||||
|
||||
// Mark regional data with source identifier
|
||||
const markedRegionalData = regionalData.map(record => ({
|
||||
...record,
|
||||
source: `${agent.region_name}|${agent.agent_id}` as const,
|
||||
region_name: agent.region_name,
|
||||
agent_id: agent.agent_id
|
||||
}));
|
||||
|
||||
history = [...history, ...markedRegionalData];
|
||||
} catch (error) {
|
||||
console.error(`Error fetching data from ${agent.region_name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Total combined records: ${history.length}`);
|
||||
|
||||
} else {
|
||||
// Fetch regional agent specific data
|
||||
const [regionName, agentId] = currentAgent.split("|");
|
||||
@@ -84,7 +131,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 === "default" ? "default" : "regional"} monitoring`);
|
||||
console.log(`Final dataset: ${filteredHistory.length} records for ${currentAgent === "all" ? "all sources" : "regional"} monitoring`);
|
||||
setUptimeData(filteredHistory);
|
||||
return filteredHistory;
|
||||
} catch (error) {
|
||||
@@ -176,7 +223,7 @@ export const useServiceData = (serviceId: string | undefined, startDate: Date, e
|
||||
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]);
|
||||
}, [startDate, endDate, serviceId, isLoading, service, selectedRegionalAgent, regionalAgents]);
|
||||
|
||||
return {
|
||||
service,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { ArrowLeft, Globe, MoreVertical, FileText, Github, Twitter, MessageSquare, Bell } from "lucide-react";
|
||||
import { ArrowLeft, Globe } from "lucide-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatusBadge } from "@/components/services/StatusBadge";
|
||||
@@ -79,17 +79,13 @@ export function ServiceHeader({
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<StatusBadge status={service.status} size="lg" />
|
||||
<ServiceMonitoringButton service={service} onStatusChange={onStatusChange} />
|
||||
{selectedRegionalAgent !== undefined && onRegionalAgentChange && (
|
||||
<RegionalAgentFilter
|
||||
selectedAgent={selectedRegionalAgent}
|
||||
onAgentChange={onRegionalAgentChange}
|
||||
/>
|
||||
)}
|
||||
<ServiceMonitoringButton service={service} onStatusChange={onStatusChange} />
|
||||
<Button variant="outline" size="icon" className="rounded-full w-8 h-8 border-border">
|
||||
<span className="sr-only">More options</span>
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ export const ServiceRow = ({
|
||||
<TableCell className="py-4">
|
||||
<ServiceRowResponseTime responseTime={service.responseTime} />
|
||||
</TableCell>
|
||||
<TableCell className="w-52 py-4">
|
||||
<TableCell className="w-60 py-4">
|
||||
<UptimeBar
|
||||
uptime={service.uptime}
|
||||
status={service.status}
|
||||
|
||||
@@ -1,52 +1,137 @@
|
||||
|
||||
import React from "react";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { useUptimeData } from "./hooks/useUptimeData";
|
||||
import { UptimeStatusItem } from "./uptime/UptimeStatusItem";
|
||||
import { useConsolidatedUptimeData } from "./hooks/useConsolidatedUptimeData";
|
||||
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
import { UptimeSummary } from "./uptime/UptimeSummary";
|
||||
import { UptimeLoadingState } from "./uptime/UptimeLoadingState";
|
||||
import { UptimeErrorState } from "./uptime/UptimeErrorState";
|
||||
import { formatRelative } from "date-fns";
|
||||
|
||||
interface UptimeBarProps {
|
||||
uptime: number;
|
||||
status: string;
|
||||
serviceId?: string;
|
||||
interval?: number; // Service monitoring interval in seconds
|
||||
serviceType?: string; // Add service type for proper data fetching
|
||||
serviceId: string;
|
||||
interval: number;
|
||||
serviceType?: string;
|
||||
}
|
||||
|
||||
export const UptimeBar = ({ uptime, status, serviceId, interval = 60, serviceType }: UptimeBarProps) => {
|
||||
const { displayItems, isLoading, error, isFetching, refetch } = useUptimeData({
|
||||
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
|
||||
});
|
||||
|
||||
// If still loading and no history, show improved loading state
|
||||
if ((isLoading || isFetching) && displayItems.length === 0) {
|
||||
return <UptimeLoadingState />;
|
||||
}
|
||||
|
||||
// If there's an error and no history, show improved error state with retry button
|
||||
if (error && displayItems.length === 0) {
|
||||
return <UptimeErrorState uptime={uptime} onRetry={refetch} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<div className="flex items-center space-x-0.5 w-full h-6">
|
||||
{displayItems.map((item, index) => (
|
||||
<UptimeStatusItem
|
||||
key={item.id || `status-${index}`}
|
||||
item={item}
|
||||
index={index}
|
||||
/>
|
||||
const getStatusColor = (itemStatus: string, hasData: boolean = true) => {
|
||||
if (!hasData) {
|
||||
return "bg-gray-300"; // No data color
|
||||
}
|
||||
|
||||
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-500"; // Distinct paused color (darker grey)
|
||||
default:
|
||||
return "bg-gray-300"; // Default fallback
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
console.log('UptimeBar rendering consolidated items:', consolidatedItems.length);
|
||||
|
||||
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-500' : 'bg-gray-300'
|
||||
}`} />
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<UptimeSummary uptime={uptime} interval={interval} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,209 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { uptimeService } from '@/services/uptimeService';
|
||||
import { UptimeData } from '@/types/service.types';
|
||||
|
||||
interface UseConsolidatedUptimeDataProps {
|
||||
serviceId?: string;
|
||||
serviceType?: string;
|
||||
status: string;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
interface ConsolidatedTimeSlot {
|
||||
timestamp: string;
|
||||
items: Array<UptimeData & { source: string; isDefault: boolean }>;
|
||||
}
|
||||
|
||||
// Helper function to safely check if a PocketBase field is actually undefined/null
|
||||
const isFieldEmpty = (field: any): boolean => {
|
||||
if (field === undefined || field === null) return true;
|
||||
if (typeof field === 'object' && field._type === 'undefined') return true;
|
||||
if (typeof field === 'string' && (field === '' || field === 'undefined')) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Helper function to safely get field value from PocketBase
|
||||
const getFieldValue = (field: any): string | number | null => {
|
||||
if (isFieldEmpty(field)) return null;
|
||||
if (typeof field === 'object' && field.value !== undefined) return field.value;
|
||||
return field;
|
||||
};
|
||||
|
||||
// Helper function to normalize timestamp to minute precision
|
||||
const normalizeTimestamp = (timestamp: string): string => {
|
||||
const date = new Date(timestamp);
|
||||
// Round down to the nearest minute
|
||||
date.setSeconds(0, 0);
|
||||
return date.toISOString();
|
||||
};
|
||||
|
||||
export const useConsolidatedUptimeData = ({ serviceId, serviceType, status, interval }: UseConsolidatedUptimeDataProps) => {
|
||||
const [consolidatedItems, setConsolidatedItems] = useState<ConsolidatedTimeSlot[]>([]);
|
||||
|
||||
// Fetch ALL uptime history data in a single query
|
||||
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
||||
queryKey: ['consolidatedUptimeHistory', serviceId, serviceType],
|
||||
queryFn: async () => {
|
||||
if (!serviceId) {
|
||||
console.log('No serviceId provided, skipping fetch');
|
||||
return [];
|
||||
}
|
||||
console.log(`Fetching consolidated uptime data for service ${serviceId} of type ${serviceType}`);
|
||||
|
||||
// Get ALL uptime history data - this should include both default and regional data
|
||||
const rawData = await uptimeService.getUptimeHistory(serviceId, 100, undefined, undefined, serviceType);
|
||||
|
||||
console.log(`Retrieved ${rawData.length} total records for service ${serviceId}`);
|
||||
console.log('Raw data sample:', rawData.slice(0, 5).map(d => ({
|
||||
timestamp: d.timestamp,
|
||||
region_name: d.region_name,
|
||||
agent_id: d.agent_id,
|
||||
status: d.status,
|
||||
service_id: d.service_id,
|
||||
response_time: d.responseTime
|
||||
})));
|
||||
|
||||
return rawData;
|
||||
},
|
||||
enabled: !!serviceId,
|
||||
refetchInterval: 30000,
|
||||
staleTime: 15000,
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
|
||||
// Process and consolidate uptime data
|
||||
const processConsolidatedData = (data: UptimeData[]): ConsolidatedTimeSlot[] => {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
console.log(`Processing ${data.length} uptime records for consolidation`);
|
||||
|
||||
// Create a map to group records by normalized timestamp (minute precision)
|
||||
const timeSlotMap = new Map<string, Array<UptimeData & { source: string; isDefault: boolean }>>();
|
||||
|
||||
data.forEach(record => {
|
||||
// Normalize timestamp to minute precision to avoid duplicate bars
|
||||
const normalizedTimestamp = normalizeTimestamp(record.timestamp);
|
||||
|
||||
// Safely extract region and agent information
|
||||
const regionName = getFieldValue(record.region_name);
|
||||
const agentId = getFieldValue(record.agent_id);
|
||||
|
||||
// Determine the source name and whether it's default monitoring
|
||||
let sourceName: string;
|
||||
let isDefault: boolean;
|
||||
|
||||
if (regionName && agentId) {
|
||||
// Regional monitoring data
|
||||
sourceName = `${regionName} (Agent ${agentId})`;
|
||||
isDefault = false;
|
||||
console.log(`Found regional data: ${sourceName} for normalized timestamp ${normalizedTimestamp}`);
|
||||
} else if (agentId && !regionName) {
|
||||
// Default monitoring with specific agent
|
||||
sourceName = `Default (Agent ${agentId})`;
|
||||
isDefault = true;
|
||||
console.log(`Found default monitoring: ${sourceName} for normalized timestamp ${normalizedTimestamp}`);
|
||||
} else {
|
||||
// Default monitoring fallback
|
||||
sourceName = 'Default (Agent 1)';
|
||||
isDefault = true;
|
||||
console.log(`Using fallback default monitoring for normalized timestamp ${normalizedTimestamp}`);
|
||||
}
|
||||
|
||||
// Get or create the array for this normalized timestamp
|
||||
if (!timeSlotMap.has(normalizedTimestamp)) {
|
||||
timeSlotMap.set(normalizedTimestamp, []);
|
||||
}
|
||||
|
||||
// Check if we already have an entry from this source for this time slot
|
||||
const existingItems = timeSlotMap.get(normalizedTimestamp)!;
|
||||
const existingFromSource = existingItems.find(item => item.source === sourceName);
|
||||
|
||||
if (!existingFromSource) {
|
||||
// Add the record with source information only if we don't already have one from this source
|
||||
timeSlotMap.get(normalizedTimestamp)!.push({
|
||||
...record,
|
||||
timestamp: normalizedTimestamp, // Use normalized timestamp
|
||||
source: sourceName,
|
||||
isDefault
|
||||
});
|
||||
|
||||
console.log(`Added record to normalized timestamp ${normalizedTimestamp}: Source=${sourceName}, Status=${record.status}, IsDefault=${isDefault}, ResponseTime=${record.responseTime}ms`);
|
||||
} else {
|
||||
console.log(`Skipping duplicate record for source ${sourceName} at timestamp ${normalizedTimestamp}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert map to consolidated time slots and sort by timestamp (newest first)
|
||||
const consolidatedTimeline: ConsolidatedTimeSlot[] = Array.from(timeSlotMap.entries())
|
||||
.map(([timestamp, items]) => ({
|
||||
timestamp,
|
||||
items: items.sort((a, b) => {
|
||||
// Sort so default monitoring appears first
|
||||
if (a.isDefault && !b.isDefault) return -1;
|
||||
if (!a.isDefault && b.isDefault) return 1;
|
||||
return 0;
|
||||
})
|
||||
}))
|
||||
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
|
||||
.slice(0, 20); // Take the most recent 20 time slots
|
||||
|
||||
console.log(`Created ${consolidatedTimeline.length} consolidated time slots with normalized timestamps`);
|
||||
consolidatedTimeline.forEach((slot, index) => {
|
||||
console.log(`Slot ${index} - Normalized Timestamp: ${slot.timestamp}, Items: ${slot.items.length}`);
|
||||
slot.items.forEach((item, itemIndex) => {
|
||||
console.log(` Item ${itemIndex}: Source=${item.source}, Status=${item.status}, ResponseTime=${item.responseTime}ms, IsDefault=${item.isDefault}`);
|
||||
});
|
||||
});
|
||||
|
||||
return consolidatedTimeline;
|
||||
};
|
||||
|
||||
// Update consolidated items when data changes
|
||||
useEffect(() => {
|
||||
if (uptimeData && uptimeData.length > 0) {
|
||||
console.log(`Processing consolidated uptime data for service ${serviceId}`);
|
||||
const processedData = processConsolidatedData(uptimeData);
|
||||
setConsolidatedItems(processedData);
|
||||
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
|
||||
// Generate placeholder data when no real data is available
|
||||
console.log(`No uptime data available for service ${serviceId}, generating placeholder`);
|
||||
|
||||
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
||||
? status
|
||||
: "paused";
|
||||
|
||||
const placeholderHistory: ConsolidatedTimeSlot[] = Array(20).fill(null).map((_, index) => {
|
||||
const timestamp = new Date(Date.now() - (index * interval * 1000));
|
||||
timestamp.setSeconds(0, 0); // Normalize to minute precision
|
||||
|
||||
return {
|
||||
timestamp: timestamp.toISOString(),
|
||||
items: [{
|
||||
id: `placeholder-${serviceId}-${index}`,
|
||||
service_id: serviceId || "",
|
||||
serviceId: serviceId || "",
|
||||
timestamp: timestamp.toISOString(),
|
||||
status: statusValue as "up" | "down" | "warning" | "paused",
|
||||
responseTime: 0,
|
||||
source: 'Default (Agent 1)',
|
||||
isDefault: true
|
||||
}]
|
||||
};
|
||||
});
|
||||
|
||||
setConsolidatedItems(placeholderHistory);
|
||||
}
|
||||
}, [uptimeData, serviceId, status, interval]);
|
||||
|
||||
return {
|
||||
consolidatedItems,
|
||||
isLoading,
|
||||
error,
|
||||
isFetching,
|
||||
refetch
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { uptimeService } from '@/services/uptimeService';
|
||||
import { UptimeData } from '@/types/service.types';
|
||||
|
||||
interface UseDefaultUptimeDataProps {
|
||||
serviceId?: string;
|
||||
serviceType?: string;
|
||||
status: string;
|
||||
interval: number;
|
||||
}
|
||||
|
||||
export const useDefaultUptimeData = ({ serviceId, serviceType, status, interval }: UseDefaultUptimeDataProps) => {
|
||||
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
||||
|
||||
// Fetch ONLY Default (Agent ID 1) uptime history data - NO regional data whatsoever
|
||||
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
||||
queryKey: ['strictDefaultUptimeHistory', serviceId, serviceType],
|
||||
queryFn: async () => {
|
||||
if (!serviceId) {
|
||||
console.log('No serviceId provided, skipping fetch');
|
||||
return [];
|
||||
}
|
||||
console.log(`Fetching STRICT Default (Agent ID 1) uptime data for service ${serviceId} of type ${serviceType}`);
|
||||
|
||||
// Get raw uptime history data
|
||||
const rawData = await uptimeService.getUptimeHistory(serviceId, 50, undefined, undefined, serviceType);
|
||||
|
||||
// ULTRA-STRICT filtering: ONLY Default monitoring records (Agent ID 1)
|
||||
// This means: NO region_name, NO agent_id, NO regional monitoring whatsoever
|
||||
const strictDefaultData = rawData.filter(record => {
|
||||
// Must have NO regional monitoring info at all
|
||||
const isStrictDefault = (
|
||||
!record.region_name &&
|
||||
!record.agent_id &&
|
||||
record.service_id === serviceId
|
||||
);
|
||||
|
||||
if (isStrictDefault) {
|
||||
console.log(`✓ ACCEPTED Default record: ${record.id} - ${record.timestamp} - Pure default monitoring`);
|
||||
} else {
|
||||
console.log(`✗ REJECTED non-default record: ${record.id} - region: ${record.region_name || 'none'}, agent: ${record.agent_id || 'none'}`);
|
||||
}
|
||||
|
||||
return isStrictDefault;
|
||||
});
|
||||
|
||||
console.log(`ULTRA-STRICT FILTER: Reduced ${rawData.length} records to ${strictDefaultData.length} pure default monitoring records`);
|
||||
return strictDefaultData;
|
||||
},
|
||||
enabled: !!serviceId,
|
||||
refetchInterval: 30000,
|
||||
staleTime: 15000,
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: 3,
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
|
||||
// Process uptime data with absolute timestamp uniqueness
|
||||
const processUptimeData = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
console.log(`Processing ${data.length} strict default monitoring records for service ${serviceId}`);
|
||||
|
||||
// Sort data by timestamp (newest first)
|
||||
const sortedData = [...data].sort((a, b) =>
|
||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
|
||||
// ABSOLUTE timestamp uniqueness - use Map for O(1) lookup
|
||||
const timestampMap = new Map<string, UptimeData>();
|
||||
|
||||
sortedData.forEach(record => {
|
||||
const exactTimestamp = record.timestamp;
|
||||
|
||||
if (!timestampMap.has(exactTimestamp)) {
|
||||
timestampMap.set(exactTimestamp, record);
|
||||
console.log(`✓ Added unique default record: ${record.id} - ${exactTimestamp}`);
|
||||
} else {
|
||||
console.log(`✗ REJECTED absolute duplicate timestamp: ${record.id} - ${exactTimestamp} (exact timestamp match)`);
|
||||
}
|
||||
});
|
||||
|
||||
// Convert Map back to array and take most recent 20
|
||||
const uniqueRecords = Array.from(timestampMap.values());
|
||||
const recentData = uniqueRecords.slice(0, 20);
|
||||
|
||||
console.log(`FINAL RESULT: ${recentData.length} absolutely unique default monitoring records`);
|
||||
|
||||
return recentData;
|
||||
};
|
||||
|
||||
// Update history items when data changes
|
||||
useEffect(() => {
|
||||
if (uptimeData && uptimeData.length > 0) {
|
||||
console.log(`Received ${uptimeData.length} strict default monitoring records for service ${serviceId}`);
|
||||
const processedData = processUptimeData(uptimeData, interval);
|
||||
setHistoryItems(processedData);
|
||||
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
|
||||
// Generate placeholder data when no real data is available
|
||||
console.log(`No strict default monitoring data available for service ${serviceId}, generating placeholder`);
|
||||
|
||||
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
||||
? status
|
||||
: "paused";
|
||||
|
||||
const placeholderHistory: UptimeData[] = Array(20).fill(null).map((_, index) => ({
|
||||
id: `placeholder-${serviceId}-${index}`,
|
||||
service_id: serviceId || "",
|
||||
serviceId: serviceId || "",
|
||||
timestamp: new Date(Date.now() - (index * interval * 1000)).toISOString(),
|
||||
status: statusValue as "up" | "down" | "warning" | "paused",
|
||||
responseTime: 0
|
||||
}));
|
||||
|
||||
setHistoryItems(placeholderHistory);
|
||||
}
|
||||
}, [uptimeData, serviceId, status, interval]);
|
||||
|
||||
// Ensure we always have exactly 20 items for consistent display
|
||||
const getDisplayItems = (): UptimeData[] => {
|
||||
const items = [...historyItems];
|
||||
|
||||
// If we have fewer than 20 items, pad with older placeholder data
|
||||
if (items.length < 20) {
|
||||
const lastItem = items.length > 0 ? items[items.length - 1] : null;
|
||||
const lastStatus = lastItem ? lastItem.status :
|
||||
(status === "up" || status === "down" || status === "warning" || status === "paused") ?
|
||||
status as "up" | "down" | "warning" | "paused" : "paused";
|
||||
|
||||
const paddingCount = 20 - items.length;
|
||||
const paddingItems: UptimeData[] = Array(paddingCount).fill(null).map((_, index) => {
|
||||
const baseTime = lastItem ? new Date(lastItem.timestamp).getTime() : Date.now();
|
||||
const timeOffset = (index + 1) * interval * 1000;
|
||||
|
||||
return {
|
||||
id: `padding-${serviceId}-${index}`,
|
||||
service_id: serviceId || "",
|
||||
serviceId: serviceId || "",
|
||||
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
||||
status: lastStatus,
|
||||
responseTime: 0
|
||||
};
|
||||
});
|
||||
|
||||
items.push(...paddingItems);
|
||||
}
|
||||
|
||||
// Final absolute validation - use Set for guaranteed uniqueness
|
||||
const finalItems = items.slice(0, 20);
|
||||
const seenTimestamps = new Set<string>();
|
||||
const absolutelyUniqueItems: UptimeData[] = [];
|
||||
|
||||
finalItems.forEach(item => {
|
||||
if (!seenTimestamps.has(item.timestamp)) {
|
||||
seenTimestamps.add(item.timestamp);
|
||||
absolutelyUniqueItems.push(item);
|
||||
} else {
|
||||
console.log(`FINAL VALIDATION: Removing absolute duplicate timestamp ${item.timestamp} from display`);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by timestamp (newest first) and ensure exactly 20 items
|
||||
const sortedValidated = absolutelyUniqueItems.sort((a, b) =>
|
||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
|
||||
console.log(`DISPLAY READY: ${sortedValidated.length} absolutely validated default monitoring items`);
|
||||
return sortedValidated.slice(0, 20);
|
||||
};
|
||||
|
||||
return {
|
||||
displayItems: getDisplayItems(),
|
||||
isLoading,
|
||||
error,
|
||||
isFetching,
|
||||
refetch
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { uptimeService } from '@/services/uptimeService';
|
||||
@@ -13,28 +14,24 @@ interface UseUptimeDataProps {
|
||||
export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseUptimeDataProps) => {
|
||||
const [historyItems, setHistoryItems] = useState<UptimeData[]>([]);
|
||||
|
||||
// Fetch real uptime history data if serviceId is provided - filtered for default monitoring with agent_id 1
|
||||
// Fetch ALL uptime history data including regional monitoring data
|
||||
const { data: uptimeData, isLoading, error, isFetching, refetch } = useQuery({
|
||||
queryKey: ['uptimeHistory', serviceId, serviceType],
|
||||
queryKey: ['allUptimeHistory', serviceId, serviceType],
|
||||
queryFn: async () => {
|
||||
if (!serviceId) {
|
||||
console.log('No serviceId provided, skipping fetch');
|
||||
return [];
|
||||
}
|
||||
console.log(`Fetching uptime data for service ${serviceId} of type ${serviceType} - filtering for default monitoring with agent_id 1`);
|
||||
console.log(`Fetching ALL uptime data for service ${serviceId} of type ${serviceType} - including regional monitoring`);
|
||||
|
||||
// Get raw uptime history data
|
||||
const rawData = await uptimeService.getUptimeHistory(serviceId, 50, undefined, undefined, serviceType);
|
||||
|
||||
// Filter for default monitoring records with agent_id 1 only
|
||||
const filteredData = rawData.filter(record => {
|
||||
// Keep records that either have no regional info (pure default) OR have agent_id 1
|
||||
return (!record.region_name && !record.agent_id) ||
|
||||
(record.agent_id === '1' || record.agent_id === 1);
|
||||
});
|
||||
// Include ALL monitoring data - both default and regional
|
||||
const allMonitoringData = rawData.filter(record => record.service_id === serviceId);
|
||||
|
||||
console.log(`Filtered ${rawData.length} records to ${filteredData.length} records for default monitoring with agent_id 1`);
|
||||
return filteredData;
|
||||
console.log(`Retrieved ${rawData.length} total records, filtered to ${allMonitoringData.length} records for service ${serviceId}`);
|
||||
return allMonitoringData;
|
||||
},
|
||||
enabled: !!serviceId,
|
||||
refetchInterval: 30000,
|
||||
@@ -44,21 +41,21 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
||||
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
|
||||
});
|
||||
|
||||
// Filter and process uptime data
|
||||
// Process uptime data to include both default and regional monitoring
|
||||
const processUptimeData = (data: UptimeData[], intervalSeconds: number): UptimeData[] => {
|
||||
if (!data || data.length === 0) return [];
|
||||
|
||||
console.log(`Processing ${data.length} filtered uptime records for service ${serviceId}`);
|
||||
console.log(`Processing ${data.length} uptime records (all monitoring data) for service ${serviceId}`);
|
||||
|
||||
// Sort data by timestamp (newest first)
|
||||
const sortedData = [...data].sort((a, b) =>
|
||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
|
||||
// Take the most recent 20 records to ensure we have enough data
|
||||
const recentData = sortedData.slice(0, 20);
|
||||
// Take the most recent records to ensure we have enough data for both default and regional
|
||||
const recentData = sortedData.slice(0, 50);
|
||||
|
||||
console.log(`Using ${recentData.length} most recent filtered records`);
|
||||
console.log(`Using ${recentData.length} most recent records including both default and regional monitoring`);
|
||||
|
||||
return recentData;
|
||||
};
|
||||
@@ -66,12 +63,12 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
||||
// Update history items when data changes
|
||||
useEffect(() => {
|
||||
if (uptimeData && uptimeData.length > 0) {
|
||||
console.log(`Received ${uptimeData.length} filtered uptime records for service ${serviceId}`);
|
||||
console.log(`Received ${uptimeData.length} uptime records (all monitoring) for service ${serviceId}`);
|
||||
const processedData = processUptimeData(uptimeData, interval);
|
||||
setHistoryItems(processedData);
|
||||
} else if (!serviceId || (uptimeData && uptimeData.length === 0)) {
|
||||
// Generate placeholder data when no real data is available
|
||||
console.log(`No filtered uptime data available for service ${serviceId}, generating placeholder`);
|
||||
console.log(`No uptime data available for service ${serviceId}, generating placeholder`);
|
||||
|
||||
const statusValue = (status === "up" || status === "down" || status === "warning" || status === "paused")
|
||||
? status
|
||||
@@ -90,39 +87,9 @@ export const useUptimeData = ({ serviceId, serviceType, status, interval }: UseU
|
||||
}
|
||||
}, [uptimeData, serviceId, status, interval]);
|
||||
|
||||
// Ensure we always have exactly 20 items for consistent display
|
||||
// Return display items - all monitoring data for tooltip usage
|
||||
const getDisplayItems = (): UptimeData[] => {
|
||||
const items = [...historyItems];
|
||||
|
||||
// If we have fewer than 20 items, pad with older placeholder data
|
||||
if (items.length < 20) {
|
||||
const lastItem = items.length > 0 ? items[items.length - 1] : null;
|
||||
const lastStatus = lastItem ? lastItem.status :
|
||||
(status === "up" || status === "down" || status === "warning" || status === "paused") ?
|
||||
status as "up" | "down" | "warning" | "paused" : "paused";
|
||||
|
||||
const paddingCount = 20 - items.length;
|
||||
const paddingItems: UptimeData[] = Array(paddingCount).fill(null).map((_, index) => {
|
||||
const baseTime = lastItem ? new Date(lastItem.timestamp).getTime() : Date.now();
|
||||
const timeOffset = (index + 1) * interval * 1000;
|
||||
|
||||
return {
|
||||
id: `padding-${serviceId}-${index}`,
|
||||
service_id: serviceId || "",
|
||||
serviceId: serviceId || "",
|
||||
timestamp: new Date(baseTime - timeOffset).toISOString(),
|
||||
status: lastStatus,
|
||||
responseTime: 0
|
||||
};
|
||||
});
|
||||
|
||||
items.push(...paddingItems);
|
||||
}
|
||||
|
||||
// Return exactly 20 items, sorted by timestamp (newest first)
|
||||
return items.slice(0, 20).sort((a, b) =>
|
||||
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||
);
|
||||
return historyItems;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,12 +8,12 @@ interface UptimeSummaryProps {
|
||||
|
||||
export const UptimeSummary = ({ uptime, interval }: UptimeSummaryProps) => {
|
||||
return (
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<div className="flex items-center justify-between text-xs mt-1">
|
||||
<span className="text-muted-foreground">
|
||||
{Math.round(uptime)}% uptime
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Last 20 checks
|
||||
Last 20 checks
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user