From cd54c102562cd699da09ac5851e16846dffdc4ea Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 28 Feb 2026 16:26:11 +0800 Subject: [PATCH] feat(discovery): add FindingDrawer component and restructure i18n keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add FindingDrawer component for displaying finding details when no associated issue exists - Refactor i18n keys for better organization: - status.* → session.status.* (session-related) - severity.* → findings.severity.* (finding-related) - Update DiscoveryDetail to show FindingDrawer for orphan findings - Add severity/priority mapping in discovery-routes for compatibility --- .../issue/discovery/DiscoveryCard.tsx | 8 +- .../issue/discovery/DiscoveryDetail.tsx | 26 ++- .../issue/discovery/FindingDrawer.tsx | 213 ++++++++++++++++++ .../issue/discovery/FindingList.tsx | 18 +- ccw/frontend/src/lib/api.ts | 12 +- ccw/frontend/src/locales/en/issues.json | 9 +- ccw/frontend/src/locales/zh/issues.json | 9 +- ccw/frontend/src/pages/DiscoveryPage.tsx | 9 +- ccw/src/core/routes/discovery-routes.ts | 8 +- ccw/src/tools/write-file.ts | 4 +- 10 files changed, 291 insertions(+), 25 deletions(-) create mode 100644 ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx diff --git a/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx b/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx index 89196c55..128b1f0c 100644 --- a/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx +++ b/ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx @@ -21,17 +21,17 @@ const statusConfig = { running: { icon: Clock, variant: 'warning' as const, - label: 'issues.discovery.status.running', + label: 'issues.discovery.session.status.running', }, completed: { icon: CheckCircle, variant: 'success' as const, - label: 'issues.discovery.status.completed', + label: 'issues.discovery.session.status.completed', }, failed: { icon: XCircle, variant: 'destructive' as const, - label: 'issues.discovery.status.failed', + label: 'issues.discovery.session.status.failed', }, }; @@ -79,7 +79,7 @@ export function DiscoveryCard({ session, isActive, onClick }: DiscoveryCardProps
- {formatMessage({ id: 'issues.discovery.findings' })}: + {formatMessage({ id: 'issues.discovery.session.findings' }, { count: session.findings_count })}: {session.findings_count}
diff --git a/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx b/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx index fd0ea1fe..2ccf0110 100644 --- a/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx +++ b/ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx @@ -12,6 +12,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs'; import { Badge } from '@/components/ui/Badge'; import { Progress } from '@/components/ui/Progress'; import { IssueDrawer } from '@/components/issue/hub/IssueDrawer'; +import { FindingDrawer } from './FindingDrawer'; import type { DiscoverySession, Finding } from '@/lib/api'; import type { Issue } from '@/lib/api'; import type { FindingFilters } from '@/hooks/useIssues'; @@ -43,6 +44,7 @@ export function DiscoveryDetail({ const { formatMessage } = useIntl(); const [activeTab, setActiveTab] = useState('findings'); const [selectedIssue, setSelectedIssue] = useState(null); + const [selectedFinding, setSelectedFinding] = useState(null); const [selectedIds, setSelectedIds] = useState([]); const handleFindingClick = (finding: Finding) => { @@ -51,14 +53,21 @@ export function DiscoveryDetail({ const relatedIssue = issues.find(i => i.id === finding.issue_id); if (relatedIssue) { setSelectedIssue(relatedIssue); + return; } } + // Otherwise, show the finding details in FindingDrawer + setSelectedFinding(finding); }; - const handleCloseDrawer = () => { + const handleCloseIssueDrawer = () => { setSelectedIssue(null); }; + const handleCloseFindingDrawer = () => { + setSelectedFinding(null); + }; + const handleExportSelected = async () => { if (onExportSelected && selectedIds.length > 0) { await onExportSelected(selectedIds); @@ -130,7 +139,7 @@ export function DiscoveryDetail({ - {formatMessage({ id: `issues.discovery.status.${session.status}` })} + {formatMessage({ id: `issues.discovery.session.status.${session.status}` })} {formatMessage({ id: 'issues.discovery.createdAt' })}: {formatDate(session.created_at)} @@ -192,7 +201,7 @@ export function DiscoveryDetail({ - {formatMessage({ id: `issues.discovery.severity.${severity}` })} + {formatMessage({ id: `issues.discovery.findings.severity.${severity}` })} {count}
@@ -240,7 +249,7 @@ export function DiscoveryDetail({ - {formatMessage({ id: `issues.discovery.status.${session.status}` })} + {formatMessage({ id: `issues.discovery.session.status.${session.status}` })}
@@ -277,7 +286,14 @@ export function DiscoveryDetail({ + + {/* Finding Detail Drawer */} +
); diff --git a/ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx b/ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx new file mode 100644 index 00000000..3ac626e4 --- /dev/null +++ b/ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx @@ -0,0 +1,213 @@ +// ======================================== +// FindingDrawer Component +// ======================================== +// Right-side finding detail drawer for displaying discovery finding details + +import { useEffect } from 'react'; +import { useIntl } from 'react-intl'; +import { X, FileText, AlertTriangle, ExternalLink, MapPin, Code, Lightbulb, Target } from 'lucide-react'; +import { Badge } from '@/components/ui/Badge'; +import { Button } from '@/components/ui/Button'; +import { cn } from '@/lib/utils'; +import type { Finding } from '@/lib/api'; + +// ========== Types ========== +export interface FindingDrawerProps { + finding: Finding | null; + isOpen: boolean; + onClose: () => void; +} + +// ========== Severity Configuration ========== +const severityConfig: Record = { + critical: { label: 'issues.discovery.findings.severity.critical', variant: 'destructive' }, + high: { label: 'issues.discovery.findings.severity.high', variant: 'destructive' }, + medium: { label: 'issues.discovery.findings.severity.medium', variant: 'warning' }, + low: { label: 'issues.discovery.findings.severity.low', variant: 'secondary' }, +}; + +function getSeverityConfig(severity: string) { + return severityConfig[severity] || { label: 'issues.discovery.findings.severity.unknown', variant: 'outline' }; +} + +// ========== Component ========== + +export function FindingDrawer({ finding, isOpen, onClose }: FindingDrawerProps) { + const { formatMessage } = useIntl(); + + // ESC key to close + useEffect(() => { + if (!isOpen) return; + const handleEsc = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handleEsc); + return () => window.removeEventListener('keydown', handleEsc); + }, [isOpen, onClose]); + + if (!finding || !isOpen) { + return null; + } + + const severity = getSeverityConfig(finding.severity); + + return ( + <> + {/* Overlay */} +