feat(audit): Enhance CreateTaskDialog with improved task creation workflow
- Add navigation to project details page after task creation - Update task creation process with more detailed toast notifications - Modify created_by field to handle null scenario for system users - Refactor CreateTaskDialog to improve user experience and error handling - Optimize repoScan service with more robust background task processing - Update example image for visual consistency Improves the audit task creation flow by providing better user feedback and streamlining the post-creation experience.
This commit is contained in:
parent
96e15452e8
commit
9b11e47b36
Binary file not shown.
|
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 368 KiB |
|
|
@ -1,4 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
@ -30,6 +31,7 @@ interface CreateTaskDialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, preselectedProjectId }: CreateTaskDialogProps) {
|
export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, preselectedProjectId }: CreateTaskDialogProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
@ -98,13 +100,21 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
|
|
||||||
await api.createAuditTask({
|
await api.createAuditTask({
|
||||||
...taskForm,
|
...taskForm,
|
||||||
created_by: "system" // 无登录场景下使用系统用户
|
created_by: null // 无登录场景下设置为null
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
toast.success("审计任务创建成功");
|
// 显示详细的提示信息
|
||||||
|
toast.success("审计任务创建成功", {
|
||||||
|
description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
onTaskCreated();
|
onTaskCreated();
|
||||||
|
|
||||||
|
// 跳转到项目详情页面
|
||||||
|
navigate(`/projects/${taskForm.project_id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create task:', error);
|
console.error('Failed to create task:', error);
|
||||||
toast.error("创建任务失败");
|
toast.error("创建任务失败");
|
||||||
|
|
|
||||||
|
|
@ -45,74 +45,81 @@ export async function runRepositoryAudit(params: {
|
||||||
created_by: params.createdBy
|
created_by: params.createdBy
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
try {
|
const taskId = (task as any).id as string;
|
||||||
const m = params.repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/i);
|
|
||||||
if (!m) throw new Error("仅支持 GitHub 仓库 URL,例如 https://github.com/owner/repo");
|
|
||||||
const owner = m[1];
|
|
||||||
const repo = m[2];
|
|
||||||
|
|
||||||
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`;
|
// 启动后台审计任务,不阻塞返回
|
||||||
const tree = await githubApi<{ tree: GithubTreeItem[] }>(treeUrl, params.githubToken);
|
(async () => {
|
||||||
let files = (tree.tree || []).filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes));
|
try {
|
||||||
// 采样限制,优先分析较小文件与常见语言
|
const m = params.repoUrl.match(/github\.com\/(.+?)\/(.+?)(?:\.git)?$/i);
|
||||||
files = files
|
if (!m) throw new Error("仅支持 GitHub 仓库 URL,例如 https://github.com/owner/repo");
|
||||||
.sort((a, b) => (a.path.length - b.path.length))
|
const owner = m[1];
|
||||||
.slice(0, MAX_ANALYZE_FILES);
|
const repo = m[2];
|
||||||
|
|
||||||
let totalFiles = 0, totalLines = 0, createdIssues = 0;
|
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`;
|
||||||
let index = 0;
|
const tree = await githubApi<{ tree: GithubTreeItem[] }>(treeUrl, params.githubToken);
|
||||||
const worker = async () => {
|
let files = (tree.tree || []).filter(i => i.type === "blob" && isTextFile(i.path) && !matchExclude(i.path, excludes));
|
||||||
while (true) {
|
// 采样限制,优先分析较小文件与常见语言
|
||||||
const current = index++;
|
files = files
|
||||||
if (current >= files.length) break;
|
.sort((a, b) => (a.path.length - b.path.length))
|
||||||
const f = files[current];
|
.slice(0, MAX_ANALYZE_FILES);
|
||||||
totalFiles++;
|
|
||||||
try {
|
|
||||||
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${f.path}`;
|
|
||||||
const contentRes = await fetch(rawUrl);
|
|
||||||
if (!contentRes.ok) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; }
|
|
||||||
const content = await contentRes.text();
|
|
||||||
if (content.length > MAX_FILE_SIZE_BYTES) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; }
|
|
||||||
totalLines += content.split(/\r?\n/).length;
|
|
||||||
const language = (f.path.split(".").pop() || "").toLowerCase();
|
|
||||||
const analysis = await CodeAnalysisEngine.analyzeCode(content, language);
|
|
||||||
const issues = analysis.issues || [];
|
|
||||||
createdIssues += issues.length;
|
|
||||||
for (const issue of issues) {
|
|
||||||
await api.createAuditIssue({
|
|
||||||
task_id: (task as any).id,
|
|
||||||
file_path: f.path,
|
|
||||||
line_number: issue.line || null,
|
|
||||||
column_number: issue.column || null,
|
|
||||||
issue_type: issue.type || "maintainability",
|
|
||||||
severity: issue.severity || "low",
|
|
||||||
title: issue.title || "Issue",
|
|
||||||
description: issue.description || null,
|
|
||||||
suggestion: issue.suggestion || null,
|
|
||||||
code_snippet: issue.code_snippet || null,
|
|
||||||
ai_explanation: issue.xai ? JSON.stringify(issue.xai) : (issue.ai_explanation || null),
|
|
||||||
status: "open",
|
|
||||||
resolved_by: null,
|
|
||||||
resolved_at: null
|
|
||||||
} as any);
|
|
||||||
}
|
|
||||||
if (totalFiles % 10 === 0) {
|
|
||||||
await api.updateAuditTask((task as any).id, { status: "running", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues } as any);
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
await new Promise(r=>setTimeout(r, LLM_GAP_MS));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pool = Array.from({ length: Math.min(LLM_CONCURRENCY, files.length) }, () => worker());
|
let totalFiles = 0, totalLines = 0, createdIssues = 0;
|
||||||
await Promise.all(pool);
|
let index = 0;
|
||||||
|
const worker = async () => {
|
||||||
|
while (true) {
|
||||||
|
const current = index++;
|
||||||
|
if (current >= files.length) break;
|
||||||
|
const f = files[current];
|
||||||
|
totalFiles++;
|
||||||
|
try {
|
||||||
|
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${encodeURIComponent(branch)}/${f.path}`;
|
||||||
|
const contentRes = await fetch(rawUrl);
|
||||||
|
if (!contentRes.ok) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; }
|
||||||
|
const content = await contentRes.text();
|
||||||
|
if (content.length > MAX_FILE_SIZE_BYTES) { await new Promise(r=>setTimeout(r, LLM_GAP_MS)); continue; }
|
||||||
|
totalLines += content.split(/\r?\n/).length;
|
||||||
|
const language = (f.path.split(".").pop() || "").toLowerCase();
|
||||||
|
const analysis = await CodeAnalysisEngine.analyzeCode(content, language);
|
||||||
|
const issues = analysis.issues || [];
|
||||||
|
createdIssues += issues.length;
|
||||||
|
for (const issue of issues) {
|
||||||
|
await api.createAuditIssue({
|
||||||
|
task_id: taskId,
|
||||||
|
file_path: f.path,
|
||||||
|
line_number: issue.line || null,
|
||||||
|
column_number: issue.column || null,
|
||||||
|
issue_type: issue.type || "maintainability",
|
||||||
|
severity: issue.severity || "low",
|
||||||
|
title: issue.title || "Issue",
|
||||||
|
description: issue.description || null,
|
||||||
|
suggestion: issue.suggestion || null,
|
||||||
|
code_snippet: issue.code_snippet || null,
|
||||||
|
ai_explanation: issue.xai ? JSON.stringify(issue.xai) : (issue.ai_explanation || null),
|
||||||
|
status: "open",
|
||||||
|
resolved_by: null,
|
||||||
|
resolved_at: null
|
||||||
|
} as any);
|
||||||
|
}
|
||||||
|
if (totalFiles % 10 === 0) {
|
||||||
|
await api.updateAuditTask(taskId, { status: "running", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues } as any);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
await new Promise(r=>setTimeout(r, LLM_GAP_MS));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
await api.updateAuditTask((task as any).id, { status: "completed", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues, quality_score: 0 } as any);
|
const pool = Array.from({ length: Math.min(LLM_CONCURRENCY, files.length) }, () => worker());
|
||||||
return (task as any).id as string;
|
await Promise.all(pool);
|
||||||
} catch (e) {
|
|
||||||
await api.updateAuditTask((task as any).id, { status: "failed" } as any);
|
await api.updateAuditTask(taskId, { status: "completed", total_files: totalFiles, scanned_files: totalFiles, total_lines: totalLines, issues_count: createdIssues, quality_score: 0 } as any);
|
||||||
throw e;
|
} catch (e) {
|
||||||
}
|
console.error('审计任务执行失败:', e);
|
||||||
|
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// 立即返回任务ID,让用户可以跳转到任务详情页面
|
||||||
|
return taskId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams, Link } from "react-router-dom";
|
import { useParams, Link, useNavigate } from "react-router-dom";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
@ -26,6 +26,7 @@ import CreateTaskDialog from "@/components/audit/CreateTaskDialog";
|
||||||
|
|
||||||
export default function ProjectDetail() {
|
export default function ProjectDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [project, setProject] = useState<Project | null>(null);
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
const [tasks, setTasks] = useState<AuditTask[]>([]);
|
const [tasks, setTasks] = useState<AuditTask[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
@ -66,17 +67,28 @@ export default function ProjectDetail() {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
setScanning(true);
|
setScanning(true);
|
||||||
await runRepositoryAudit({
|
console.log('开始启动审计任务...');
|
||||||
|
const taskId = await runRepositoryAudit({
|
||||||
projectId: id,
|
projectId: id,
|
||||||
repoUrl: project.repository_url,
|
repoUrl: project.repository_url,
|
||||||
branch: project.default_branch || 'main',
|
branch: project.default_branch || 'main',
|
||||||
githubToken: undefined,
|
githubToken: undefined,
|
||||||
createdBy: undefined
|
createdBy: undefined
|
||||||
});
|
});
|
||||||
toast.success('已启动仓库审计');
|
|
||||||
await loadProjectData();
|
console.log('审计任务创建成功,taskId:', taskId);
|
||||||
|
|
||||||
|
// 显示详细的提示信息
|
||||||
|
toast.success('审计任务已启动', {
|
||||||
|
description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
// 跳转到任务详情页面
|
||||||
|
console.log('准备跳转到:', `/tasks/${taskId}`);
|
||||||
|
navigate(`/tasks/${taskId}`);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error('启动审计失败:', e);
|
||||||
toast.error(e?.message || '启动审计失败');
|
toast.error(e?.message || '启动审计失败');
|
||||||
} finally {
|
} finally {
|
||||||
setScanning(false);
|
setScanning(false);
|
||||||
|
|
@ -116,7 +128,10 @@ export default function ProjectDetail() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTaskCreated = () => {
|
const handleTaskCreated = () => {
|
||||||
toast.success("审计任务已创建");
|
toast.success("审计任务已创建", {
|
||||||
|
description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
loadProjectData(); // 重新加载项目数据以显示新任务
|
loadProjectData(); // 重新加载项目数据以显示新任务
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Calendar,
|
Calendar,
|
||||||
Users,
|
Users,
|
||||||
Settings,
|
Settings,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Code,
|
Code,
|
||||||
Shield,
|
Shield,
|
||||||
|
|
@ -29,12 +29,15 @@ import { scanZipFile, validateZipFile } from "@/features/projects/services";
|
||||||
import type { Project, CreateProjectForm } from "@/shared/types";
|
import type { Project, CreateProjectForm } from "@/shared/types";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import CreateTaskDialog from "@/components/audit/CreateTaskDialog";
|
||||||
|
|
||||||
export default function Projects() {
|
export default function Projects() {
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
|
const [showCreateTaskDialog, setShowCreateTaskDialog] = useState(false);
|
||||||
|
const [selectedProjectForTask, setSelectedProjectForTask] = useState<string>("");
|
||||||
const [uploadProgress, setUploadProgress] = useState(0);
|
const [uploadProgress, setUploadProgress] = useState(0);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -79,7 +82,7 @@ export default function Projects() {
|
||||||
...createForm,
|
...createForm,
|
||||||
// 无登录场景下不传 owner_id,由后端置为 null
|
// 无登录场景下不传 owner_id,由后端置为 null
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
toast.success("项目创建成功");
|
toast.success("项目创建成功");
|
||||||
setShowCreateDialog(false);
|
setShowCreateDialog(false);
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
|
|
@ -189,6 +192,19 @@ export default function Projects() {
|
||||||
return new Date(dateString).toLocaleDateString('zh-CN');
|
return new Date(dateString).toLocaleDateString('zh-CN');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCreateTask = (projectId: string) => {
|
||||||
|
setSelectedProjectForTask(projectId);
|
||||||
|
setShowCreateTaskDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTaskCreated = () => {
|
||||||
|
toast.success("审计任务已创建", {
|
||||||
|
description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...',
|
||||||
|
duration: 5000
|
||||||
|
});
|
||||||
|
// 任务创建后会自动跳转到项目详情页面
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
|
@ -205,7 +221,7 @@ export default function Projects() {
|
||||||
<h1 className="page-title">项目管理</h1>
|
<h1 className="page-title">项目管理</h1>
|
||||||
<p className="page-subtitle">管理您的代码项目,配置审计规则和查看分析结果</p>
|
<p className="page-subtitle">管理您的代码项目,配置审计规则和查看分析结果</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button>
|
<Button>
|
||||||
|
|
@ -217,13 +233,13 @@ export default function Projects() {
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>创建新项目</DialogTitle>
|
<DialogTitle>创建新项目</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<Tabs defaultValue="repository" className="w-full">
|
<Tabs defaultValue="repository" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-2">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<TabsTrigger value="repository">Git 仓库</TabsTrigger>
|
<TabsTrigger value="repository">Git 仓库</TabsTrigger>
|
||||||
<TabsTrigger value="upload">上传代码</TabsTrigger>
|
<TabsTrigger value="upload">上传代码</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="repository" className="space-y-4 mt-6">
|
<TabsContent value="repository" className="space-y-4 mt-6">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|
@ -237,8 +253,8 @@ export default function Projects() {
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="repository_type">仓库类型</Label>
|
<Label htmlFor="repository_type">仓库类型</Label>
|
||||||
<Select
|
<Select
|
||||||
value={createForm.repository_type}
|
value={createForm.repository_type}
|
||||||
onValueChange={(value: any) => setCreateForm({ ...createForm, repository_type: value })}
|
onValueChange={(value: any) => setCreateForm({ ...createForm, repository_type: value })}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
|
|
@ -323,7 +339,7 @@ export default function Projects() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="upload" className="space-y-4 mt-6">
|
<TabsContent value="upload" className="space-y-4 mt-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="upload-name">项目名称 *</Label>
|
<Label htmlFor="upload-name">项目名称 *</Label>
|
||||||
|
|
@ -490,16 +506,16 @@ export default function Projects() {
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{/* 项目信息 */}
|
{/* 项目信息 */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{project.repository_url && (
|
{project.repository_url && (
|
||||||
<div className="flex items-center text-sm text-gray-500">
|
<div className="flex items-center text-sm text-gray-500">
|
||||||
<GitBranch className="w-4 h-4 mr-2 flex-shrink-0" />
|
<GitBranch className="w-4 h-4 mr-2 flex-shrink-0" />
|
||||||
<a
|
<a
|
||||||
href={project.repository_url}
|
href={project.repository_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="hover:text-primary transition-colors flex items-center truncate"
|
className="hover:text-primary transition-colors flex items-center truncate"
|
||||||
>
|
>
|
||||||
|
|
@ -508,7 +524,7 @@ export default function Projects() {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Calendar className="w-4 h-4 mr-2" />
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
|
|
@ -545,9 +561,13 @@ export default function Projects() {
|
||||||
查看详情
|
查看详情
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button size="sm" className="btn-primary">
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="btn-primary"
|
||||||
|
onClick={() => handleCreateTask(project.id)}
|
||||||
|
>
|
||||||
<Shield className="w-4 h-4 mr-2" />
|
<Shield className="w-4 h-4 mr-2" />
|
||||||
审计
|
新建任务
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
@ -638,6 +658,14 @@ export default function Projects() {
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 创建任务对话框 */}
|
||||||
|
<CreateTaskDialog
|
||||||
|
open={showCreateTaskDialog}
|
||||||
|
onOpenChange={setShowCreateTaskDialog}
|
||||||
|
onTaskCreated={handleTaskCreated}
|
||||||
|
preselectedProjectId={selectedProjectForTask}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue