DependencyGraph.tsx
1 import { useQuery } from '@tanstack/react-query' 2 import type { Chain } from '../../types/vote' 3 import * as rollbackService from '../../services/rollback' 4 5 interface DependencyGraphProps { 6 repoId: string 7 chain: Chain 8 highlightPrHash?: string 9 } 10 11 export default function DependencyGraph({ 12 repoId, 13 chain, 14 highlightPrHash, 15 }: DependencyGraphProps) { 16 const { data: dependencies = [], isLoading } = useQuery({ 17 queryKey: ['prDependencies', repoId, chain], 18 queryFn: () => rollbackService.getPRDependencies(repoId, chain), 19 }) 20 21 if (isLoading) { 22 return ( 23 <div className="bg-white border rounded-lg p-6"> 24 <div className="flex justify-center"> 25 <div className="animate-spin h-6 w-6 border-2 border-gray-400 border-t-transparent rounded-full" /> 26 </div> 27 </div> 28 ) 29 } 30 31 if (dependencies.length === 0) { 32 return ( 33 <div className="bg-white border rounded-lg p-6"> 34 <p className="text-gray-500 text-sm text-center"> 35 No PR dependencies found for this repository. 36 </p> 37 </div> 38 ) 39 } 40 41 // Build graph structure 42 const nodes = new Map<string, DependencyNode>() 43 for (const dep of dependencies) { 44 nodes.set(dep.prHash, { 45 prHash: dep.prHash, 46 dependsOn: dep.dependsOn, 47 dependedBy: dep.dependedBy, 48 level: 0, 49 }) 50 } 51 52 // Calculate levels (simple BFS) 53 const rootNodes = dependencies.filter((d) => d.dependsOn.length === 0) 54 const queue = [...rootNodes.map((r) => r.prHash)] 55 const visited = new Set<string>() 56 57 while (queue.length > 0) { 58 const current = queue.shift()! 59 if (visited.has(current)) continue 60 visited.add(current) 61 62 const node = nodes.get(current) 63 if (!node) continue 64 65 for (const child of node.dependedBy) { 66 const childNode = nodes.get(child) 67 if (childNode) { 68 childNode.level = Math.max(childNode.level, node.level + 1) 69 queue.push(child) 70 } 71 } 72 } 73 74 // Group by level 75 const levels: string[][] = [] 76 for (const [hash, node] of nodes) { 77 if (!levels[node.level]) levels[node.level] = [] 78 levels[node.level].push(hash) 79 } 80 81 return ( 82 <div className="bg-white border rounded-lg p-4"> 83 <h4 className="font-medium mb-4">PR Dependency Graph</h4> 84 85 <div className="space-y-4"> 86 {levels.map((levelNodes, level) => ( 87 <div key={level}> 88 <div className="text-xs text-gray-500 mb-2"> 89 Level {level} {level === 0 ? '(Root PRs)' : ''} 90 </div> 91 <div className="flex flex-wrap gap-2"> 92 {levelNodes.map((prHash) => { 93 const node = nodes.get(prHash)! 94 const isHighlighted = prHash === highlightPrHash 95 const hasDependents = node.dependedBy.length > 0 96 97 return ( 98 <div 99 key={prHash} 100 className={`relative group ${ 101 isHighlighted 102 ? 'ring-2 ring-blue-500 ring-offset-2 rounded-lg' 103 : '' 104 }`} 105 > 106 <div 107 className={`px-3 py-2 rounded-lg text-xs font-mono ${ 108 hasDependents 109 ? 'bg-amber-100 border border-amber-300' 110 : 'bg-gray-100 border border-gray-300' 111 }`} 112 > 113 {prHash.slice(0, 8)}... 114 {hasDependents && ( 115 <span className="ml-2 text-amber-600"> 116 ({node.dependedBy.length} dependents) 117 </span> 118 )} 119 </div> 120 121 {/* Tooltip */} 122 <div className="absolute bottom-full left-0 mb-2 hidden group-hover:block z-10"> 123 <div className="bg-gray-900 text-white text-xs rounded-lg p-3 shadow-lg min-w-48"> 124 <div className="font-mono mb-2">{prHash}</div> 125 {node.dependsOn.length > 0 && ( 126 <div className="mb-2"> 127 <div className="text-gray-400">Depends on:</div> 128 {node.dependsOn.map((d) => ( 129 <div key={d} className="font-mono text-green-400"> 130 {d.slice(0, 8)}... 131 </div> 132 ))} 133 </div> 134 )} 135 {node.dependedBy.length > 0 && ( 136 <div> 137 <div className="text-gray-400">Depended by:</div> 138 {node.dependedBy.map((d) => ( 139 <div key={d} className="font-mono text-amber-400"> 140 {d.slice(0, 8)}... 141 </div> 142 ))} 143 </div> 144 )} 145 </div> 146 </div> 147 </div> 148 ) 149 })} 150 </div> 151 </div> 152 ))} 153 </div> 154 155 {/* Legend */} 156 <div className="mt-6 pt-4 border-t flex gap-4 text-xs"> 157 <div className="flex items-center gap-2"> 158 <div className="w-4 h-4 bg-gray-100 border border-gray-300 rounded" /> 159 <span className="text-gray-600">Leaf PR (no dependents)</span> 160 </div> 161 <div className="flex items-center gap-2"> 162 <div className="w-4 h-4 bg-amber-100 border border-amber-300 rounded" /> 163 <span className="text-gray-600">Has dependents (risky to rollback)</span> 164 </div> 165 </div> 166 </div> 167 ) 168 } 169 170 interface DependencyNode { 171 prHash: string 172 dependsOn: string[] 173 dependedBy: string[] 174 level: number 175 } 176 177 /** 178 * Compact dependency indicator for PR cards 179 */ 180 export function DependencyBadge({ 181 dependsOn, 182 dependedBy, 183 }: { 184 dependsOn: number 185 dependedBy: number 186 }) { 187 if (dependsOn === 0 && dependedBy === 0) return null 188 189 return ( 190 <div className="flex items-center gap-2 text-xs"> 191 {dependsOn > 0 && ( 192 <span className="px-2 py-0.5 bg-green-100 text-green-700 rounded"> 193 {dependsOn} deps 194 </span> 195 )} 196 {dependedBy > 0 && ( 197 <span className="px-2 py-0.5 bg-amber-100 text-amber-700 rounded"> 198 {dependedBy} dependents 199 </span> 200 )} 201 </div> 202 ) 203 }