191 lines
8.0 KiB
TypeScript
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>
|
|
)
|
|
}
|