import { useState } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { comparisonApi, reposApi, extractionApi } from '../lib/api'
import LoadingSpinner from '../components/LoadingSpinner'
export default function PRImpact() {
const queryClient = useQueryClient()
const [repo, setRepo] = useState('')
const [fromRef, setFromRef] = useState('main')
const [toRef, setToRef] = useState('')
const [status, setStatus] = useState<string>('')
const { data: reposData } = useQuery({
queryKey: ['repos'],
queryFn: async () => {
const response = await reposApi.list()
return response.data
},
})
const { data: versionsData } = useQuery({
queryKey: ['versions', repo],
queryFn: async () => {
if (!repo) return null
const response = await reposApi.getVersions(repo)
return response.data
},
enabled: !!repo,
})
const mutation = useMutation({
mutationFn: async () => {
if (!repo || !fromRef || !toRef) {
throw new Error('Repository and both refs are required')
}
setStatus('Checking if refs are extracted...')
// Fetch versions if not already loaded
let versions = (versionsData as Array<{ ref: string; refType: string }>) || []
if (!versions || versions.length === 0) {
const versionsResponse = await reposApi.getVersions(repo)
versions = (versionsResponse.data as Array<{ ref: string; refType: string }>) || []
}
// Check if refs are already extracted
const fromExtracted = versions.some((v) => v.ref === fromRef)
const toExtracted = versions.some((v) => v.ref === toRef)
const refsToExtract: Array<{ ref: string; name: string }> = []
if (!fromExtracted) {
refsToExtract.push({ ref: fromRef, name: 'base ref' })
}
if (!toExtracted) {
refsToExtract.push({ ref: toRef, name: 'PR ref' })
}
// Extract missing refs
if (refsToExtract.length > 0) {
setStatus(`Extracting ${refsToExtract.map((r) => r.name).join(' and ')}...`)
const extractionPromises = refsToExtract.map(async ({ ref, name }) => {
setStatus(`Extracting ${name} (${ref})...`)
try {
const response = await extractionApi.extract(repo, ref, false)
return response.data
} catch (error) {
throw new Error(`Failed to extract ${name} (${ref}): ${error instanceof Error ? error.message : String(error)}`)
}
})
await Promise.all(extractionPromises)
// Invalidate queries to refresh versions list
await queryClient.invalidateQueries({ queryKey: ['versions', repo] })
await queryClient.invalidateQueries({ queryKey: ['repos'] })
setStatus('Extraction complete. Running analysis...')
// Small delay to ensure data is saved and queries refresh
await new Promise((resolve) => setTimeout(resolve, 1000))
}
// Now run the PR impact analysis
setStatus('Analyzing PR impact...')
const response = await comparisonApi.prImpact(repo, fromRef, toRef)
setStatus('')
return response.data
},
})
const repos = (reposData as { repos?: Array<{ name: string }> })?.repos || []
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (repo && fromRef && toRef) {
mutation.mutate()
}
}
return (
<div>
<h1 className="text-3xl font-bold mb-6">PR Impact Preview</h1>
<form onSubmit={handleSubmit} className="bg-gray-800 rounded-lg p-6 mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium mb-2">Repository</label>
<select
value={repo}
onChange={(e) => setRepo(e.target.value)}
required
className="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2"
>
<option value="">Select repo</option>
{repos.map((r) => (
<option key={r.name} value={r.name}>
{r.name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2">Base Ref (from)</label>
<input
type="text"
value={fromRef}
onChange={(e) => setFromRef(e.target.value)}
required
placeholder="main"
className="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2">PR Ref (to)</label>
<input
type="text"
value={toRef}
onChange={(e) => setToRef(e.target.value)}
required
placeholder="feature/new-auth"
className="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2"
/>
</div>
</div>
<button
type="submit"
disabled={mutation.isPending}
className="mt-4 px-6 py-2 bg-blue-600 rounded hover:bg-blue-700 disabled:opacity-50"
>
{mutation.isPending ? 'Processing...' : 'Analyze Impact'}
</button>
</form>
{status && (
<div className="bg-blue-900/30 border border-blue-700 rounded-lg p-4 mb-4">
<div className="flex items-center gap-2">
<LoadingSpinner />
<span>{status}</span>
</div>
</div>
)}
{mutation.isPending && !status && <LoadingSpinner />}
{mutation.error && (
<div className="text-red-500 p-4">
Error: {mutation.error instanceof Error ? mutation.error.message : 'Unknown error'}
</div>
)}
{mutation.data && (
<div className="bg-gray-800 rounded-lg p-6">
<h2 className="text-2xl font-bold mb-4">Impact Analysis Results</h2>
{mutation.data.error ? (
<div className="text-red-500 p-4">
<strong>Error:</strong> {mutation.data.message || mutation.data.error}
</div>
) : (
<>
{mutation.data.summary && (
<div className="mb-4 p-4 bg-gray-700 rounded">
<h3 className="font-semibold mb-2">Summary</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<div className="text-gray-400">Changed Types</div>
<div className="text-lg font-bold">{mutation.data.summary.totalChangedTypes || 0}</div>
</div>
<div>
<div className="text-gray-400">High Risk</div>
<div className="text-lg font-bold text-red-500">{mutation.data.summary.highRiskChanges || 0}</div>
</div>
<div>
<div className="text-gray-400">Affected Repos</div>
<div className="text-lg font-bold">{mutation.data.summary.affectedRepos || 0}</div>
</div>
<div>
<div className="text-gray-400">Total Impact</div>
<div className="text-lg font-bold">{mutation.data.summary.totalImpact || 0}</div>
</div>
</div>
</div>
)}
{mutation.data.impact && Array.isArray(mutation.data.impact) && mutation.data.impact.length > 0 && (
<div className="mb-4">
<h3 className="font-semibold mb-2">Impact Details</h3>
<div className="space-y-2">
{mutation.data.impact.slice(0, 10).map((item: any, idx: number) => (
<div
key={idx}
className={`p-3 rounded border-l-4 ${
item.risk === 'high'
? 'bg-red-900/20 border-red-500'
: item.risk === 'medium'
? 'bg-yellow-900/20 border-yellow-500'
: 'bg-gray-700 border-gray-500'
}`}
>
<div className="flex items-start justify-between">
<div>
<span className="font-semibold">{item.type}</span>
<span className="text-gray-400 ml-2">({item.change})</span>
</div>
<span
className={`px-2 py-1 rounded text-xs ${
item.risk === 'high'
? 'bg-red-600'
: item.risk === 'medium'
? 'bg-yellow-600'
: 'bg-gray-600'
}`}
>
{item.risk} risk
</span>
</div>
{item.affectedRepos && item.affectedRepos.length > 0 && (
<div className="mt-2 text-sm text-gray-300">
Affected repos: {item.affectedRepos.join(', ')}
</div>
)}
{item.fieldChanges && (
<div className="mt-2 text-xs">
{item.fieldChanges.added && item.fieldChanges.added.length > 0 && (
<div className="text-green-400">
+ Added: {item.fieldChanges.added.join(', ')}
</div>
)}
{item.fieldChanges.removed && item.fieldChanges.removed.length > 0 && (
<div className="text-red-400">
- Removed: {item.fieldChanges.removed.join(', ')}
</div>
)}
</div>
)}
</div>
))}
</div>
</div>
)}
<details className="mt-4">
<summary className="cursor-pointer text-sm text-gray-400 hover:text-gray-300">
View raw JSON
</summary>
<pre className="bg-gray-900 p-4 rounded overflow-auto text-sm mt-2">
{JSON.stringify(mutation.data, null, 2)}
</pre>
</details>
</>
)}
</div>
)}
</div>
)
}
export { PRImpact }