use dioxus::prelude::*; use crate::components::code_snippet::CodeSnippet; use crate::components::page_header::PageHeader; use crate::components::severity_badge::SeverityBadge; #[component] pub fn FindingDetailPage(id: String) -> Element { let finding_id = id.clone(); let mut finding = use_resource(move || { let fid = finding_id.clone(); async move { crate::infrastructure::findings::fetch_finding_detail(fid) .await .ok() } }); let snapshot = finding.read().clone(); match snapshot { Some(Some(f)) => { let finding_id_for_status = id.clone(); let finding_id_for_feedback = id.clone(); let existing_feedback = f.developer_feedback.clone().unwrap_or_default(); rsx! { PageHeader { title: f.title.clone(), description: format!("{} | {} | {}", f.scanner, f.scan_type, f.status), } div { class: "flex gap-2 mb-4", SeverityBadge { severity: f.severity.to_string() } if let Some(cwe) = &f.cwe { span { class: "badge badge-info", "{cwe}" } } if let Some(cve) = &f.cve { span { class: "badge badge-high", "{cve}" } } if let Some(score) = f.cvss_score { span { class: "badge badge-medium", "CVSS: {score}" } } if let Some(confidence) = f.confidence { span { class: "badge badge-info", "Confidence: {confidence:.1}" } } } div { class: "card", div { class: "card-header", "Description" } p { "{f.description}" } } if let Some(rationale) = &f.triage_rationale { div { class: "card", div { class: "card-header", "Triage Rationale" } div { style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px;", if let Some(action) = &f.triage_action { span { class: "badge badge-info", "{action}" } } } p { style: "color: var(--text-secondary); font-size: 14px;", "{rationale}" } } } if let Some(code) = &f.code_snippet { div { class: "card", div { class: "card-header", "Code Evidence" } CodeSnippet { code: code.clone(), file_path: f.file_path.clone().unwrap_or_default(), line_number: f.line_number.unwrap_or(0), } } } if let Some(remediation) = &f.remediation { div { class: "card", div { class: "card-header", "Remediation" } p { "{remediation}" } } } if let Some(fix) = &f.suggested_fix { div { class: "card", div { class: "card-header", "Suggested Fix" } CodeSnippet { code: fix.clone() } } } if let Some(url) = &f.tracker_issue_url { div { class: "card", div { class: "card-header", "Linked Issue" } a { href: "{url}", target: "_blank", style: "color: var(--accent);", "{url}" } } } div { class: "card", div { class: "card-header", "Update Status" } div { class: "flex gap-2", for status in ["open", "triaged", "resolved", "false_positive", "ignored"] { { let status_str = status.to_string(); let id_clone = finding_id_for_status.clone(); rsx! { button { class: "btn btn-ghost", onclick: move |_| { let s = status_str.clone(); let id = id_clone.clone(); spawn(async move { let _ = crate::infrastructure::findings::update_finding_status(id, s).await; }); finding.restart(); }, "{status}" } } } } } } div { class: "card", div { class: "card-header", "Developer Feedback" } p { style: "font-size: 13px; color: var(--text-secondary); margin-bottom: 8px;", "Share your assessment of this finding (e.g. false positive, actionable, needs context)" } textarea { style: "width: 100%; min-height: 80px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; color: var(--text-primary); font-size: 14px; resize: vertical;", value: "{existing_feedback}", oninput: move |e| { let feedback = e.value(); let id = finding_id_for_feedback.clone(); spawn(async move { let _ = crate::infrastructure::findings::update_finding_feedback(id, feedback).await; }); }, } } } } Some(None) => rsx! { div { class: "card", p { "Finding not found." } } }, None => rsx! { div { class: "loading", "Loading finding..." } }, } }