Add DAST, graph modules, toast notifications, and dashboard enhancements

Add DAST scanning and code knowledge graph features across the stack:
- compliance-dast and compliance-graph workspace crates
- Agent API handlers and routes for DAST targets/scans and graph builds
- Core models and traits for DAST and graph domains
- Dashboard pages for DAST targets/findings/overview and graph explorer/impact
- Toast notification system with auto-dismiss for async action feedback
- Button click animations and disabled states for better UX

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-03-04 13:53:50 +01:00
co-authored by Claude Opus 4.6
parent 03ee69834d
commit cea8f59e10
69 changed files with 8745 additions and 54 deletions
@@ -0,0 +1,105 @@
use dioxus::prelude::*;
use crate::components::page_header::PageHeader;
use crate::components::toast::{ToastType, Toasts};
use crate::infrastructure::graph::{fetch_graph, trigger_graph_build};
#[component]
pub fn GraphExplorerPage(repo_id: String) -> Element {
let repo_id_clone = repo_id.clone();
let mut graph_data = use_resource(move || {
let rid = repo_id_clone.clone();
async move {
if rid.is_empty() {
return None;
}
fetch_graph(rid).await.ok()
}
});
let mut building = use_signal(|| false);
let mut toasts = use_context::<Toasts>();
rsx! {
PageHeader {
title: "Code Knowledge Graph",
description: "Interactive visualization of code structure and relationships",
}
if repo_id.is_empty() {
div { class: "card",
p { "Select a repository to view its code graph." }
p { "You can trigger a graph build from the Repositories page." }
}
} else {
div { style: "margin-bottom: 16px;",
button {
class: "btn btn-primary",
disabled: building(),
onclick: {
let rid = repo_id.clone();
move |_| {
let rid = rid.clone();
building.set(true);
spawn(async move {
match trigger_graph_build(rid).await {
Ok(_) => toasts.push(ToastType::Success, "Graph build triggered"),
Err(e) => toasts.push(ToastType::Error, e.to_string()),
}
building.set(false);
graph_data.restart();
});
}
},
if building() { "Building..." } else { "Build Graph" }
}
}
div { class: "card",
h3 { "Graph Explorer \u{2014} {repo_id}" }
match &*graph_data.read() {
Some(Some(data)) => {
let build = data.data.build.clone().unwrap_or_default();
let node_count = build.get("node_count").and_then(|n| n.as_u64()).unwrap_or(0);
let edge_count = build.get("edge_count").and_then(|n| n.as_u64()).unwrap_or(0);
let community_count = build.get("community_count").and_then(|n| n.as_u64()).unwrap_or(0);
rsx! {
div { class: "grid grid-cols-3 gap-4 mb-4",
div { class: "stat-card",
div { class: "stat-value", "{node_count}" }
div { class: "stat-label", "Nodes" }
}
div { class: "stat-card",
div { class: "stat-value", "{edge_count}" }
div { class: "stat-label", "Edges" }
}
div { class: "stat-card",
div { class: "stat-value", "{community_count}" }
div { class: "stat-label", "Communities" }
}
}
div {
id: "graph-container",
style: "width: 100%; height: 600px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-secondary);",
}
script {
r#"
console.log('Graph explorer loaded');
"#
}
}
},
Some(None) => rsx! {
p { "No graph data available. Build the graph first." }
},
None => rsx! {
p { "Loading graph data..." }
},
}
}
}
}
}