use dioxus::prelude::*; use crate::components::page_header::PageHeader; use crate::components::pagination::Pagination; #[component] pub fn SbomPage() -> Element { let mut page = use_signal(|| 1u64); let sbom = use_resource(move || { let p = page(); async move { crate::infrastructure::sbom::fetch_sbom(p).await.ok() } }); rsx! { PageHeader { title: "SBOM", description: "Software Bill of Materials - dependency inventory across all repositories", } match &*sbom.read() { Some(Some(resp)) => { let total_pages = resp.total.unwrap_or(0).div_ceil(50).max(1); rsx! { div { class: "card", div { class: "table-wrapper", table { thead { tr { th { "Package" } th { "Version" } th { "Manager" } th { "License" } th { "Vulnerabilities" } } } tbody { for entry in &resp.data { tr { td { style: "font-weight: 500;", "{entry.name}" } td { style: "font-family: monospace; font-size: 13px;", "{entry.version}" } td { "{entry.package_manager}" } td { "{entry.license.as_deref().unwrap_or(\"-\")}" } td { if entry.known_vulnerabilities.is_empty() { span { style: "color: var(--success);", "None" } } else { span { class: "badge badge-high", "{entry.known_vulnerabilities.len()} vuln(s)" } } } } } } } } Pagination { current_page: page(), total_pages: total_pages, on_page_change: move |p| page.set(p), } } } }, Some(None) => rsx! { div { class: "card", p { "Failed to load SBOM." } } }, None => rsx! { div { class: "loading", "Loading SBOM..." } }, } } }