Skip to content

Commit cd54c10

Browse files
author
catlog22
committed
feat(discovery): add FindingDrawer component and restructure i18n keys
- 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
1 parent c3ddf7e commit cd54c10

10 files changed

Lines changed: 291 additions & 25 deletions

File tree

ccw/frontend/src/components/issue/discovery/DiscoveryCard.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ const statusConfig = {
2121
running: {
2222
icon: Clock,
2323
variant: 'warning' as const,
24-
label: 'issues.discovery.status.running',
24+
label: 'issues.discovery.session.status.running',
2525
},
2626
completed: {
2727
icon: CheckCircle,
2828
variant: 'success' as const,
29-
label: 'issues.discovery.status.completed',
29+
label: 'issues.discovery.session.status.completed',
3030
},
3131
failed: {
3232
icon: XCircle,
3333
variant: 'destructive' as const,
34-
label: 'issues.discovery.status.failed',
34+
label: 'issues.discovery.session.status.failed',
3535
},
3636
};
3737

@@ -79,7 +79,7 @@ export function DiscoveryCard({ session, isActive, onClick }: DiscoveryCardProps
7979
<div className="flex items-center justify-between text-sm">
8080
<div className="flex items-center gap-4">
8181
<div className="flex items-center gap-1">
82-
<span className="text-muted-foreground">{formatMessage({ id: 'issues.discovery.findings' })}:</span>
82+
<span className="text-muted-foreground">{formatMessage({ id: 'issues.discovery.session.findings' }, { count: session.findings_count })}:</span>
8383
<span className="font-medium text-foreground">{session.findings_count}</span>
8484
</div>
8585
</div>

ccw/frontend/src/components/issue/discovery/DiscoveryDetail.tsx

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
1212
import { Badge } from '@/components/ui/Badge';
1313
import { Progress } from '@/components/ui/Progress';
1414
import { IssueDrawer } from '@/components/issue/hub/IssueDrawer';
15+
import { FindingDrawer } from './FindingDrawer';
1516
import type { DiscoverySession, Finding } from '@/lib/api';
1617
import type { Issue } from '@/lib/api';
1718
import type { FindingFilters } from '@/hooks/useIssues';
@@ -43,6 +44,7 @@ export function DiscoveryDetail({
4344
const { formatMessage } = useIntl();
4445
const [activeTab, setActiveTab] = useState('findings');
4546
const [selectedIssue, setSelectedIssue] = useState<Issue | null>(null);
47+
const [selectedFinding, setSelectedFinding] = useState<Finding | null>(null);
4648
const [selectedIds, setSelectedIds] = useState<string[]>([]);
4749

4850
const handleFindingClick = (finding: Finding) => {
@@ -51,14 +53,21 @@ export function DiscoveryDetail({
5153
const relatedIssue = issues.find(i => i.id === finding.issue_id);
5254
if (relatedIssue) {
5355
setSelectedIssue(relatedIssue);
56+
return;
5457
}
5558
}
59+
// Otherwise, show the finding details in FindingDrawer
60+
setSelectedFinding(finding);
5661
};
5762

58-
const handleCloseDrawer = () => {
63+
const handleCloseIssueDrawer = () => {
5964
setSelectedIssue(null);
6065
};
6166

67+
const handleCloseFindingDrawer = () => {
68+
setSelectedFinding(null);
69+
};
70+
6271
const handleExportSelected = async () => {
6372
if (onExportSelected && selectedIds.length > 0) {
6473
await onExportSelected(selectedIds);
@@ -130,7 +139,7 @@ export function DiscoveryDetail({
130139
<Badge
131140
variant={session.status === 'completed' ? 'success' : session.status === 'failed' ? 'destructive' : 'warning'}
132141
>
133-
{formatMessage({ id: `issues.discovery.status.${session.status}` })}
142+
{formatMessage({ id: `issues.discovery.session.status.${session.status}` })}
134143
</Badge>
135144
<span className="text-sm text-muted-foreground">
136145
{formatMessage({ id: 'issues.discovery.createdAt' })}: {formatDate(session.created_at)}
@@ -192,7 +201,7 @@ export function DiscoveryDetail({
192201
<Badge
193202
variant={severity === 'critical' || severity === 'high' ? 'destructive' : severity === 'medium' ? 'warning' : 'secondary'}
194203
>
195-
{formatMessage({ id: `issues.discovery.severity.${severity}` })}
204+
{formatMessage({ id: `issues.discovery.findings.severity.${severity}` })}
196205
</Badge>
197206
<span className="font-medium">{count}</span>
198207
</div>
@@ -240,7 +249,7 @@ export function DiscoveryDetail({
240249
<Badge
241250
variant={session.status === 'completed' ? 'success' : session.status === 'failed' ? 'destructive' : 'warning'}
242251
>
243-
{formatMessage({ id: `issues.discovery.status.${session.status}` })}
252+
{formatMessage({ id: `issues.discovery.session.status.${session.status}` })}
244253
</Badge>
245254
</div>
246255
<div>
@@ -277,7 +286,14 @@ export function DiscoveryDetail({
277286
<IssueDrawer
278287
issue={selectedIssue}
279288
isOpen={selectedIssue !== null}
280-
onClose={handleCloseDrawer}
289+
onClose={handleCloseIssueDrawer}
290+
/>
291+
292+
{/* Finding Detail Drawer */}
293+
<FindingDrawer
294+
finding={selectedFinding}
295+
isOpen={selectedFinding !== null}
296+
onClose={handleCloseFindingDrawer}
281297
/>
282298
</div>
283299
);
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// ========================================
2+
// FindingDrawer Component
3+
// ========================================
4+
// Right-side finding detail drawer for displaying discovery finding details
5+
6+
import { useEffect } from 'react';
7+
import { useIntl } from 'react-intl';
8+
import { X, FileText, AlertTriangle, ExternalLink, MapPin, Code, Lightbulb, Target } from 'lucide-react';
9+
import { Badge } from '@/components/ui/Badge';
10+
import { Button } from '@/components/ui/Button';
11+
import { cn } from '@/lib/utils';
12+
import type { Finding } from '@/lib/api';
13+
14+
// ========== Types ==========
15+
export interface FindingDrawerProps {
16+
finding: Finding | null;
17+
isOpen: boolean;
18+
onClose: () => void;
19+
}
20+
21+
// ========== Severity Configuration ==========
22+
const severityConfig: Record<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info' }> = {
23+
critical: { label: 'issues.discovery.findings.severity.critical', variant: 'destructive' },
24+
high: { label: 'issues.discovery.findings.severity.high', variant: 'destructive' },
25+
medium: { label: 'issues.discovery.findings.severity.medium', variant: 'warning' },
26+
low: { label: 'issues.discovery.findings.severity.low', variant: 'secondary' },
27+
};
28+
29+
function getSeverityConfig(severity: string) {
30+
return severityConfig[severity] || { label: 'issues.discovery.findings.severity.unknown', variant: 'outline' };
31+
}
32+
33+
// ========== Component ==========
34+
35+
export function FindingDrawer({ finding, isOpen, onClose }: FindingDrawerProps) {
36+
const { formatMessage } = useIntl();
37+
38+
// ESC key to close
39+
useEffect(() => {
40+
if (!isOpen) return;
41+
const handleEsc = (e: KeyboardEvent) => {
42+
if (e.key === 'Escape') onClose();
43+
};
44+
window.addEventListener('keydown', handleEsc);
45+
return () => window.removeEventListener('keydown', handleEsc);
46+
}, [isOpen, onClose]);
47+
48+
if (!finding || !isOpen) {
49+
return null;
50+
}
51+
52+
const severity = getSeverityConfig(finding.severity);
53+
54+
return (
55+
<>
56+
{/* Overlay */}
57+
<div
58+
className={cn(
59+
'fixed inset-0 bg-black/40 transition-opacity z-40',
60+
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
61+
)}
62+
onClick={onClose}
63+
aria-hidden="true"
64+
/>
65+
66+
{/* Drawer */}
67+
<div
68+
className={cn(
69+
'fixed top-0 right-0 h-full w-1/2 bg-background border-l border-border shadow-2xl z-50 flex flex-col transition-transform duration-300 ease-in-out',
70+
isOpen ? 'translate-x-0' : 'translate-x-full'
71+
)}
72+
role="dialog"
73+
aria-modal="true"
74+
style={{ minWidth: '400px', maxWidth: '800px' }}
75+
>
76+
{/* Header */}
77+
<div className="flex items-start justify-between p-6 border-b border-border bg-card">
78+
<div className="flex-1 min-w-0 mr-4">
79+
<div className="flex items-center gap-2 mb-2 flex-wrap">
80+
<span className="text-xs font-mono text-muted-foreground">{finding.id}</span>
81+
<Badge variant={severity.variant}>
82+
{formatMessage({ id: severity.label })}
83+
</Badge>
84+
{finding.type && (
85+
<Badge variant="outline">{finding.type}</Badge>
86+
)}
87+
{finding.category && (
88+
<Badge variant="info">{finding.category}</Badge>
89+
)}
90+
</div>
91+
<h2 className="text-lg font-semibold text-foreground">
92+
{finding.title}
93+
</h2>
94+
</div>
95+
<Button variant="ghost" size="icon" onClick={onClose} className="flex-shrink-0 hover:bg-secondary">
96+
<X className="h-5 w-5" />
97+
</Button>
98+
</div>
99+
100+
{/* Content */}
101+
<div className="flex-1 overflow-y-auto p-6 space-y-6">
102+
{/* Description */}
103+
<div>
104+
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
105+
<AlertTriangle className="h-4 w-4" />
106+
{formatMessage({ id: 'issues.discovery.findings.description' })}
107+
</h3>
108+
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
109+
{finding.description}
110+
</p>
111+
</div>
112+
113+
{/* File Location */}
114+
{finding.file && (
115+
<div>
116+
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
117+
<MapPin className="h-4 w-4" />
118+
{formatMessage({ id: 'issues.discovery.findings.location' })}
119+
</h3>
120+
<div className="flex items-center gap-2 text-sm">
121+
<FileText className="h-4 w-4 text-muted-foreground" />
122+
<code className="px-2 py-1 bg-muted rounded text-xs">
123+
{finding.file}
124+
{finding.line && `:${finding.line}`}
125+
</code>
126+
</div>
127+
</div>
128+
)}
129+
130+
{/* Code Snippet */}
131+
{finding.code_snippet && (
132+
<div>
133+
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
134+
<Code className="h-4 w-4" />
135+
{formatMessage({ id: 'issues.discovery.findings.codeSnippet' })}
136+
</h3>
137+
<pre className="p-3 bg-muted rounded-md overflow-x-auto text-xs border border-border">
138+
<code>{finding.code_snippet}</code>
139+
</pre>
140+
</div>
141+
)}
142+
143+
{/* Suggested Fix */}
144+
{finding.suggested_issue && (
145+
<div>
146+
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
147+
<Lightbulb className="h-4 w-4" />
148+
{formatMessage({ id: 'issues.discovery.findings.suggestedFix' })}
149+
</h3>
150+
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
151+
{finding.suggested_issue}
152+
</p>
153+
</div>
154+
)}
155+
156+
{/* Confidence */}
157+
{finding.confidence !== undefined && (
158+
<div>
159+
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
160+
<Target className="h-4 w-4" />
161+
{formatMessage({ id: 'issues.discovery.findings.confidence' })}
162+
</h3>
163+
<div className="flex items-center gap-2">
164+
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
165+
<div
166+
className={cn(
167+
"h-full transition-all",
168+
finding.confidence >= 0.9 ? "bg-green-500" :
169+
finding.confidence >= 0.7 ? "bg-yellow-500" : "bg-red-500"
170+
)}
171+
style={{ width: `${finding.confidence * 100}%` }}
172+
/>
173+
</div>
174+
<span className="text-sm font-medium">
175+
{Math.round(finding.confidence * 100)}%
176+
</span>
177+
</div>
178+
</div>
179+
)}
180+
181+
{/* Reference */}
182+
{finding.reference && (
183+
<div>
184+
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
185+
<ExternalLink className="h-4 w-4" />
186+
{formatMessage({ id: 'issues.discovery.findings.reference' })}
187+
</h3>
188+
<a
189+
href={finding.reference}
190+
target="_blank"
191+
rel="noopener noreferrer"
192+
className="text-sm text-primary hover:underline break-all"
193+
>
194+
{finding.reference}
195+
</a>
196+
</div>
197+
)}
198+
199+
{/* Perspective */}
200+
{finding.perspective && (
201+
<div className="pt-4 border-t border-border">
202+
<Badge variant="secondary" className="text-xs">
203+
{formatMessage({ id: 'issues.discovery.findings.perspective' })}: {finding.perspective}
204+
</Badge>
205+
</div>
206+
)}
207+
</div>
208+
</div>
209+
</>
210+
);
211+
}
212+
213+
export default FindingDrawer;

ccw/frontend/src/components/issue/discovery/FindingList.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ interface FindingListProps {
2424
}
2525

2626
const severityConfig: Record<string, { variant: 'destructive' | 'warning' | 'secondary' | 'outline' | 'success' | 'info' | 'default'; label: string }> = {
27-
critical: { variant: 'destructive', label: 'issues.discovery.severity.critical' },
28-
high: { variant: 'destructive', label: 'issues.discovery.severity.high' },
29-
medium: { variant: 'warning', label: 'issues.discovery.severity.medium' },
30-
low: { variant: 'secondary', label: 'issues.discovery.severity.low' },
27+
critical: { variant: 'destructive', label: 'issues.discovery.findings.severity.critical' },
28+
high: { variant: 'destructive', label: 'issues.discovery.findings.severity.high' },
29+
medium: { variant: 'warning', label: 'issues.discovery.findings.severity.medium' },
30+
low: { variant: 'secondary', label: 'issues.discovery.findings.severity.low' },
3131
};
3232

3333
function getSeverityConfig(severity: string) {
34-
return severityConfig[severity] || { variant: 'outline', label: 'issues.discovery.severity.unknown' };
34+
return severityConfig[severity] || { variant: 'outline', label: 'issues.discovery.findings.severity.unknown' };
3535
}
3636

3737
export function FindingList({
@@ -116,10 +116,10 @@ export function FindingList({
116116
</SelectTrigger>
117117
<SelectContent>
118118
<SelectItem value="all">{formatMessage({ id: 'issues.discovery.findings.severity.all' })}</SelectItem>
119-
<SelectItem value="critical">{formatMessage({ id: 'issues.discovery.severity.critical' })}</SelectItem>
120-
<SelectItem value="high">{formatMessage({ id: 'issues.discovery.severity.high' })}</SelectItem>
121-
<SelectItem value="medium">{formatMessage({ id: 'issues.discovery.severity.medium' })}</SelectItem>
122-
<SelectItem value="low">{formatMessage({ id: 'issues.discovery.severity.low' })}</SelectItem>
119+
<SelectItem value="critical">{formatMessage({ id: 'issues.discovery.findings.severity.critical' })}</SelectItem>
120+
<SelectItem value="high">{formatMessage({ id: 'issues.discovery.findings.severity.high' })}</SelectItem>
121+
<SelectItem value="medium">{formatMessage({ id: 'issues.discovery.findings.severity.medium' })}</SelectItem>
122+
<SelectItem value="low">{formatMessage({ id: 'issues.discovery.findings.severity.low' })}</SelectItem>
123123
</SelectContent>
124124
</Select>
125125
{uniqueTypes.length > 0 && (

ccw/frontend/src/lib/api.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,12 @@ export interface Finding {
10771077
created_at: string;
10781078
issue_id?: string; // Associated issue ID if exported
10791079
exported?: boolean; // Whether this finding has been exported as an issue
1080+
// Additional fields from discovery backend
1081+
category?: string;
1082+
suggested_issue?: string;
1083+
confidence?: number;
1084+
reference?: string;
1085+
perspective?: string;
10801086
}
10811087

10821088
export async function fetchDiscoveries(projectPath?: string): Promise<DiscoverySession[]> {
@@ -1131,7 +1137,11 @@ export async function fetchDiscoveryFindings(
11311137
? `/api/discoveries/${encodeURIComponent(sessionId)}/findings?path=${encodeURIComponent(projectPath)}`
11321138
: `/api/discoveries/${encodeURIComponent(sessionId)}/findings`;
11331139
const data = await fetchApi<{ findings?: Finding[] }>(url);
1134-
return data.findings ?? [];
1140+
// Map backend 'priority' to frontend 'severity' for compatibility
1141+
return (data.findings ?? []).map(f => ({
1142+
...f,
1143+
severity: f.severity || (f as any).priority || 'medium'
1144+
}));
11351145
}
11361146

11371147
/**

0 commit comments

Comments
 (0)