mirror of
https://github.com/chaitin/MonkeyCode.git
synced 2026-02-02 14:53:55 +08:00
352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
import Card from '@/components/card';
|
||
import { Ellipsis, Modal } from '@c-x/ui';
|
||
|
||
import { useEffect, useRef, useState } from 'react';
|
||
|
||
import { DomainSecurityScanningResult, DomainSecurityScanningRiskDetail } from '@/api/types';
|
||
import { getSecurityScanningDetail, getUserSecurityScanningDetail } from '@/api';
|
||
import { Box, CircularProgress, Grid2 as Grid, List, ListItem, ListItemButton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||
import ListAltIcon from '@mui/icons-material/ListAlt';
|
||
import ViewSidebarOutlinedIcon from '@mui/icons-material/ViewSidebarOutlined';
|
||
import MonacoEditor from '@monaco-editor/react';
|
||
import type * as Monaco from 'monaco-editor';
|
||
|
||
interface RiskLevelBoxProps {
|
||
level: 'severe' | 'critical' | 'suggest';
|
||
}
|
||
|
||
const RiskLevelBox = ({ level }: RiskLevelBoxProps) => {
|
||
const riskConfig = {
|
||
severe: {
|
||
text: '严重',
|
||
color: 'risk.severe',
|
||
},
|
||
critical: {
|
||
text: '高风险',
|
||
color: 'risk.critical',
|
||
},
|
||
suggest: {
|
||
text: '低风险',
|
||
color: 'risk.suggest',
|
||
},
|
||
};
|
||
|
||
const config = riskConfig[level];
|
||
|
||
if (!config) return null;
|
||
|
||
return (
|
||
<Box sx={{
|
||
backgroundColor: config.color,
|
||
color: '#fff',
|
||
borderRadius: '4px',
|
||
textAlign: 'center',
|
||
width: '80px',
|
||
minWidth: '80px',
|
||
fontSize: '12px',
|
||
lineHeight: '20px'
|
||
}}>
|
||
{config.text}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
const getLanguageByFilename = (filename: string = ''): string => {
|
||
const extension = filename.split('.').pop()?.toLowerCase() || '';
|
||
const languageMap: Record<string, string> = {
|
||
js: 'javascript',
|
||
jsx: 'javascript',
|
||
ts: 'typescript',
|
||
tsx: 'typescript',
|
||
json: 'json',
|
||
html: 'html',
|
||
htm: 'html',
|
||
css: 'css',
|
||
scss: 'scss',
|
||
sass: 'sass',
|
||
less: 'less',
|
||
md: 'markdown',
|
||
markdown: 'markdown',
|
||
py: 'python',
|
||
pyw: 'python',
|
||
java: 'java',
|
||
c: 'c',
|
||
cpp: 'cpp',
|
||
cc: 'cpp',
|
||
cxx: 'cpp',
|
||
h: 'c',
|
||
hpp: 'cpp',
|
||
cs: 'csharp',
|
||
php: 'php',
|
||
php3: 'php',
|
||
php4: 'php',
|
||
php5: 'php',
|
||
phtml: 'php',
|
||
rb: 'ruby',
|
||
go: 'go',
|
||
rs: 'rust',
|
||
swift: 'swift',
|
||
kt: 'kotlin',
|
||
kts: 'kotlin',
|
||
scala: 'scala',
|
||
sh: 'shell',
|
||
bash: 'shell',
|
||
sql: 'sql',
|
||
yaml: 'yaml',
|
||
yml: 'yaml',
|
||
xml: 'xml',
|
||
vue: 'vue',
|
||
svelte: 'svelte',
|
||
};
|
||
|
||
return languageMap[extension] || 'plaintext';
|
||
};
|
||
|
||
const TaskDetail = ({
|
||
task,
|
||
open,
|
||
onClose,
|
||
admin,
|
||
}: {
|
||
task?: DomainSecurityScanningResult;
|
||
open: boolean;
|
||
onClose: () => void;
|
||
admin: boolean
|
||
}) => {
|
||
const [loading, setLoading] = useState(true);
|
||
const [vulns, setVulns] = useState<DomainSecurityScanningRiskDetail[]>([]);
|
||
const [vulDetail, setVulDetail] = useState<DomainSecurityScanningRiskDetail | undefined>(undefined);
|
||
const [viewMode, setViewMode] = useState<'list' | 'detail'>('detail');
|
||
|
||
const fetchData = async () => {
|
||
setLoading(true);
|
||
const resp = await (admin ? getSecurityScanningDetail : getUserSecurityScanningDetail)({
|
||
id: task?.id as string
|
||
});
|
||
setVulns(resp);
|
||
setLoading(false);
|
||
};
|
||
|
||
useEffect(() => {
|
||
setVulns([]);
|
||
setVulDetail(undefined);
|
||
setViewMode('detail');
|
||
console.log(!!vulDetail)
|
||
if (open) {
|
||
fetchData();
|
||
};
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [task, open]);
|
||
|
||
// 保存编辑器实例的引用
|
||
const editorRef = useRef<Monaco.editor.IStandaloneCodeEditor | null>(null);
|
||
|
||
// 监听 vulDetail 的变化,重新高亮显示
|
||
useEffect(() => {
|
||
if (editorRef.current) {
|
||
highlightVulnerability(editorRef.current, vulDetail);
|
||
}
|
||
}, [vulDetail]);
|
||
|
||
// 高亮显示漏洞代码
|
||
const highlightVulnerability = (editor: Monaco.editor.IStandaloneCodeEditor, vulDetail: DomainSecurityScanningRiskDetail | undefined) => {
|
||
// 清除之前的装饰器
|
||
editor.deltaDecorations(editor.getModel()?.getAllDecorations().map(d => d.id) || [], []);
|
||
|
||
// 如果有 start 和 end 位置信息,则设置选区
|
||
if (vulDetail?.start && vulDetail?.end) {
|
||
const startLine = vulDetail.start.line ?? 1;
|
||
const startColumn = vulDetail.start.col ?? 1;
|
||
const endLine = vulDetail.end.line ?? startLine;
|
||
const endColumn = vulDetail.end.col ?? startColumn;
|
||
|
||
// 设置选区
|
||
const selection = {
|
||
startLineNumber: startLine,
|
||
startColumn: startColumn,
|
||
endLineNumber: endLine,
|
||
endColumn: endColumn
|
||
};
|
||
|
||
editor.setSelection(selection);
|
||
|
||
// 添加装饰器以增强高亮效果
|
||
editor.deltaDecorations([], [
|
||
{
|
||
range: selection,
|
||
options: {
|
||
isWholeLine: false,
|
||
className: 'highlighted-code',
|
||
inlineClassName: 'highlighted-code-inline',
|
||
overviewRuler: {
|
||
color: 'rgba(255, 255, 0, 0.5)',
|
||
position: 1 // Monaco.Editor.OverviewRulerLane.Center
|
||
}
|
||
}
|
||
}
|
||
]);
|
||
|
||
// 滚动到选区
|
||
editor.revealLineInCenter(startLine);
|
||
}
|
||
};
|
||
|
||
const handleEditorDidMount = (editor: Monaco.editor.IStandaloneCodeEditor) => {
|
||
// 保存编辑器实例
|
||
editorRef.current = editor;
|
||
|
||
// 初始高亮
|
||
highlightVulnerability(editor, vulDetail);
|
||
};
|
||
|
||
return (
|
||
<Modal title={
|
||
<Stack direction={'row'} >
|
||
{vulDetail && <ToggleButtonGroup
|
||
exclusive
|
||
value={viewMode}
|
||
size="small"
|
||
onChange={(e, value) => {
|
||
setViewMode(value)
|
||
}}
|
||
>
|
||
<ToggleButton value="list">
|
||
<ListAltIcon />
|
||
</ToggleButton>
|
||
<ToggleButton value="detail" disabled={!vulDetail}>
|
||
<ViewSidebarOutlinedIcon sx={{ transform: 'rotate(180deg)' }} />
|
||
</ToggleButton>
|
||
</ToggleButtonGroup>}
|
||
<Ellipsis
|
||
sx={{
|
||
fontWeight: 'bold',
|
||
fontSize: 20,
|
||
lineHeight: '40px',
|
||
width: 700,
|
||
ml: '20px'
|
||
}}
|
||
>
|
||
{task?.name} / {task?.project_name}
|
||
</Ellipsis>
|
||
</Stack>
|
||
}
|
||
width={1200}
|
||
open={open}
|
||
onCancel={onClose}
|
||
footer={null}
|
||
>
|
||
<Card sx={{ p: 0, background: 'transparent', boxShadow: 'none' }}>
|
||
<Grid container sx={{ height: '70vh' }}>
|
||
<Grid size={viewMode === 'detail' && !!vulDetail ? 5 : 12} sx={{
|
||
height: '100%',
|
||
overflow: 'auto'
|
||
}}>
|
||
<List>
|
||
{loading ? (
|
||
<div style={{ display: 'flex', justifyContent: 'center', padding: '20px' }}>
|
||
<CircularProgress />
|
||
</div>
|
||
) : (
|
||
vulns.map((vuln) => (
|
||
<ListItem key={vuln.id}
|
||
sx={{
|
||
padding: 0,
|
||
width: '100%'
|
||
}}>
|
||
<ListItemButton
|
||
selected={vulDetail?.id === vuln.id}
|
||
onClick={() => {
|
||
setVulDetail(vuln);
|
||
}}
|
||
sx={{
|
||
borderBottomWidth: '1px',
|
||
borderBottomStyle: 'solid',
|
||
borderBottomColor: 'background.paper',
|
||
fontSize: '14px',
|
||
width: '100%'
|
||
}}>
|
||
<Stack direction={"column"} sx={{
|
||
width: '100%'
|
||
}}>
|
||
<Stack direction={"row"}>
|
||
<RiskLevelBox level={vuln.level as 'severe' | 'critical' | 'suggest'} />
|
||
<Box sx={{
|
||
fontSize: '14px',
|
||
ml: '20px',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
lineHeight: '20px'
|
||
}}>{vuln.desc}</Box>
|
||
</Stack>
|
||
<Box sx={{
|
||
color: 'text.tertiary',
|
||
fontSize: '14px',
|
||
mt: '6px',
|
||
width: '100%',
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
}}>
|
||
{vuln.filename}:{vuln?.start?.line}
|
||
</Box>
|
||
|
||
{!!vulDetail && vulDetail?.id === vuln.id && <Box sx={{
|
||
fontSize: '12px',
|
||
mt: '6px',
|
||
}}>关键代码位于第 {vulDetail?.start?.line} 行{vulDetail?.start?.line !== vulDetail?.end?.line && `至第 ${vulDetail?.end?.line} 行`}</Box>}
|
||
{!!vulDetail && vulDetail?.id === vuln.id && <Box sx={{
|
||
fontSize: '12px',
|
||
mt: '6px',
|
||
}}>
|
||
<pre style={{
|
||
backgroundColor: '#fff',
|
||
padding: '10px',
|
||
borderRadius: '4px',
|
||
border: '1px solid #ccc',
|
||
overflow: 'hidden',
|
||
}}>{vulDetail?.lines}</pre>
|
||
</Box>}
|
||
{!!vulDetail && vulDetail?.id === vuln.id && <Box sx={{
|
||
fontSize: '12px',
|
||
mt: '6px',
|
||
}}>修改建议:{vulDetail?.fix}</Box>}
|
||
</Stack>
|
||
</ListItemButton>
|
||
</ListItem>
|
||
))
|
||
)}
|
||
</List>
|
||
</Grid>
|
||
{viewMode === 'detail' && !!vulDetail && <Grid size={7} sx={{
|
||
height: '100%',
|
||
overflow: 'auto'
|
||
}}>
|
||
<Box sx={{
|
||
width: '100%',
|
||
height: '100%',
|
||
}}>
|
||
<style> {`.monaco-editor.vs-dark .highlighted-code { background-color: rgba(255, 0, 0, 0.3) !important; }`} </style>
|
||
<MonacoEditor
|
||
height="100%"
|
||
value={vulDetail?.content || ''}
|
||
theme='vs-dark'
|
||
language={getLanguageByFilename(vulDetail?.filename)}
|
||
options={{
|
||
readOnly: true,
|
||
minimap: {
|
||
enabled: false
|
||
}
|
||
}}
|
||
onMount={handleEditorDidMount}
|
||
></MonacoEditor>
|
||
</Box>
|
||
</Grid>}
|
||
</Grid>
|
||
</Card>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default TaskDetail;
|