Open Source Team Metrics based on PRs
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 274 lines 9.7 kB view raw
1"use client" 2 3import * as React from "react" 4import { useTeamFilterParams } from "@/hooks/use-team-filter" 5import { LineChart, Line, ResponsiveContainer, Tooltip } from "recharts" 6import { IconArrowUpRight, IconArrowDownRight } from "@tabler/icons-react" 7 8import { 9 Card, 10 CardContent, 11 CardHeader, 12 CardTitle, 13} from "@/components/ui/card" 14 15type TimeSeriesDataPoint = { 16 date: string; 17 prThroughput: number; 18 cycleTime: number; 19 reviewTime: number; 20 codingHours: number; 21}; 22 23interface EnhancedCompactEngineeringMetricsProps { 24 initialData?: TimeSeriesDataPoint[]; 25 className?: string; 26} 27 28export function EnhancedCompactEngineeringMetrics({ 29 initialData, 30 className 31}: EnhancedCompactEngineeringMetricsProps) { 32 const [chartData, setChartData] = React.useState<TimeSeriesDataPoint[]>(initialData || []); 33 const [loading, setLoading] = React.useState(!initialData); 34 const [error, setError] = React.useState<string | null>(null); 35 const teamFilterParams = useTeamFilterParams(); 36 37 // Background refresh after initial load 38 React.useEffect(() => { 39 if (!initialData) return; 40 41 // Delay background refresh by 2 seconds to not interfere with initial render 42 const refreshTimer = setTimeout(async () => { 43 try { 44 const url = `/api/metrics/time-series?${teamFilterParams}`; 45 const response = await fetch(url); 46 if (response.ok) { 47 const data = await response.json(); 48 setChartData(data); 49 } 50 } catch (error) { 51 // Silent fail on background refresh - we have initial data 52 console.warn("Background refresh failed:", error); 53 } 54 }, 2000); 55 56 return () => clearTimeout(refreshTimer); 57 }, [initialData, teamFilterParams]); 58 59 React.useEffect(() => { 60 if (initialData) return; // Skip if we have initial data 61 62 const fetchData = async () => { 63 try { 64 setLoading(true); 65 setError(null); 66 67 const url = `/api/metrics/time-series?${teamFilterParams}`; 68 const response = await fetch(url); 69 70 if (!response.ok) { 71 throw new Error(`Failed to fetch metrics: ${response.status} ${response.statusText}`); 72 } 73 74 const data = await response.json(); 75 setChartData(data); 76 } catch (error) { 77 console.error("Failed to load time series data:", error); 78 setError(error instanceof Error ? error.message : "An unknown error occurred"); 79 } finally { 80 setLoading(false); 81 } 82 }; 83 84 fetchData(); 85 }, [initialData, teamFilterParams]); 86 87 const { filteredData, metrics } = React.useMemo(() => { 88 if (!chartData.length) return { 89 filteredData: [], 90 metrics: { 91 prCurrent: 0, prChange: 0, 92 cycleCurrent: 0, cycleChange: 0, 93 reviewCurrent: 0, reviewChange: 0, 94 codingCurrent: 0, codingChange: 0 95 } 96 }; 97 98 // Sort data by date in ascending order 99 const sortedData = [...chartData].sort((a, b) => 100 new Date(a.date).getTime() - new Date(b.date).getTime() 101 ); 102 103 const filtered = sortedData; 104 105 // Calculate metrics 106 const latest = filtered[filtered.length - 1] || { prThroughput: 0, cycleTime: 0, reviewTime: 0, codingHours: 0 }; 107 const previous = filtered[filtered.length - 2] || { prThroughput: 0, cycleTime: 0, reviewTime: 0, codingHours: 0 }; 108 109 const prCurrent = latest.prThroughput; 110 const prChange = previous.prThroughput === 0 ? 0 : 111 ((prCurrent - previous.prThroughput) / previous.prThroughput) * 100; 112 113 const cycleCurrent = latest.cycleTime; 114 const cycleChange = previous.cycleTime === 0 ? 0 : 115 ((cycleCurrent - previous.cycleTime) / previous.cycleTime) * 100; 116 117 const reviewCurrent = latest.reviewTime; 118 const reviewChange = previous.reviewTime === 0 ? 0 : 119 ((reviewCurrent - previous.reviewTime) / previous.reviewTime) * 100; 120 121 const codingCurrent = latest.codingHours; 122 const codingChange = previous.codingHours === 0 ? 0 : 123 ((codingCurrent - previous.codingHours) / previous.codingHours) * 100; 124 125 return { 126 filteredData: filtered, 127 metrics: { 128 prCurrent, 129 prChange: isNaN(prChange) ? 0 : prChange, 130 cycleCurrent, 131 cycleChange: isNaN(cycleChange) ? 0 : cycleChange, 132 reviewCurrent, 133 reviewChange: isNaN(reviewChange) ? 0 : reviewChange, 134 codingCurrent, 135 codingChange: isNaN(codingChange) ? 0 : codingChange 136 } 137 }; 138 }, [chartData]); 139 140 const metricsConfig = [ 141 { 142 name: "Shipping Velocity", 143 value: metrics.prCurrent, 144 change: metrics.prChange, 145 dataKey: "prThroughput", 146 color: "#3b82f6", 147 unit: "", 148 isReversed: false 149 }, 150 { 151 name: "Delivery Speed", 152 value: metrics.cycleCurrent, 153 change: metrics.cycleChange, 154 dataKey: "cycleTime", 155 color: "#f97316", 156 unit: "hrs", 157 isReversed: true 158 }, 159 { 160 name: "Feedback Time", 161 value: metrics.reviewCurrent, 162 change: metrics.reviewChange, 163 dataKey: "reviewTime", 164 color: "#a855f7", 165 unit: "hrs", 166 isReversed: true 167 }, 168 { 169 name: "Flow State", 170 value: metrics.codingCurrent, 171 change: metrics.codingChange, 172 dataKey: "codingHours", 173 color: "#10b981", 174 unit: "hrs", 175 isReversed: false 176 } 177 ]; 178 179 if (loading) { 180 return ( 181 <Card className={`@container/card bg-gradient-to-t from-gray-50/30 to-white dark:from-card/10 dark:to-card shadow-xs ${className}`}> 182 <CardHeader className="pb-2"> 183 <CardTitle className="text-base">Team Flow Metrics</CardTitle> 184 </CardHeader> 185 <CardContent className="h-[220px] flex items-center justify-center"> 186 <div className="animate-pulse w-full h-2/3 bg-muted rounded"></div> 187 </CardContent> 188 </Card> 189 ); 190 } 191 192 if (error) { 193 return ( 194 <Card className={`@container/card bg-gradient-to-t from-gray-50/30 to-white dark:from-card/10 dark:to-card shadow-xs ${className}`}> 195 <CardHeader className="pb-2"> 196 <CardTitle className="text-base">Team Flow Metrics</CardTitle> 197 </CardHeader> 198 <CardContent> 199 <p className="text-red-500">{error}</p> 200 <button 201 onClick={() => window.location.reload()} 202 className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" 203 > 204 Retry 205 </button> 206 </CardContent> 207 </Card> 208 ); 209 } 210 211 return ( 212 <Card className={`@container/card bg-gradient-to-t from-gray-50/30 to-white dark:from-card/10 dark:to-card shadow-xs ${className}`}> 213 <CardHeader className="pb-0"> 214 <CardTitle className="text-base flex items-center justify-between"> 215 Team Flow Metrics 216 {initialData && ( 217 <span className="text-xs text-muted-foreground font-normal"> 218 Server-enhanced 219 </span> 220 )} 221 </CardTitle> 222 </CardHeader> 223 <CardContent className="pt-2"> 224 <div className="space-y-4"> 225 {metricsConfig.map((metric) => ( 226 <div key={metric.name} className="flex items-start space-x-4"> 227 <div className="w-[140px] pt-1"> 228 <div className="space-y-1"> 229 <div className="text-xs font-medium text-muted-foreground">{metric.name}</div> 230 <div className="flex items-center"> 231 <span className="text-sm font-bold mr-1">{metric.value.toFixed(1)}{metric.unit}</span> 232 <span className={`text-xs font-medium ${ 233 metric.isReversed ? 234 (metric.change < 0 ? 'text-green-500' : metric.change > 0 ? 'text-orange-500' : 'text-muted-foreground') : 235 (metric.change > 0 ? 'text-green-500' : metric.change < 0 ? 'text-orange-500' : 'text-muted-foreground') 236 }`}> 237 {metric.isReversed ? 238 (metric.change < 0 ? <IconArrowDownRight className="h-3 w-3 mr-0.5" /> : metric.change > 0 ? <IconArrowUpRight className="h-3 w-3 mr-0.5" /> : null) : 239 (metric.change > 0 ? <IconArrowUpRight className="h-3 w-3 mr-0.5" /> : metric.change < 0 ? <IconArrowDownRight className="h-3 w-3 mr-0.5" /> : null) 240 } 241 {Math.abs(metric.change).toFixed(1)}% 242 </span> 243 </div> 244 </div> 245 </div> 246 <div className="flex-1 h-[40px]"> 247 <ResponsiveContainer width="100%" height="100%"> 248 <LineChart data={filteredData}> 249 <Line 250 type="monotone" 251 dataKey={metric.dataKey} 252 stroke={metric.color} 253 strokeWidth={3.5} 254 dot={false} 255 activeDot={{ r: 6, strokeWidth: 0 }} 256 /> 257 <Tooltip 258 formatter={(value) => [`${value}${metric.unit}`, metric.name]} 259 labelFormatter={(label) => { 260 const date = new Date(label); 261 return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); 262 }} 263 contentStyle={{ fontSize: '12px' }} 264 /> 265 </LineChart> 266 </ResponsiveContainer> 267 </div> 268 </div> 269 ))} 270 </div> 271 </CardContent> 272 </Card> 273 ); 274}