diff --git a/application/src/components/services/ResponseTimeChart.tsx b/application/src/components/services/ResponseTimeChart.tsx index 51d04e2..3d70d59 100644 --- a/application/src/components/services/ResponseTimeChart.tsx +++ b/application/src/components/services/ResponseTimeChart.tsx @@ -1,27 +1,34 @@ -import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line } from "recharts"; +import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, LineChart, Line, BarChart, Bar } from "recharts"; import { UptimeData } from "@/types/service.types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useTheme } from "@/contexts/ThemeContext"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { format } from "date-fns"; +import { Button } from "@/components/ui/button"; +import { AreaChart as AreaChartIcon, BarChart3, TrendingUp } from "lucide-react"; interface ResponseTimeChartProps { uptimeData: UptimeData[]; } +type ChartType = 'area' | 'line' | 'bar'; + export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { const { theme } = useTheme(); + const [chartType, setChartType] = useState('area'); - // Modern color palette for different chart lines with solid colors at 90-100% opacity - const modernColors = [ - { stroke: '#f59e0b', fill: 'rgba(111, 86, 63, 0.95)' }, // Yellow (changed from blue) - { stroke: '#10b981', fill: 'rgba(0, 84, 56, 0.95)' }, // Emerald - { stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.95)' }, // Blue (moved to second position) - { stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.95)' }, // Red - { stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.95)' }, // Violet - { stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.95)' }, // Cyan - { stroke: '#f97316', fill: 'rgba(231, 148, 89, 0.95)' }, // Orange - { stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.95)' }, // Lime + // Fixed color palette - consistent colors that don't change + const fixedColors = [ + { stroke: '#3b82f6', fill: 'rgba(59, 130, 246, 0.2)', name: 'Ocean Blue' }, + { stroke: '#10b981', fill: 'rgba(16, 185, 129, 0.2)', name: 'Emerald Green' }, + { stroke: '#f59e0b', fill: 'rgba(245, 158, 11, 0.2)', name: 'Golden Amber' }, + { stroke: '#ef4444', fill: 'rgba(239, 68, 68, 0.2)', name: 'Ruby Red' }, + { stroke: '#8b5cf6', fill: 'rgba(139, 92, 246, 0.2)', name: 'Royal Purple' }, + { stroke: '#06b6d4', fill: 'rgba(6, 182, 212, 0.2)', name: 'Sky Cyan' }, + { stroke: '#f97316', fill: 'rgba(249, 115, 22, 0.2)', name: 'Sunset Orange' }, + { stroke: '#84cc16', fill: 'rgba(132, 204, 22, 0.2)', name: 'Fresh Lime' }, + { stroke: '#ec4899', fill: 'rgba(236, 72, 153, 0.2)', name: 'Vibrant Pink' }, + { stroke: '#14b8a6', fill: 'rgba(20, 184, 166, 0.2)', name: 'Ocean Teal' }, ]; // Check if we have data from multiple sources @@ -160,7 +167,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { return [Math.max(0, minValue - padding), maxValue + padding]; }, [chartData, hasMultipleSources]); - // Get unique sources for legend with proper labeling and colors + // Get unique sources for legend with consistent fixed colors const sources = useMemo(() => { if (!hasMultipleSources) return []; @@ -175,25 +182,29 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { } }); - const regionalSources = Array.from(sourceSet).map((source, index) => { + // Sort sources to ensure consistent ordering + const sortedSources = Array.from(sourceSet).sort(); + + const regionalSources = sortedSources.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})`; + label = `${regionName} (Agent ${data.agent_id})`; } else if (regionName === 'Default' && data?.agent_id === '1') { - label = `Default System Check (Agent 1)`; + label = `Default System Check`; } - const colorIndex = index % modernColors.length; + const colorIndex = index % fixedColors.length; return { key: `regional_${source.replace('|', '_')}`, label: label, - stroke: modernColors[colorIndex].stroke, - fill: modernColors[colorIndex].fill + stroke: fixedColors[colorIndex].stroke, + fill: fixedColors[colorIndex].fill, + colorName: fixedColors[colorIndex].name }; }); @@ -205,9 +216,10 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { const defaultSources = (hasPureDefault && !hasRegionalDefault) ? [{ key: 'default', - label: 'Default', - stroke: modernColors[regionalSources.length % modernColors.length].stroke, - fill: modernColors[regionalSources.length % modernColors.length].fill + label: 'Default System', + stroke: fixedColors[regionalSources.length % fixedColors.length].stroke, + fill: fixedColors[regionalSources.length % fixedColors.length].fill, + colorName: fixedColors[regionalSources.length % fixedColors.length].name }] : []; return [...defaultSources, ...regionalSources]; @@ -219,13 +231,13 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { const data = payload[0].payload; return ( -
-

{String(label)}

-

{String(data.date)}

+
+

{String(label)}

+

{String(data.date)}

