jira-ai-fixer/portal/src/pages/Issues.tsx

191 lines
8.0 KiB
TypeScript

import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'
import {
Search,
Filter,
RefreshCw,
ExternalLink,
CheckCircle,
Clock,
XCircle,
Activity
} from 'lucide-react'
import { api } from '../lib/api'
import { clsx } from 'clsx'
type IssueStatus = 'pending' | 'analyzing' | 'analyzed' | 'fix_generated' | 'pr_created' | 'accepted' | 'rejected' | 'failed'
interface Issue {
id: string
jira_key: string
title: string
status: IssueStatus
module?: string
confidence?: number
created_at: string
pr_url?: string
}
const statusConfig: Record<IssueStatus, { label: string; color: string; icon: React.ReactNode }> = {
pending: { label: 'Pendente', color: 'bg-gray-100 text-gray-800', icon: <Clock className="h-4 w-4" /> },
analyzing: { label: 'Analisando', color: 'bg-blue-100 text-blue-800', icon: <Activity className="h-4 w-4 animate-spin" /> },
analyzed: { label: 'Analisado', color: 'bg-purple-100 text-purple-800', icon: <CheckCircle className="h-4 w-4" /> },
fix_generated: { label: 'Fix Gerado', color: 'bg-indigo-100 text-indigo-800', icon: <CheckCircle className="h-4 w-4" /> },
pr_created: { label: 'PR Criado', color: 'bg-cyan-100 text-cyan-800', icon: <ExternalLink className="h-4 w-4" /> },
accepted: { label: 'Aceito', color: 'bg-green-100 text-green-800', icon: <CheckCircle className="h-4 w-4" /> },
rejected: { label: 'Rejeitado', color: 'bg-red-100 text-red-800', icon: <XCircle className="h-4 w-4" /> },
failed: { label: 'Falhou', color: 'bg-red-100 text-red-800', icon: <XCircle className="h-4 w-4" /> },
}
export default function Issues() {
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<IssueStatus | ''>('')
const { data, isLoading, refetch } = useQuery({
queryKey: ['issues', statusFilter],
queryFn: () => api.get('/api/issues', {
params: { status: statusFilter || undefined }
}).then(r => r.data),
})
const issues: Issue[] = data?.items || []
const filteredIssues = issues.filter(issue =>
issue.jira_key.toLowerCase().includes(search.toLowerCase()) ||
issue.title.toLowerCase().includes(search.toLowerCase())
)
return (
<div className="space-y-6">
{/* Filters */}
<div className="bg-white rounded-lg shadow p-4">
<div className="flex flex-wrap gap-4 items-center">
<div className="flex-1 min-w-[200px] relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Buscar por JIRA key ou título..."
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex items-center gap-2">
<Filter className="h-5 w-5 text-gray-400" />
<select
value={statusFilter}
onChange={e => setStatusFilter(e.target.value as IssueStatus | '')}
className="border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500"
>
<option value="">Todos os status</option>
{Object.entries(statusConfig).map(([key, config]) => (
<option key={key} value={key}>{config.label}</option>
))}
</select>
</div>
<button
onClick={() => refetch()}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
<RefreshCw className="h-4 w-4" />
Atualizar
</button>
</div>
</div>
{/* Issues Table */}
<div className="bg-white rounded-lg shadow overflow-hidden">
{isLoading ? (
<div className="flex items-center justify-center h-64">
<Activity className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : filteredIssues.length === 0 ? (
<div className="text-center text-gray-500 py-16">
<Clock className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p className="text-lg font-medium">Nenhuma issue encontrada</p>
<p className="text-sm mt-2">
Issues do JIRA aparecerão aqui após configurar os webhooks
</p>
</div>
) : (
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">JIRA Key</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Título</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Módulo</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Confiança</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{filteredIssues.map(issue => {
const status = statusConfig[issue.status]
return (
<tr key={issue.id} className="hover:bg-gray-50">
<td className="px-6 py-4">
<a
href="#"
className="text-blue-600 hover:underline font-medium"
>
{issue.jira_key}
</a>
</td>
<td className="px-6 py-4 text-gray-900 max-w-md truncate">
{issue.title}
</td>
<td className="px-6 py-4 text-gray-500">
{issue.module || '-'}
</td>
<td className="px-6 py-4">
<span className={clsx('inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium', status.color)}>
{status.icon}
{status.label}
</span>
</td>
<td className="px-6 py-4">
{issue.confidence ? (
<div className="flex items-center gap-2">
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full"
style={{ width: `${issue.confidence * 100}%` }}
/>
</div>
<span className="text-sm text-gray-600">
{(issue.confidence * 100).toFixed(0)}%
</span>
</div>
) : '-'}
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<button className="text-blue-600 hover:text-blue-800 text-sm">
Ver
</button>
{issue.pr_url && (
<a
href={issue.pr_url}
target="_blank"
rel="noopener noreferrer"
className="text-green-600 hover:text-green-800 text-sm flex items-center gap-1"
>
PR <ExternalLink className="h-3 w-3" />
</a>
)}
</div>
</td>
</tr>
)
})}
</tbody>
</table>
)}
</div>
</div>
)
}