CodeReview/src/components/audit/CreateTaskDialog.tsx

552 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
GitBranch,
Settings,
FileText,
AlertCircle,
Info,
Zap,
Shield,
Search
} from "lucide-react";
import { api } from "@/shared/config/database";
import type { Project, CreateAuditTaskForm } from "@/shared/types";
import { toast } from "sonner";
interface CreateTaskDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onTaskCreated: () => void;
preselectedProjectId?: string;
}
export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, preselectedProjectId }: CreateTaskDialogProps) {
const navigate = useNavigate();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [taskForm, setTaskForm] = useState<CreateAuditTaskForm>({
project_id: "",
task_type: "repository",
branch_name: "main",
exclude_patterns: ["node_modules/**", ".git/**", "dist/**", "build/**", "*.log"],
scan_config: {
include_tests: true,
include_docs: false,
max_file_size: 1024, // KB
analysis_depth: "standard"
}
});
const commonExcludePatterns = [
{ label: "node_modules", value: "node_modules/**", description: "Node.js 依赖包" },
{ label: ".git", value: ".git/**", description: "Git 版本控制文件" },
{ label: "dist/build", value: "dist/**", description: "构建输出目录" },
{ label: "logs", value: "*.log", description: "日志文件" },
{ label: "cache", value: ".cache/**", description: "缓存文件" },
{ label: "temp", value: "temp/**", description: "临时文件" },
{ label: "vendor", value: "vendor/**", description: "第三方库" },
{ label: "coverage", value: "coverage/**", description: "测试覆盖率报告" }
];
useEffect(() => {
if (open) {
loadProjects();
// 如果有预选择的项目ID设置到表单中
if (preselectedProjectId) {
setTaskForm(prev => ({ ...prev, project_id: preselectedProjectId }));
}
}
}, [open, preselectedProjectId]);
const loadProjects = async () => {
try {
setLoading(true);
const data = await api.getProjects();
setProjects(data.filter(p => p.is_active));
} catch (error) {
console.error('Failed to load projects:', error);
toast.error("加载项目失败");
} finally {
setLoading(false);
}
};
const handleCreateTask = async () => {
if (!taskForm.project_id) {
toast.error("请选择项目");
return;
}
if (taskForm.task_type === "repository" && !taskForm.branch_name?.trim()) {
toast.error("请输入分支名称");
return;
}
try {
setCreating(true);
await api.createAuditTask({
...taskForm,
created_by: null // 无登录场景下设置为null
} as any);
// 显示详细的提示信息
toast.success("审计任务创建成功", {
description: '因为网络和代码文件大小等因素审计时长通常至少需要1分钟请耐心等待...',
duration: 5000
});
onOpenChange(false);
resetForm();
onTaskCreated();
// 跳转到项目详情页面
navigate(`/projects/${taskForm.project_id}`);
} catch (error) {
console.error('Failed to create task:', error);
toast.error("创建任务失败");
} finally {
setCreating(false);
}
};
const resetForm = () => {
setTaskForm({
project_id: "",
task_type: "repository",
branch_name: "main",
exclude_patterns: ["node_modules/**", ".git/**", "dist/**", "build/**", "*.log"],
scan_config: {
include_tests: true,
include_docs: false,
max_file_size: 1024,
analysis_depth: "standard"
}
});
setSearchTerm("");
};
const toggleExcludePattern = (pattern: string) => {
const patterns = taskForm.exclude_patterns || [];
if (patterns.includes(pattern)) {
setTaskForm({
...taskForm,
exclude_patterns: patterns.filter(p => p !== pattern)
});
} else {
setTaskForm({
...taskForm,
exclude_patterns: [...patterns, pattern]
});
}
};
const addCustomPattern = (pattern: string) => {
if (pattern.trim() && !taskForm.exclude_patterns.includes(pattern.trim())) {
setTaskForm({
...taskForm,
exclude_patterns: [...taskForm.exclude_patterns, pattern.trim()]
});
}
};
const removeExcludePattern = (pattern: string) => {
setTaskForm({
...taskForm,
exclude_patterns: taskForm.exclude_patterns.filter(p => p !== pattern)
});
};
const selectedProject = projects.find(p => p.id === taskForm.project_id);
const filteredProjects = projects.filter(project =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.description?.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center space-x-2">
<Shield className="w-5 h-5 text-primary" />
<span></span>
</DialogTitle>
</DialogHeader>
<div className="space-y-6">
{/* 项目选择 */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-base font-medium"></Label>
<Badge variant="outline" className="text-xs">
{filteredProjects.length}
</Badge>
</div>
{/* 项目搜索 */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索项目名称..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* 项目列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-60 overflow-y-auto">
{loading ? (
<div className="col-span-2 flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<Card
key={project.id}
className={`cursor-pointer transition-all hover:shadow-md ${
taskForm.project_id === project.id
? 'ring-2 ring-primary bg-primary/5'
: 'hover:bg-gray-50'
}`}
onClick={() => setTaskForm({ ...taskForm, project_id: project.id })}
>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className="font-medium text-sm">{project.name}</h4>
{project.description && (
<p className="text-xs text-gray-500 mt-1 line-clamp-2">
{project.description}
</p>
)}
<div className="flex items-center space-x-4 mt-2 text-xs text-gray-400">
<span>{project.repository_type?.toUpperCase() || 'OTHER'}</span>
<span>{project.default_branch}</span>
</div>
</div>
{taskForm.project_id === project.id && (
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-white"></div>
</div>
)}
</div>
</CardContent>
</Card>
))
) : (
<div className="col-span-2 text-center py-8 text-gray-500">
<FileText className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">
{searchTerm ? '未找到匹配的项目' : '暂无可用项目'}
</p>
</div>
)}
</div>
</div>
{/* 任务配置 */}
{selectedProject && (
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="basic" className="flex items-center space-x-2">
<GitBranch className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="exclude" className="flex items-center space-x-2">
<FileText className="w-4 h-4" />
<span></span>
</TabsTrigger>
<TabsTrigger value="advanced" className="flex items-center space-x-2">
<Settings className="w-4 h-4" />
<span></span>
</TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4 mt-6">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="task_type"></Label>
<Select
value={taskForm.task_type}
onValueChange={(value: any) => setTaskForm({ ...taskForm, task_type: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="repository">
<div className="flex items-center space-x-2">
<GitBranch className="w-4 h-4" />
<span></span>
</div>
</SelectItem>
<SelectItem value="instant">
<div className="flex items-center space-x-2">
<Zap className="w-4 h-4" />
<span></span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
{taskForm.task_type === "repository" && (
<div className="space-y-2">
<Label htmlFor="branch_name"></Label>
<Input
id="branch_name"
value={taskForm.branch_name || ""}
onChange={(e) => setTaskForm({ ...taskForm, branch_name: e.target.value })}
placeholder={selectedProject.default_branch || "main"}
/>
</div>
)}
</div>
{/* 项目信息展示 */}
<Card className="bg-blue-50 border-blue-200">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<Info className="w-5 h-5 text-blue-600 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-blue-900 mb-1">{selectedProject.name}</p>
<div className="text-blue-700 space-y-1">
{selectedProject.description && (
<p>{selectedProject.description}</p>
)}
<p>{selectedProject.default_branch}</p>
{selectedProject.programming_languages && (
<p>{JSON.parse(selectedProject.programming_languages).join(', ')}</p>
)}
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="exclude" className="space-y-4 mt-6">
<div className="space-y-4">
<div>
<Label className="text-base font-medium"></Label>
<p className="text-sm text-gray-500 mt-1">
</p>
</div>
{/* 常用排除模式 */}
<div className="grid grid-cols-2 gap-3">
{commonExcludePatterns.map((pattern) => (
<div key={pattern.value} className="flex items-center space-x-3 p-3 border rounded-lg hover:bg-gray-50">
<Checkbox
checked={taskForm.exclude_patterns.includes(pattern.value)}
onCheckedChange={() => toggleExcludePattern(pattern.value)}
/>
<div className="flex-1">
<p className="text-sm font-medium">{pattern.label}</p>
<p className="text-xs text-gray-500">{pattern.description}</p>
</div>
</div>
))}
</div>
{/* 自定义排除模式 */}
<div className="space-y-2">
<Label></Label>
<div className="flex space-x-2">
<Input
placeholder="例如: *.tmp, test/**"
onKeyPress={(e) => {
if (e.key === 'Enter') {
addCustomPattern(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
/>
<Button
type="button"
variant="outline"
onClick={(e) => {
const input = e.currentTarget.previousElementSibling as HTMLInputElement;
addCustomPattern(input.value);
input.value = '';
}}
>
</Button>
</div>
</div>
{/* 已选择的排除模式 */}
{taskForm.exclude_patterns.length > 0 && (
<div className="space-y-2">
<Label></Label>
<div className="flex flex-wrap gap-2">
{taskForm.exclude_patterns.map((pattern) => (
<Badge
key={pattern}
variant="secondary"
className="cursor-pointer hover:bg-red-100 hover:text-red-800"
onClick={() => removeExcludePattern(pattern)}
>
{pattern} ×
</Badge>
))}
</div>
</div>
)}
</div>
</TabsContent>
<TabsContent value="advanced" className="space-y-4 mt-6">
<div className="space-y-6">
<div>
<Label className="text-base font-medium"></Label>
<p className="text-sm text-gray-500 mt-1">
</p>
</div>
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
<div className="flex items-center space-x-3">
<Checkbox
checked={taskForm.scan_config.include_tests}
onCheckedChange={(checked) =>
setTaskForm({
...taskForm,
scan_config: { ...taskForm.scan_config, include_tests: !!checked }
})
}
/>
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500"> *test*, *spec* </p>
</div>
</div>
<div className="flex items-center space-x-3">
<Checkbox
checked={taskForm.scan_config.include_docs}
onCheckedChange={(checked) =>
setTaskForm({
...taskForm,
scan_config: { ...taskForm.scan_config, include_docs: !!checked }
})
}
/>
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500"> README, docs </p>
</div>
</div>
</div>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="max_file_size"> (KB)</Label>
<Input
id="max_file_size"
type="number"
value={taskForm.scan_config.max_file_size}
onChange={(e) =>
setTaskForm({
...taskForm,
scan_config: {
...taskForm.scan_config,
max_file_size: parseInt(e.target.value) || 1024
}
})
}
min="1"
max="10240"
/>
</div>
<div className="space-y-2">
<Label htmlFor="analysis_depth"></Label>
<Select
value={taskForm.scan_config.analysis_depth}
onValueChange={(value: any) =>
setTaskForm({
...taskForm,
scan_config: { ...taskForm.scan_config, analysis_depth: value }
})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="basic"></SelectItem>
<SelectItem value="standard"></SelectItem>
<SelectItem value="deep"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 分析深度说明 */}
<Card className="bg-amber-50 border-amber-200">
<CardContent className="p-4">
<div className="flex items-start space-x-3">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-amber-900 mb-2"></p>
<ul className="text-amber-800 space-y-1 text-xs">
<li> <strong></strong></li>
<li> <strong></strong></li>
<li> <strong></strong></li>
</ul>
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
)}
{/* 操作按钮 */}
<div className="flex justify-end space-x-3 pt-4 border-t">
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={creating}>
</Button>
<Button
onClick={handleCreateTask}
disabled={!taskForm.project_id || creating}
className="btn-primary"
>
{creating ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
...
</>
) : (
<>
<Shield className="w-4 h-4 mr-2" />
</>
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}