All checks were successful
Build and Deploy Log Center / build-and-deploy (push) Successful in 2m16s
- 新增 Project 模型(repo_url, local_path, name, description) - 项目 CRUD API(GET/PUT /api/v1/projects) - 日志上报自动 upsert Project 记录 - ErrorLog 增加 failure_reason 字段 - update_task_status / create_repair_report 写入失败原因 - Repair Agent 优先从 API 获取项目配置,回退 .env - 新增 Web 端「项目管理」页面(表格 + 行内编辑) - BugList/BugDetail/RepairList 展示失败原因 - 更新接入指南文档 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
296 lines
13 KiB
TypeScript
296 lines
13 KiB
TypeScript
import { useState, useEffect } from 'react';
|
||
import { useParams, Link, useLocation } from 'react-router-dom';
|
||
import { ArrowLeft, Play, Loader2, FileCode, GitCommit, History, AlertTriangle } from 'lucide-react';
|
||
import { getBugDetail, triggerRepair, getRepairReportsByBug, type ErrorLog, type RepairReport } from '../api';
|
||
|
||
const SOURCE_LABELS: Record<string, string> = {
|
||
runtime: '运行时',
|
||
cicd: 'CI/CD',
|
||
deployment: '部署',
|
||
};
|
||
|
||
const STATUS_LABELS: Record<string, string> = {
|
||
NEW: '新发现',
|
||
VERIFYING: '验证中',
|
||
CANNOT_REPRODUCE: '无法复现',
|
||
PENDING_FIX: '待修复',
|
||
FIXING: '修复中',
|
||
FIXED: '已修复',
|
||
VERIFIED: '已验证',
|
||
DEPLOYED: '已部署',
|
||
FIX_FAILED: '修复失败',
|
||
};
|
||
|
||
export default function BugDetail() {
|
||
const { id } = useParams<{ id: string }>();
|
||
const location = useLocation();
|
||
const [bug, setBug] = useState<ErrorLog | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [repairing, setRepairing] = useState(false);
|
||
const [repairMessage, setRepairMessage] = useState('');
|
||
const [repairHistory, setRepairHistory] = useState<RepairReport[]>([]);
|
||
|
||
const backSearch = location.state?.fromSearch || '';
|
||
|
||
useEffect(() => {
|
||
const fetchBug = async () => {
|
||
if (!id) return;
|
||
try {
|
||
const response = await getBugDetail(parseInt(id));
|
||
setBug(response.data);
|
||
} catch (error) {
|
||
console.error('Failed to fetch bug:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
fetchBug();
|
||
}, [id]);
|
||
|
||
useEffect(() => {
|
||
if (id) {
|
||
getRepairReportsByBug(parseInt(id)).then(res => {
|
||
setRepairHistory(res.data.items);
|
||
}).catch(console.error);
|
||
}
|
||
}, [id]);
|
||
|
||
const handleTriggerRepair = async () => {
|
||
if (!bug) return;
|
||
setRepairing(true);
|
||
setRepairMessage('');
|
||
try {
|
||
await triggerRepair(bug.id);
|
||
setBug({ ...bug, status: 'PENDING_FIX' });
|
||
setRepairMessage('已成功触发修复');
|
||
} catch (error) {
|
||
console.error('Failed to trigger repair:', error);
|
||
setRepairMessage('触发修复失败');
|
||
} finally {
|
||
setRepairing(false);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="loading">
|
||
<div className="spinner"></div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!bug) {
|
||
return <div className="loading">未找到该缺陷</div>;
|
||
}
|
||
|
||
const isRuntime = !bug.source || bug.source === 'runtime';
|
||
const canTriggerRepair = ['NEW', 'FIX_FAILED'].includes(bug.status) && isRuntime;
|
||
|
||
return (
|
||
<div>
|
||
<Link to={`/bugs${backSearch ? `?${backSearch}` : ''}`} className="back-link">
|
||
<ArrowLeft size={14} />
|
||
返回缺陷列表
|
||
</Link>
|
||
|
||
<div className="detail-card">
|
||
<div className="detail-header">
|
||
<div>
|
||
<h2 className="detail-title" style={{ color: 'var(--error)' }}>
|
||
{bug.error_type}: {bug.error_message}
|
||
</h2>
|
||
<div className="detail-meta">
|
||
<span>项目:{bug.project_id}</span>
|
||
<span>来源:<span className={`source-badge source-${bug.source || 'runtime'}`}>{SOURCE_LABELS[bug.source] || '运行时'}</span></span>
|
||
<span>环境:{bug.environment}</span>
|
||
<span>级别:{bug.level}</span>
|
||
</div>
|
||
</div>
|
||
<span className={`status-badge status-${bug.status}`}>
|
||
{STATUS_LABELS[bug.status] || bug.status}
|
||
</span>
|
||
</div>
|
||
|
||
{bug.file_path && (
|
||
<div className="detail-section">
|
||
<div className="detail-section-title">文件位置</div>
|
||
<div className="detail-section-value">
|
||
<FileCode size={14} style={{ display: 'inline', verticalAlign: 'middle', marginRight: '6px' }} />
|
||
{bug.file_path} : 第 {bug.line_number} 行
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{bug.source === 'cicd' && bug.context?.workflow_name && (
|
||
<div className="detail-section">
|
||
<div className="detail-section-title">CI/CD 信息</div>
|
||
<div className="detail-section-value">
|
||
工作流:{bug.context.workflow_name} / {bug.context.job_name} / {bug.context.step_name}
|
||
{bug.context.branch && <><br />分支:{bug.context.branch}</>}
|
||
{bug.context.run_url && (
|
||
<><br /><a href={bug.context.run_url} target="_blank" rel="noopener noreferrer">查看 CI 日志</a></>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{bug.source === 'deployment' && bug.context?.pod_name && (
|
||
<div className="detail-section">
|
||
<div className="detail-section-title">部署信息</div>
|
||
<div className="detail-section-value">
|
||
命名空间:{bug.context.namespace} | Pod:{bug.context.pod_name}
|
||
<br />
|
||
容器:{bug.context.container_name} | 重启次数:{bug.context.restart_count}
|
||
{bug.context.node_name && <><br />节点:{bug.context.node_name}</>}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{bug.commit_hash && (
|
||
<div className="detail-section">
|
||
<div className="detail-section-title">Git 信息</div>
|
||
<div className="detail-section-value">
|
||
<GitCommit size={14} style={{ display: 'inline', verticalAlign: 'middle', marginRight: '6px' }} />
|
||
{bug.commit_hash}
|
||
{bug.version && ` | v${bug.version}`}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="detail-section">
|
||
<div className="detail-section-title">堆栈跟踪</div>
|
||
<pre className="code-block error">
|
||
{typeof bug.stack_trace === 'string'
|
||
? bug.stack_trace
|
||
: JSON.stringify(bug.stack_trace, null, 2)}
|
||
</pre>
|
||
</div>
|
||
|
||
{bug.context && Object.keys(bug.context).length > 0 && (
|
||
<div className="detail-section">
|
||
<div className="detail-section-title">上下文信息</div>
|
||
<pre className="code-block accent">
|
||
{JSON.stringify(bug.context, null, 2)}
|
||
</pre>
|
||
</div>
|
||
)}
|
||
|
||
<div className="actions-bar">
|
||
<button
|
||
className="trigger-repair-btn"
|
||
onClick={handleTriggerRepair}
|
||
disabled={!canTriggerRepair || repairing}
|
||
>
|
||
{repairing ? (
|
||
<Loader2 size={14} className="spinner" />
|
||
) : (
|
||
<Play size={14} />
|
||
)}
|
||
{repairing ? '触发中...' : '触发修复'}
|
||
</button>
|
||
{repairMessage && (
|
||
<span style={{
|
||
fontSize: '13px',
|
||
color: repairMessage.includes('成功') ? 'var(--success)' : 'var(--error)'
|
||
}}>
|
||
{repairMessage}
|
||
</span>
|
||
)}
|
||
{!canTriggerRepair && !repairing && (
|
||
<span style={{ fontSize: '13px', color: 'var(--text-tertiary)' }}>
|
||
{!isRuntime
|
||
? 'CI/CD 和部署错误暂不支持自动修复'
|
||
: '仅"新发现"或"修复失败"状态的缺陷可触发修复'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{bug.failure_reason && (
|
||
<div className="detail-card" style={{ borderLeft: '3px solid var(--error)' }}>
|
||
<div className="detail-section-title" style={{ marginBottom: '8px', display: 'flex', alignItems: 'center', gap: '6px', color: 'var(--error)' }}>
|
||
<AlertTriangle size={14} />
|
||
修复失败原因
|
||
</div>
|
||
<pre className="code-block error">{bug.failure_reason}</pre>
|
||
</div>
|
||
)}
|
||
|
||
<div className="detail-card">
|
||
<div className="detail-section-title" style={{ marginBottom: '12px' }}>元数据</div>
|
||
<table className="meta-table">
|
||
<tbody>
|
||
<tr>
|
||
<td className="meta-label">缺陷编号</td>
|
||
<td>{bug.id}</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="meta-label">指纹</td>
|
||
<td className="cell-mono">{bug.fingerprint}</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="meta-label">重试次数</td>
|
||
<td>{bug.retry_count}</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="meta-label">上报时间</td>
|
||
<td>{new Date(bug.timestamp).toLocaleString()}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{repairHistory.length > 0 && (
|
||
<div className="detail-card">
|
||
<div className="detail-section-title" style={{ marginBottom: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||
<History size={14} />
|
||
修复历史 ({repairHistory.length} 次尝试)
|
||
</div>
|
||
<div className="table-container">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>轮次</th>
|
||
<th>状态</th>
|
||
<th>测试结果</th>
|
||
<th>失败原因</th>
|
||
<th>时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{repairHistory.map(report => (
|
||
<tr key={report.id}>
|
||
<td>第 {report.repair_round} 轮</td>
|
||
<td>
|
||
<span className={`status-badge status-${report.status}`}>
|
||
{STATUS_LABELS[report.status] || report.status}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<span className={report.test_passed ? 'test-pass' : 'test-fail'}>
|
||
{report.test_passed ? '通过' : '失败'}
|
||
</span>
|
||
</td>
|
||
<td style={{ maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||
{report.failure_reason || '-'}
|
||
</td>
|
||
<td className="cell-secondary">
|
||
{new Date(report.created_at).toLocaleString()}
|
||
</td>
|
||
<td>
|
||
<Link to={`/repairs/${report.id}`} className="btn-link">
|
||
查看
|
||
</Link>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|