You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
254 lines
7.5 KiB
254 lines
7.5 KiB
import React, { useState, useRef, useEffect } from 'react';
|
|
import { Card, Button, Space, Tag, message } from 'antd';
|
|
import {
|
|
SaveOutlined,
|
|
ReloadOutlined,
|
|
BugOutlined,
|
|
CheckCircleOutlined,
|
|
ExclamationCircleOutlined
|
|
} from '@ant-design/icons';
|
|
import { vulnerabilityService } from '../../services/api';
|
|
import { Vulnerability } from '../../types';
|
|
import './CodeEditor.css';
|
|
|
|
interface CodeEditorProps {
|
|
filePath: string;
|
|
content: string;
|
|
language: string;
|
|
vulnerabilities: Vulnerability[];
|
|
onSave: (content: string) => Promise<void>;
|
|
onRefresh: () => void;
|
|
}
|
|
|
|
const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
filePath,
|
|
content,
|
|
language,
|
|
vulnerabilities,
|
|
onSave,
|
|
onRefresh
|
|
}) => {
|
|
const [editedContent, setEditedContent] = useState(content);
|
|
const [selectedVulnerability, setSelectedVulnerability] = useState<Vulnerability | null>(null);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
|
|
useEffect(() => {
|
|
setEditedContent(content);
|
|
}, [content]);
|
|
|
|
const getSeverityColor = (severity: string) => {
|
|
const colorMap: { [key: string]: string } = {
|
|
critical: '#ff4d4f',
|
|
high: '#ff7a45',
|
|
medium: '#ffa940',
|
|
low: '#73d13d',
|
|
info: '#40a9ff',
|
|
};
|
|
return colorMap[severity] || '#d9d9d9';
|
|
};
|
|
|
|
const getSeverityIcon = (severity: string) => {
|
|
const iconMap: { [key: string]: React.ReactNode } = {
|
|
critical: <ExclamationCircleOutlined style={{ color: '#ff4d4f' }} />,
|
|
high: <ExclamationCircleOutlined style={{ color: '#ff7a45' }} />,
|
|
medium: <BugOutlined style={{ color: '#ffa940' }} />,
|
|
low: <CheckCircleOutlined style={{ color: '#73d13d' }} />,
|
|
info: <CheckCircleOutlined style={{ color: '#40a9ff' }} />,
|
|
};
|
|
return iconMap[severity] || <BugOutlined />;
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
setIsSaving(true);
|
|
try {
|
|
await onSave(editedContent);
|
|
message.success('代码保存成功');
|
|
} catch (error) {
|
|
message.error('代码保存失败');
|
|
console.error(error);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleVulnerabilityClick = (vulnerability: Vulnerability) => {
|
|
setSelectedVulnerability(vulnerability);
|
|
// 滚动到对应行
|
|
if (textareaRef.current) {
|
|
const lines = editedContent.split('\n');
|
|
const targetLine = vulnerability.line_number || 1;
|
|
if (targetLine <= lines.length) {
|
|
const lineHeight = 20; // 估算行高
|
|
const scrollTop = (targetLine - 1) * lineHeight;
|
|
textareaRef.current.scrollTop = scrollTop;
|
|
textareaRef.current.focus();
|
|
}
|
|
}
|
|
};
|
|
|
|
const applyAIFix = async (vulnerability: Vulnerability) => {
|
|
if (!vulnerability.ai_suggestion) {
|
|
message.warning('该漏洞暂无AI修复建议');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 这里可以实现自动应用AI建议的逻辑
|
|
// 目前先显示建议内容
|
|
message.info(`AI修复建议: ${vulnerability.ai_suggestion}`);
|
|
|
|
// 标记漏洞为已修复
|
|
await vulnerabilityService.updateVulnerability(vulnerability.id, {
|
|
status: 'fixed'
|
|
});
|
|
|
|
message.success('漏洞已标记为已修复');
|
|
onRefresh(); // 刷新数据
|
|
} catch (error) {
|
|
message.error('修复失败');
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const renderLineNumbers = () => {
|
|
const lines = editedContent.split('\n');
|
|
return lines.map((_, index) => (
|
|
<div key={index} className="line-number">
|
|
{index + 1}
|
|
</div>
|
|
));
|
|
};
|
|
|
|
const renderVulnerabilityMarkers = () => {
|
|
return vulnerabilities.map((vuln) => (
|
|
<div
|
|
key={vuln.id}
|
|
className="vulnerability-marker"
|
|
style={{
|
|
top: `${((vuln.line_number || 1) - 1) * 20 + 2}px`,
|
|
backgroundColor: getSeverityColor(vuln.severity),
|
|
}}
|
|
onClick={() => handleVulnerabilityClick(vuln)}
|
|
title={`${vuln.severity.toUpperCase()}: ${vuln.message}`}
|
|
/>
|
|
));
|
|
};
|
|
|
|
return (
|
|
<div className="code-editor-container">
|
|
<Card
|
|
title={
|
|
<div className="editor-header">
|
|
<span className="file-path">{filePath}</span>
|
|
<Space>
|
|
<Button
|
|
icon={<ReloadOutlined />}
|
|
onClick={onRefresh}
|
|
size="small"
|
|
>
|
|
刷新
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
icon={<SaveOutlined />}
|
|
onClick={handleSave}
|
|
loading={isSaving}
|
|
size="small"
|
|
>
|
|
保存
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
}
|
|
extra={
|
|
<Space>
|
|
{vulnerabilities.length > 0 && (
|
|
<Tag color="red">
|
|
{vulnerabilities.length} 个漏洞
|
|
</Tag>
|
|
)}
|
|
</Space>
|
|
}
|
|
>
|
|
<div className="editor-content">
|
|
{/* 漏洞标记侧边栏 */}
|
|
<div className="vulnerability-sidebar">
|
|
{renderVulnerabilityMarkers()}
|
|
</div>
|
|
|
|
{/* 行号 */}
|
|
<div className="line-numbers">
|
|
{renderLineNumbers()}
|
|
</div>
|
|
|
|
{/* 代码编辑器 */}
|
|
<div className="editor-main">
|
|
<textarea
|
|
ref={textareaRef}
|
|
className="code-textarea"
|
|
value={editedContent}
|
|
onChange={(e) => setEditedContent(e.target.value)}
|
|
spellCheck={false}
|
|
style={{ fontFamily: 'Monaco, Consolas, "Courier New", monospace' }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 漏洞详情面板 */}
|
|
{selectedVulnerability && (
|
|
<Card
|
|
title={
|
|
<div className="vulnerability-header">
|
|
{getSeverityIcon(selectedVulnerability.severity)}
|
|
<span style={{ marginLeft: 8 }}>
|
|
漏洞详情 - 第 {selectedVulnerability.line_number || 1} 行
|
|
</span>
|
|
<Tag
|
|
color={getSeverityColor(selectedVulnerability.severity)}
|
|
style={{ marginLeft: 8 }}
|
|
>
|
|
{selectedVulnerability.severity.toUpperCase()}
|
|
</Tag>
|
|
</div>
|
|
}
|
|
size="small"
|
|
style={{ marginTop: 16 }}
|
|
extra={
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
onClick={() => applyAIFix(selectedVulnerability)}
|
|
disabled={!selectedVulnerability.ai_suggestion}
|
|
>
|
|
应用AI修复
|
|
</Button>
|
|
}
|
|
>
|
|
<div className="vulnerability-details">
|
|
<p><strong>规则ID:</strong> {selectedVulnerability.rule_id}</p>
|
|
<p><strong>描述:</strong> {selectedVulnerability.message}</p>
|
|
|
|
{selectedVulnerability.ai_suggestion && (
|
|
<div className="ai-suggestion">
|
|
<p><strong>AI修复建议:</strong></p>
|
|
<div className="suggestion-content">
|
|
{selectedVulnerability.ai_suggestion}
|
|
</div>
|
|
{selectedVulnerability.ai_confidence && (
|
|
<p className="confidence">
|
|
<strong>置信度:</strong> {(selectedVulnerability.ai_confidence * 100).toFixed(1)}%
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CodeEditor;
|