Files
compliance-scanner-agent/compliance-dashboard/src/pages/overview.rs
T
sharang 0065c7c4b2
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 3m59s
CI / Security Audit (push) Successful in 1m44s
CI / Tests (push) Successful in 5m2s
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
feat: UI improvements with icons, back navigation, and overview cards (#7)
2026-03-09 17:09:40 +00:00

230 lines
11 KiB
Rust

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}" }
}
}
}