use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::*; use dioxus_free_icons::Icon; use crate::app::Route; use crate::components::page_header::PageHeader; use crate::components::stat_card::StatCard; use crate::infrastructure::mcp::fetch_mcp_servers; use crate::infrastructure::repositories::fetch_repositories; #[cfg(feature = "server")] use crate::infrastructure::stats::fetch_overview_stats; #[component] pub fn OverviewPage() -> Element { let stats = use_resource(move || async move { #[cfg(feature = "server")] { fetch_overview_stats().await.ok() } #[cfg(not(feature = "server"))] { crate::infrastructure::stats::fetch_overview_stats() .await .ok() } }); let repos = use_resource(|| async { fetch_repositories(1).await.ok() }); let mcp_servers = use_resource(|| async { fetch_mcp_servers().await.ok() }); rsx! { PageHeader { title: "Overview", description: "Security and compliance scanning dashboard", } match &*stats.read() { Some(Some(s)) => rsx! { div { class: "stat-cards", StatCard { label: "Repositories", value: s.total_repositories.to_string() } StatCard { label: "Total Findings", value: s.total_findings.to_string() } StatCard { label: "Critical", value: s.critical_findings.to_string(), color: "var(--danger)", } StatCard { label: "High", value: s.high_findings.to_string(), color: "#f97316", } StatCard { label: "Medium", value: s.medium_findings.to_string(), color: "var(--warning)", } StatCard { label: "Low", value: s.low_findings.to_string(), color: "var(--success)", } StatCard { label: "Dependencies", value: s.total_sbom_entries.to_string() } StatCard { label: "CVE Alerts", value: s.total_cve_alerts.to_string() } StatCard { label: "Tracker Issues", value: s.total_issues.to_string() } } div { class: "card", div { class: "card-header", "Severity Distribution" } div { class: "severity-chart", SeverityBar { label: "Critical", count: s.critical_findings, max: s.total_findings, color: "var(--danger)" } SeverityBar { label: "High", count: s.high_findings, max: s.total_findings, color: "var(--orange)" } SeverityBar { label: "Medium", count: s.medium_findings, max: s.total_findings, color: "var(--warning)" } SeverityBar { label: "Low", count: s.low_findings, max: s.total_findings, color: "var(--success)" } } } // AI Chat section div { class: "card", div { class: "card-header", "AI Chat" } match &*repos.read() { Some(Some(data)) => { let repo_list = &data.data; if repo_list.is_empty() { rsx! { p { style: "padding: 1rem; color: var(--text-secondary);", "No repositories found. Add a repository to start chatting." } } } else { rsx! { div { class: "grid", style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; padding: 1rem;", for repo in repo_list { { let repo_id = repo.id.map(|id| id.to_hex()).unwrap_or_default(); let name = repo.name.clone(); rsx! { Link { to: Route::ChatPage { repo_id }, class: "graph-repo-card", div { class: "graph-repo-card-header", div { class: "graph-repo-card-icon", Icon { icon: BsChatDots, width: 20, height: 20 } } h3 { class: "graph-repo-card-name", "{name}" } } } } } } } } } }, Some(None) => rsx! { p { style: "padding: 1rem; color: var(--text-secondary);", "Failed to load repositories." } }, None => rsx! { div { class: "loading", "Loading repositories..." } }, } } // MCP Servers section div { class: "card", div { class: "card-header", "MCP Servers" } match &*mcp_servers.read() { Some(Some(resp)) => { if resp.data.is_empty() { rsx! { p { style: "padding: 1rem; color: var(--text-secondary);", "No MCP servers registered." } } } else { rsx! { div { style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; padding: 1rem;", for server in resp.data.iter() { { let status_color = match server.status { compliance_core::models::McpServerStatus::Running => "var(--success)", compliance_core::models::McpServerStatus::Stopped => "var(--text-secondary)", compliance_core::models::McpServerStatus::Error => "var(--danger)", }; let status_label = format!("{}", server.status); let endpoint = server.endpoint_url.clone(); let name = server.name.clone(); rsx! { div { class: "card", style: "padding: 0.75rem;", div { style: "display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;", span { style: "width: 8px; height: 8px; border-radius: 50%; background: {status_color}; display: inline-block;", } strong { "{name}" } } p { style: "font-size: 0.8rem; color: var(--text-secondary); margin: 0; word-break: break-all;", "{endpoint}" } p { style: "font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.25rem;", "{status_label}" } } } } } } div { style: "padding: 0 1rem 1rem;", Link { to: Route::McpServersPage {}, class: "btn btn-primary btn-sm", "Manage" } } } } }, Some(None) => rsx! { p { style: "padding: 1rem; color: var(--text-secondary);", "Failed to load MCP servers." } }, None => rsx! { div { class: "loading", "Loading..." } }, } } }, Some(None) => rsx! { div { class: "card", p { style: "color: var(--text-secondary);", "Unable to load stats. Make sure the agent API is running." } } }, None => rsx! { div { class: "loading", "Loading overview..." } }, } } } #[component] fn SeverityBar(label: String, count: u64, max: u64, color: String) -> Element { let height_pct = if max > 0 { (count as f64 / max as f64) * 100.0 } else { 0.0 }; rsx! { div { class: "severity-bar", div { class: "severity-bar-count", "{count}" } div { class: "severity-bar-fill", style: "height: {height_pct.max(2.0)}%; background: {color};", } div { class: "severity-bar-label", "{label}" } } } }