mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 15:03:57 +08:00
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
This commit is contained in:
@@ -21,17 +21,17 @@ const statusConfig = {
|
|||||||
running: {
|
running: {
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
variant: 'warning' as const,
|
variant: 'warning' as const,
|
||||||
label: 'issues.discovery.status.running',
|
label: 'issues.discovery.session.status.running',
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
icon: CheckCircle,
|
icon: CheckCircle,
|
||||||
variant: 'success' as const,
|
variant: 'success' as const,
|
||||||
label: 'issues.discovery.status.completed',
|
label: 'issues.discovery.session.status.completed',
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
icon: XCircle,
|
icon: XCircle,
|
||||||
variant: 'destructive' as const,
|
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
|
|||||||
<div className="flex items-center justify-between text-sm">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-muted-foreground">{formatMessage({ id: 'issues.discovery.findings' })}:</span>
|
<span className="text-muted-foreground">{formatMessage({ id: 'issues.discovery.session.findings' }, { count: session.findings_count })}:</span>
|
||||||
<span className="font-medium text-foreground">{session.findings_count}</span>
|
<span className="font-medium text-foreground">{session.findings_count}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
|
|||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Progress } from '@/components/ui/Progress';
|
import { Progress } from '@/components/ui/Progress';
|
||||||
import { IssueDrawer } from '@/components/issue/hub/IssueDrawer';
|
import { IssueDrawer } from '@/components/issue/hub/IssueDrawer';
|
||||||
|
import { FindingDrawer } from './FindingDrawer';
|
||||||
import type { DiscoverySession, Finding } from '@/lib/api';
|
import type { DiscoverySession, Finding } from '@/lib/api';
|
||||||
import type { Issue } from '@/lib/api';
|
import type { Issue } from '@/lib/api';
|
||||||
import type { FindingFilters } from '@/hooks/useIssues';
|
import type { FindingFilters } from '@/hooks/useIssues';
|
||||||
@@ -43,6 +44,7 @@ export function DiscoveryDetail({
|
|||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [activeTab, setActiveTab] = useState('findings');
|
const [activeTab, setActiveTab] = useState('findings');
|
||||||
const [selectedIssue, setSelectedIssue] = useState<Issue | null>(null);
|
const [selectedIssue, setSelectedIssue] = useState<Issue | null>(null);
|
||||||
|
const [selectedFinding, setSelectedFinding] = useState<Finding | null>(null);
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
|
|
||||||
const handleFindingClick = (finding: Finding) => {
|
const handleFindingClick = (finding: Finding) => {
|
||||||
@@ -51,14 +53,21 @@ export function DiscoveryDetail({
|
|||||||
const relatedIssue = issues.find(i => i.id === finding.issue_id);
|
const relatedIssue = issues.find(i => i.id === finding.issue_id);
|
||||||
if (relatedIssue) {
|
if (relatedIssue) {
|
||||||
setSelectedIssue(relatedIssue);
|
setSelectedIssue(relatedIssue);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Otherwise, show the finding details in FindingDrawer
|
||||||
|
setSelectedFinding(finding);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCloseDrawer = () => {
|
const handleCloseIssueDrawer = () => {
|
||||||
setSelectedIssue(null);
|
setSelectedIssue(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseFindingDrawer = () => {
|
||||||
|
setSelectedFinding(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleExportSelected = async () => {
|
const handleExportSelected = async () => {
|
||||||
if (onExportSelected && selectedIds.length > 0) {
|
if (onExportSelected && selectedIds.length > 0) {
|
||||||
await onExportSelected(selectedIds);
|
await onExportSelected(selectedIds);
|
||||||
@@ -130,7 +139,7 @@ export function DiscoveryDetail({
|
|||||||
<Badge
|
<Badge
|
||||||
variant={session.status === 'completed' ? 'success' : session.status === 'failed' ? 'destructive' : 'warning'}
|
variant={session.status === 'completed' ? 'success' : session.status === 'failed' ? 'destructive' : 'warning'}
|
||||||
>
|
>
|
||||||
{formatMessage({ id: `issues.discovery.status.${session.status}` })}
|
{formatMessage({ id: `issues.discovery.session.status.${session.status}` })}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{formatMessage({ id: 'issues.discovery.createdAt' })}: {formatDate(session.created_at)}
|
{formatMessage({ id: 'issues.discovery.createdAt' })}: {formatDate(session.created_at)}
|
||||||
@@ -192,7 +201,7 @@ export function DiscoveryDetail({
|
|||||||
<Badge
|
<Badge
|
||||||
variant={severity === 'critical' || severity === 'high' ? 'destructive' : severity === 'medium' ? 'warning' : 'secondary'}
|
variant={severity === 'critical' || severity === 'high' ? 'destructive' : severity === 'medium' ? 'warning' : 'secondary'}
|
||||||
>
|
>
|
||||||
{formatMessage({ id: `issues.discovery.severity.${severity}` })}
|
{formatMessage({ id: `issues.discovery.findings.severity.${severity}` })}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="font-medium">{count}</span>
|
<span className="font-medium">{count}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -240,7 +249,7 @@ export function DiscoveryDetail({
|
|||||||
<Badge
|
<Badge
|
||||||
variant={session.status === 'completed' ? 'success' : session.status === 'failed' ? 'destructive' : 'warning'}
|
variant={session.status === 'completed' ? 'success' : session.status === 'failed' ? 'destructive' : 'warning'}
|
||||||
>
|
>
|
||||||
{formatMessage({ id: `issues.discovery.status.${session.status}` })}
|
{formatMessage({ id: `issues.discovery.session.status.${session.status}` })}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -277,7 +286,14 @@ export function DiscoveryDetail({
|
|||||||
<IssueDrawer
|
<IssueDrawer
|
||||||
issue={selectedIssue}
|
issue={selectedIssue}
|
||||||
isOpen={selectedIssue !== null}
|
isOpen={selectedIssue !== null}
|
||||||
onClose={handleCloseDrawer}
|
onClose={handleCloseIssueDrawer}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Finding Detail Drawer */}
|
||||||
|
<FindingDrawer
|
||||||
|
finding={selectedFinding}
|
||||||
|
isOpen={selectedFinding !== null}
|
||||||
|
onClose={handleCloseFindingDrawer}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
213
ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx
Normal file
213
ccw/frontend/src/components/issue/discovery/FindingDrawer.tsx
Normal file
@@ -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<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info' }> = {
|
||||||
|
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 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 bg-black/40 transition-opacity z-40',
|
||||||
|
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||||
|
)}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Drawer */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'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',
|
||||||
|
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||||
|
)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
style={{ minWidth: '400px', maxWidth: '800px' }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between p-6 border-b border-border bg-card">
|
||||||
|
<div className="flex-1 min-w-0 mr-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
|
<span className="text-xs font-mono text-muted-foreground">{finding.id}</span>
|
||||||
|
<Badge variant={severity.variant}>
|
||||||
|
{formatMessage({ id: severity.label })}
|
||||||
|
</Badge>
|
||||||
|
{finding.type && (
|
||||||
|
<Badge variant="outline">{finding.type}</Badge>
|
||||||
|
)}
|
||||||
|
{finding.category && (
|
||||||
|
<Badge variant="info">{finding.category}</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
|
{finding.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" onClick={onClose} className="flex-shrink-0 hover:bg-secondary">
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||||
|
<AlertTriangle className="h-4 w-4" />
|
||||||
|
{formatMessage({ id: 'issues.discovery.findings.description' })}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
|
||||||
|
{finding.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* File Location */}
|
||||||
|
{finding.file && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||||
|
<MapPin className="h-4 w-4" />
|
||||||
|
{formatMessage({ id: 'issues.discovery.findings.location' })}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2 text-sm">
|
||||||
|
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<code className="px-2 py-1 bg-muted rounded text-xs">
|
||||||
|
{finding.file}
|
||||||
|
{finding.line && `:${finding.line}`}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Code Snippet */}
|
||||||
|
{finding.code_snippet && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||||
|
<Code className="h-4 w-4" />
|
||||||
|
{formatMessage({ id: 'issues.discovery.findings.codeSnippet' })}
|
||||||
|
</h3>
|
||||||
|
<pre className="p-3 bg-muted rounded-md overflow-x-auto text-xs border border-border">
|
||||||
|
<code>{finding.code_snippet}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Suggested Fix */}
|
||||||
|
{finding.suggested_issue && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||||
|
<Lightbulb className="h-4 w-4" />
|
||||||
|
{formatMessage({ id: 'issues.discovery.findings.suggestedFix' })}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
|
||||||
|
{finding.suggested_issue}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Confidence */}
|
||||||
|
{finding.confidence !== undefined && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||||
|
<Target className="h-4 w-4" />
|
||||||
|
{formatMessage({ id: 'issues.discovery.findings.confidence' })}
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-full transition-all",
|
||||||
|
finding.confidence >= 0.9 ? "bg-green-500" :
|
||||||
|
finding.confidence >= 0.7 ? "bg-yellow-500" : "bg-red-500"
|
||||||
|
)}
|
||||||
|
style={{ width: `${finding.confidence * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{Math.round(finding.confidence * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Reference */}
|
||||||
|
{finding.reference && (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
{formatMessage({ id: 'issues.discovery.findings.reference' })}
|
||||||
|
</h3>
|
||||||
|
<a
|
||||||
|
href={finding.reference}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-primary hover:underline break-all"
|
||||||
|
>
|
||||||
|
{finding.reference}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Perspective */}
|
||||||
|
{finding.perspective && (
|
||||||
|
<div className="pt-4 border-t border-border">
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{formatMessage({ id: 'issues.discovery.findings.perspective' })}: {finding.perspective}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FindingDrawer;
|
||||||
@@ -24,14 +24,14 @@ interface FindingListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const severityConfig: Record<string, { variant: 'destructive' | 'warning' | 'secondary' | 'outline' | 'success' | 'info' | 'default'; label: string }> = {
|
const severityConfig: Record<string, { variant: 'destructive' | 'warning' | 'secondary' | 'outline' | 'success' | 'info' | 'default'; label: string }> = {
|
||||||
critical: { variant: 'destructive', label: 'issues.discovery.severity.critical' },
|
critical: { variant: 'destructive', label: 'issues.discovery.findings.severity.critical' },
|
||||||
high: { variant: 'destructive', label: 'issues.discovery.severity.high' },
|
high: { variant: 'destructive', label: 'issues.discovery.findings.severity.high' },
|
||||||
medium: { variant: 'warning', label: 'issues.discovery.severity.medium' },
|
medium: { variant: 'warning', label: 'issues.discovery.findings.severity.medium' },
|
||||||
low: { variant: 'secondary', label: 'issues.discovery.severity.low' },
|
low: { variant: 'secondary', label: 'issues.discovery.findings.severity.low' },
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSeverityConfig(severity: string) {
|
function getSeverityConfig(severity: string) {
|
||||||
return severityConfig[severity] || { variant: 'outline', label: 'issues.discovery.severity.unknown' };
|
return severityConfig[severity] || { variant: 'outline', label: 'issues.discovery.findings.severity.unknown' };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FindingList({
|
export function FindingList({
|
||||||
@@ -116,10 +116,10 @@ export function FindingList({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">{formatMessage({ id: 'issues.discovery.findings.severity.all' })}</SelectItem>
|
<SelectItem value="all">{formatMessage({ id: 'issues.discovery.findings.severity.all' })}</SelectItem>
|
||||||
<SelectItem value="critical">{formatMessage({ id: 'issues.discovery.severity.critical' })}</SelectItem>
|
<SelectItem value="critical">{formatMessage({ id: 'issues.discovery.findings.severity.critical' })}</SelectItem>
|
||||||
<SelectItem value="high">{formatMessage({ id: 'issues.discovery.severity.high' })}</SelectItem>
|
<SelectItem value="high">{formatMessage({ id: 'issues.discovery.findings.severity.high' })}</SelectItem>
|
||||||
<SelectItem value="medium">{formatMessage({ id: 'issues.discovery.severity.medium' })}</SelectItem>
|
<SelectItem value="medium">{formatMessage({ id: 'issues.discovery.findings.severity.medium' })}</SelectItem>
|
||||||
<SelectItem value="low">{formatMessage({ id: 'issues.discovery.severity.low' })}</SelectItem>
|
<SelectItem value="low">{formatMessage({ id: 'issues.discovery.findings.severity.low' })}</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{uniqueTypes.length > 0 && (
|
{uniqueTypes.length > 0 && (
|
||||||
|
|||||||
@@ -1077,6 +1077,12 @@ export interface Finding {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
issue_id?: string; // Associated issue ID if exported
|
issue_id?: string; // Associated issue ID if exported
|
||||||
exported?: boolean; // Whether this finding has been exported as an issue
|
exported?: boolean; // Whether this finding has been exported as an issue
|
||||||
|
// Additional fields from discovery backend
|
||||||
|
category?: string;
|
||||||
|
suggested_issue?: string;
|
||||||
|
confidence?: number;
|
||||||
|
reference?: string;
|
||||||
|
perspective?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDiscoveries(projectPath?: string): Promise<DiscoverySession[]> {
|
export async function fetchDiscoveries(projectPath?: string): Promise<DiscoverySession[]> {
|
||||||
@@ -1131,7 +1137,11 @@ export async function fetchDiscoveryFindings(
|
|||||||
? `/api/discoveries/${encodeURIComponent(sessionId)}/findings?path=${encodeURIComponent(projectPath)}`
|
? `/api/discoveries/${encodeURIComponent(sessionId)}/findings?path=${encodeURIComponent(projectPath)}`
|
||||||
: `/api/discoveries/${encodeURIComponent(sessionId)}/findings`;
|
: `/api/discoveries/${encodeURIComponent(sessionId)}/findings`;
|
||||||
const data = await fetchApi<{ findings?: Finding[] }>(url);
|
const data = await fetchApi<{ findings?: Finding[] }>(url);
|
||||||
return data.findings ?? [];
|
// Map backend 'priority' to frontend 'severity' for compatibility
|
||||||
|
return (data.findings ?? []).map(f => ({
|
||||||
|
...f,
|
||||||
|
severity: f.severity || (f as any).priority || 'medium'
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -353,7 +353,14 @@
|
|||||||
"hasIssue": "Linked",
|
"hasIssue": "Linked",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"selectAll": "Select All",
|
"selectAll": "Select All",
|
||||||
"deselectAll": "Deselect All"
|
"deselectAll": "Deselect All",
|
||||||
|
"description": "Description",
|
||||||
|
"location": "Location",
|
||||||
|
"codeSnippet": "Code Snippet",
|
||||||
|
"suggestedFix": "Suggested Fix",
|
||||||
|
"confidence": "Confidence",
|
||||||
|
"reference": "Reference",
|
||||||
|
"perspective": "Perspective"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"findings": "Findings",
|
"findings": "Findings",
|
||||||
|
|||||||
@@ -353,7 +353,14 @@
|
|||||||
"hasIssue": "已关联",
|
"hasIssue": "已关联",
|
||||||
"export": "导出",
|
"export": "导出",
|
||||||
"selectAll": "全选",
|
"selectAll": "全选",
|
||||||
"deselectAll": "取消全选"
|
"deselectAll": "取消全选",
|
||||||
|
"description": "问题描述",
|
||||||
|
"location": "文件位置",
|
||||||
|
"codeSnippet": "代码片段",
|
||||||
|
"suggestedFix": "建议修复",
|
||||||
|
"confidence": "置信度",
|
||||||
|
"reference": "参考链接",
|
||||||
|
"perspective": "视角"
|
||||||
},
|
},
|
||||||
"tabs": {
|
"tabs": {
|
||||||
"findings": "发现",
|
"findings": "发现",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useIntl } from 'react-intl';
|
|||||||
import { Radar, AlertCircle, Loader2 } from 'lucide-react';
|
import { Radar, AlertCircle, Loader2 } from 'lucide-react';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { useIssueDiscovery } from '@/hooks/useIssues';
|
import { useIssueDiscovery, useIssues } from '@/hooks/useIssues';
|
||||||
import { DiscoveryCard } from '@/components/issue/discovery/DiscoveryCard';
|
import { DiscoveryCard } from '@/components/issue/discovery/DiscoveryCard';
|
||||||
import { DiscoveryDetail } from '@/components/issue/discovery/DiscoveryDetail';
|
import { DiscoveryDetail } from '@/components/issue/discovery/DiscoveryDetail';
|
||||||
|
|
||||||
@@ -29,6 +29,12 @@ export function DiscoveryPage() {
|
|||||||
isExporting,
|
isExporting,
|
||||||
} = useIssueDiscovery({ refetchInterval: 3000 });
|
} = useIssueDiscovery({ refetchInterval: 3000 });
|
||||||
|
|
||||||
|
// Fetch issues to find related ones when clicking findings
|
||||||
|
const { issues } = useIssues({
|
||||||
|
// Don't apply filters to get all issues for matching
|
||||||
|
filter: undefined
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -167,6 +173,7 @@ export function DiscoveryPage() {
|
|||||||
onExport={exportFindings}
|
onExport={exportFindings}
|
||||||
onExportSelected={exportSelectedFindings}
|
onExportSelected={exportSelectedFindings}
|
||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
|
issues={issues}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -262,7 +262,13 @@ function flattenFindings(perspectiveResults: any[]): any[] {
|
|||||||
const allFindings: any[] = [];
|
const allFindings: any[] = [];
|
||||||
for (const result of perspectiveResults) {
|
for (const result of perspectiveResults) {
|
||||||
if (result.findings) {
|
if (result.findings) {
|
||||||
allFindings.push(...result.findings);
|
// Map backend 'priority' to frontend 'severity' for compatibility
|
||||||
|
const mappedFindings = result.findings.map((f: any) => ({
|
||||||
|
...f,
|
||||||
|
severity: f.severity || f.priority || 'medium',
|
||||||
|
sessionId: f.discovery_id || result.discovery_id
|
||||||
|
}));
|
||||||
|
allFindings.push(...mappedFindings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allFindings;
|
return allFindings;
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ export const schema: ToolSchema = {
|
|||||||
name: 'write_file',
|
name: 'write_file',
|
||||||
description: `Write content to file. Auto-creates parent directories.
|
description: `Write content to file. Auto-creates parent directories.
|
||||||
|
|
||||||
Usage: write_file(path="file.js", content="code here")
|
Required: path (string), content (string)
|
||||||
Options: backup=true (backup before overwrite), createDirectories=false (disable auto-creation), encoding="utf8"`,
|
Options: backup=true, createDirectories=false, encoding="utf8"`,
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|||||||
Reference in New Issue
Block a user