use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::*; use dioxus_free_icons::Icon; 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! { div { class: "back-nav", button { class: "btn btn-ghost btn-back", onclick: move |_| { navigator().go_back(); }, Icon { icon: BsArrowLeft, width: 16, height: 16 } "Back" } } 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(); let label = match status { "open" => "Open", "triaged" => "Triaged", "resolved" => "Resolved", "false_positive" => "False Positive", "ignored" => "Ignored", _ => status, }; rsx! { button { class: "btn btn-ghost", title: "{label}", 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(); }, match status { "open" => rsx! { Icon { icon: BsCircle, width: 14, height: 14 } }, "triaged" => rsx! { Icon { icon: BsEye, width: 14, height: 14 } }, "resolved" => rsx! { Icon { icon: BsCheckCircle, width: 14, height: 14 } }, "false_positive" => rsx! { Icon { icon: BsXCircle, width: 14, height: 14 } }, "ignored" => rsx! { Icon { icon: BsDashCircle, width: 14, height: 14 } }, _ => rsx! {}, } " {label}" } } } } } } 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..." } }, } }