{hasMultipleSources ? ( // Multi-source tooltip -
+
{sources.map(source => { const valueKey = source.key === 'default' ? 'defaultValue' : `${source.key}_value`; const statusKey = source.key === 'default' ? 'defaultStatus' : `${source.key}_status`; @@ -234,35 +246,45 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { if (value === undefined && status === undefined) return null; - let statusColor = "bg-gray-800"; + let statusBadgeClass = "px-2 py-1 rounded-full text-xs font-medium"; let statusText = "No Data"; if (status === "up") { - statusColor = "bg-emerald-800"; + statusBadgeClass += " bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"; statusText = "Up"; } else if (status === "down") { - statusColor = "bg-red-800"; + statusBadgeClass += " bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"; statusText = "Down"; } else if (status === "warning") { - statusColor = "bg-yellow-800"; + statusBadgeClass += " bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"; statusText = "Warning"; } else if (status === "paused") { - statusColor = "bg-gray-800"; + statusBadgeClass += " bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"; statusText = "Paused"; + } else { + statusBadgeClass += " bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"; } return ( -
-
+
+
- {String(source.label)} +
+ {String(source.label)} + {source.colorName} +
-
- {status === "paused" ? "Paused" : - value !== null && value !== undefined ? `${value} ms` : "No data"} +
+ + {statusText} + +
+ {status === "paused" ? "Paused" : + value !== null && value !== undefined ? `${value} ms` : "No data"} +
); @@ -271,19 +293,19 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { ) : ( // Single source tooltip - showing status with color <> -
-
+
- { + { data.status === "up" ? "Up" : data.status === "down" ? "Down" : data.status === "warning" ? "Warning" : "Paused" }
-

+

{data.status === "paused" ? "Monitoring paused" : data.value !== null ? `${data.value} ms` : "No data"}

@@ -295,6 +317,182 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { return null; }; + // Render different chart types + const renderChart = () => { + const commonProps = { + data: chartData, + margin: { top: 10, right: 30, left: 0, bottom: 30 } + }; + + const commonAxisProps = { + xAxis: { + dataKey: "time", + stroke: theme === 'dark' ? '#666' : '#9ca3af', + angle: -45, + textAnchor: "end" as const, + tick: { fontSize: 10 }, + height: 60, + interval: "preserveStartEnd" as const, + minTickGap: 5 + }, + yAxis: { + stroke: theme === 'dark' ? '#666' : '#9ca3af', + allowDecimals: false, + domain: yAxisDomain + } + }; + + if (hasMultipleSources) { + switch (chartType) { + case 'line': + return ( + + + + + } /> + + {sources.find(s => s.key === 'default') && ( + s.key === 'default')?.stroke} + strokeWidth={3} + dot={{ r: 4, strokeWidth: 2 }} + connectNulls={false} + /> + )} + + {sources.filter(s => s.key !== 'default').map((source) => ( + + ))} + + ); + + case 'bar': + return ( + + + + + } /> + + {sources.find(s => s.key === 'default') && ( + s.key === 'default')?.stroke} + opacity={0.8} + /> + )} + + {sources.filter(s => s.key !== 'default').map((source) => ( + + ))} + + ); + + default: // area + return ( + + + + + } /> + + {sources.find(s => s.key === 'default') && ( + s.key === 'default')?.stroke} + fill={sources.find(s => s.key === 'default')?.fill} + strokeWidth={3} + dot={false} + connectNulls={false} + /> + )} + + {sources.filter(s => s.key !== 'default').map((source) => ( + + ))} + + ); + } + } else { + // Single source charts remain the same + return ( + + + + + } /> + + + + + + + + {chartData.map((entry, index) => + entry.status === 'paused' ? ( + + ) : null + )} + + ); + } + }; + // Check if we have any data to display const hasData = uptimeData.length > 0; @@ -319,21 +517,55 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { Response Time History - {hasData && ( - - {dateRangeDisplay} - - )} +
+ {hasMultipleSources && ( +
+ + + +
+ )} + {hasData && ( + + {dateRangeDisplay} + + )} +
{hasMultipleSources && ( -
+
+
+ Monitoring Sources: +
{sources.map(source => ( -
+
- {String(source.label)} + {String(source.label)} + ({source.colorName})
))}
@@ -347,118 +579,7 @@ export function ResponseTimeChart({ uptimeData }: ResponseTimeChartProps) { ) : (
- {hasMultipleSources ? ( - - - - - } /> - - {/* Default monitoring area with modern solid background */} - {sources.find(s => s.key === 'default') && ( - s.key === 'default')?.stroke} - fill={sources.find(s => s.key === 'default')?.fill} - strokeWidth={2.5} - dot={false} - connectNulls={false} - /> - )} - - {/* Regional monitoring areas with modern solid backgrounds */} - {sources.filter(s => s.key !== 'default').map((source) => ( - - ))} - - ) : ( - // For single regional agent or default monitoring only - use solid background colors - - - - - } /> - - {/* Modern solid background areas for different statuses */} - - - - - - - {/* Reference lines for paused status */} - {chartData.map((entry, index) => - entry.status === 'paused' ? ( - - ) : null - )} - - )} + {renderChart()}
)}