Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 1x 1x 1x | 'use client';
import React, { useState } from 'react';
import { TimeRangeSelector, type TimeRange } from '../TimeRangeSelector';
import { ResponseTimeChart, type TimeSeriesDataPoint } from '../charts/ResponseTimeChart';
import { RequestVolumeChart, type VolumeDataPoint } from '../charts/RequestVolumeChart';
import { EndpointDistributionChart, type EndpointDistribution } from '../charts/EndpointDistributionChart';
import { StatusCodeChart, type StatusCodeData } from '../charts/StatusCodeChart';
import { PerformanceMetricCard } from '../performance/PerformanceMetricCard';
import { ExportButton } from '../performance/ExportButton';
import {
usePerformancePolling,
formatLastUpdated,
type PerformanceData,
} from '@/hooks/usePerformancePolling';
import { formatDuration, formatNumber } from '@/lib/monitoring/percentiles';
export interface PerformanceDashboardProps {
/** Initial data to display before polling starts */
initialData?: PerformanceData;
/** Default time range */
defaultTimeRange?: TimeRange;
/** Default auto-refresh state */
defaultAutoRefresh?: boolean;
/** Polling interval in milliseconds */
pollingInterval?: number;
}
/**
* PerformanceDashboard - Main dashboard component for performance monitoring
* Combines all chart components with time range selection and auto-refresh
*/
export function PerformanceDashboard({
initialData,
defaultTimeRange = '24h',
defaultAutoRefresh = true,
pollingInterval = 30000,
}: PerformanceDashboardProps) {
const [timeRange, setTimeRange] = useState<TimeRange>(defaultTimeRange);
const [autoRefresh, setAutoRefresh] = useState(defaultAutoRefresh);
const { data, isLoading, isRefetching, lastUpdated, refetch } = usePerformancePolling({
interval: autoRefresh ? pollingInterval : null,
timeRange,
enabled: true,
});
// Use initial data if no polled data yet
const displayData = data || initialData;
return (
<div className="space-y-6" data-testid="performance-dashboard">
{/* Header with controls */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex items-center gap-4">
<TimeRangeSelector value={timeRange} onChange={setTimeRange} />
</div>
<div className="flex items-center gap-4">
{/* Auto-refresh toggle */}
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={autoRefresh}
onChange={(e) => setAutoRefresh(e.target.checked)}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
<span className="text-sm text-gray-700 dark:text-gray-300">Auto-refresh</span>
</label>
{/* Manual refresh button */}
<button
onClick={() => refetch()}
disabled={isRefetching}
className="px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isRefetching ? 'Refreshing...' : 'Refresh'}
</button>
{/* Export button */}
<ExportButton timeRange={timeRange} />
{/* Last updated */}
<span className="text-sm text-gray-500 dark:text-gray-400">
Updated: {formatLastUpdated(lastUpdated)}
</span>
</div>
</div>
{/* Summary Cards */}
{displayData && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
<PerformanceMetricCard
title="Total Requests"
value={formatNumber(displayData.summary.totalRequests)}
subtitle={`Last ${timeRange}`}
/>
<PerformanceMetricCard
title="Avg Response Time"
value={formatDuration(displayData.summary.avgResponseTime)}
subtitle="All endpoints"
thresholds={{ good: 100, warning: 300 }}
numericValue={displayData.summary.avgResponseTime}
/>
<PerformanceMetricCard
title="P95 Response"
value={formatDuration(displayData.summary.p95ResponseTime)}
subtitle="95th percentile"
thresholds={{ good: 200, warning: 500 }}
numericValue={displayData.summary.p95ResponseTime}
/>
<PerformanceMetricCard
title="P99 Response"
value={formatDuration(displayData.summary.p99ResponseTime)}
subtitle="99th percentile"
thresholds={{ good: 500, warning: 1000 }}
numericValue={displayData.summary.p99ResponseTime}
/>
<PerformanceMetricCard
title="Success Rate"
value={`${displayData.summary.successRate.toFixed(1)}%`}
subtitle="2xx responses"
valueClassName={
displayData.summary.successRate >= 99
? 'text-green-600 dark:text-green-400'
: displayData.summary.successRate >= 95
? 'text-yellow-600 dark:text-yellow-400'
: 'text-red-600 dark:text-red-400'
}
/>
</div>
)}
{/* Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ResponseTimeChart
data={displayData?.timeSeries || []}
timeRange={timeRange}
showP95
showP99
isLoading={isLoading}
/>
<RequestVolumeChart
data={displayData?.volume || []}
timeRange={timeRange}
stacked
isLoading={isLoading}
/>
<EndpointDistributionChart
data={displayData?.endpoints || []}
maxSlices={8}
isLoading={isLoading}
/>
<StatusCodeChart data={displayData?.statusCodes || []} isLoading={isLoading} />
</div>
{/* Slow Requests List */}
{displayData && displayData.slowRequests.length > 0 && (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
Slow Requests ({'>'}500ms)
</h3>
<div className="space-y-2">
{displayData.slowRequests.slice(0, 10).map((request) => (
<div
key={request.id}
className="flex items-center justify-between p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded"
>
<div>
<span className="font-medium text-red-600 dark:text-red-400">
{formatDuration(request.duration)}
</span>
<span className="ml-2 text-sm text-gray-700 dark:text-gray-300">
{request.name}
</span>
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
{new Date(request.timestamp).toLocaleString()}
</span>
</div>
))}
</div>
</div>
)}
{/* Loading overlay for initial load */}
{isLoading && !displayData && (
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-blue-500 border-t-transparent mb-4" />
<p className="text-gray-500 dark:text-gray-400">Loading performance data...</p>
</div>
</div>
)}
</div>
);
}
// Re-export types
export type { TimeSeriesDataPoint, VolumeDataPoint, EndpointDistribution, StatusCodeData, PerformanceData };
|