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
230 lines
11 KiB
Rust
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}" }
|
|
}
|
|
}
|
|
}
|