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:
@@ -12,7 +12,7 @@ path = "../bin/main.rs"
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
web = ["dioxus/web", "dioxus/router", "dioxus/fullstack", "dep:web-sys"]
|
||||
web = ["dioxus/web", "dioxus/router", "dioxus/fullstack", "dep:web-sys", "dep:gloo-timers"]
|
||||
server = [
|
||||
"dioxus/server",
|
||||
"dioxus/router",
|
||||
@@ -43,6 +43,7 @@ thiserror = { workspace = true }
|
||||
# Web-only
|
||||
reqwest = { workspace = true, optional = true }
|
||||
web-sys = { version = "0.3", optional = true }
|
||||
gloo-timers = { version = "0.3", features = ["futures"], optional = true }
|
||||
|
||||
# Server-only
|
||||
axum = { version = "0.8", optional = true }
|
||||
|
||||
@@ -300,6 +300,87 @@ tr:hover {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Toast notifications */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-width: 280px;
|
||||
max-width: 420px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
pointer-events: auto;
|
||||
animation: toast-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border: 1px solid var(--success);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
border: 1px solid var(--danger);
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.toast-info {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border: 1px solid var(--info);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.toast-dismiss {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.toast-dismiss:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@keyframes toast-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button click animation + disabled */
|
||||
.btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
|
||||
@@ -20,6 +20,20 @@ pub enum Route {
|
||||
SbomPage {},
|
||||
#[route("/issues")]
|
||||
IssuesPage {},
|
||||
#[route("/graph")]
|
||||
GraphIndexPage {},
|
||||
#[route("/graph/:repo_id")]
|
||||
GraphExplorerPage { repo_id: String },
|
||||
#[route("/graph/:repo_id/impact/:finding_id")]
|
||||
ImpactAnalysisPage { repo_id: String, finding_id: String },
|
||||
#[route("/dast")]
|
||||
DastOverviewPage {},
|
||||
#[route("/dast/targets")]
|
||||
DastTargetsPage {},
|
||||
#[route("/dast/findings")]
|
||||
DastFindingsPage {},
|
||||
#[route("/dast/findings/:id")]
|
||||
DastFindingDetailPage { id: String },
|
||||
#[route("/settings")]
|
||||
SettingsPage {},
|
||||
}
|
||||
|
||||
@@ -2,15 +2,18 @@ use dioxus::prelude::*;
|
||||
|
||||
use crate::app::Route;
|
||||
use crate::components::sidebar::Sidebar;
|
||||
use crate::components::toast::{ToastContainer, Toasts};
|
||||
|
||||
#[component]
|
||||
pub fn AppShell() -> Element {
|
||||
use_context_provider(Toasts::new);
|
||||
rsx! {
|
||||
div { class: "app-shell",
|
||||
Sidebar {}
|
||||
main { class: "main-content",
|
||||
Outlet::<Route> {}
|
||||
}
|
||||
ToastContainer {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,4 @@ pub mod pagination;
|
||||
pub mod severity_badge;
|
||||
pub mod sidebar;
|
||||
pub mod stat_card;
|
||||
pub mod toast;
|
||||
|
||||
@@ -40,6 +40,16 @@ pub fn Sidebar() -> Element {
|
||||
route: Route::IssuesPage {},
|
||||
icon: rsx! { Icon { icon: BsListTask, width: 18, height: 18 } },
|
||||
},
|
||||
NavItem {
|
||||
label: "Code Graph",
|
||||
route: Route::GraphIndexPage {},
|
||||
icon: rsx! { Icon { icon: BsDiagram3, width: 18, height: 18 } },
|
||||
},
|
||||
NavItem {
|
||||
label: "DAST",
|
||||
route: Route::DastOverviewPage {},
|
||||
icon: rsx! { Icon { icon: BsBug, width: 18, height: 18 } },
|
||||
},
|
||||
NavItem {
|
||||
label: "Settings",
|
||||
route: Route::SettingsPage {},
|
||||
@@ -58,6 +68,12 @@ pub fn Sidebar() -> Element {
|
||||
{
|
||||
let is_active = match (¤t_route, &item.route) {
|
||||
(Route::FindingDetailPage { .. }, Route::FindingsPage {}) => true,
|
||||
(Route::GraphIndexPage {}, Route::GraphIndexPage {}) => true,
|
||||
(Route::GraphExplorerPage { .. }, Route::GraphIndexPage {}) => true,
|
||||
(Route::ImpactAnalysisPage { .. }, Route::GraphIndexPage {}) => true,
|
||||
(Route::DastTargetsPage {}, Route::DastOverviewPage {}) => true,
|
||||
(Route::DastFindingsPage {}, Route::DastOverviewPage {}) => true,
|
||||
(Route::DastFindingDetailPage { .. }, Route::DastOverviewPage {}) => true,
|
||||
(a, b) => a == b,
|
||||
};
|
||||
let class = if is_active { "nav-item active" } else { "nav-item" };
|
||||
|
||||
86
compliance-dashboard/src/components/toast.rs
Normal file
86
compliance-dashboard/src/components/toast.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ToastType {
|
||||
Success,
|
||||
Error,
|
||||
Info,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ToastMessage {
|
||||
pub id: usize,
|
||||
pub message: String,
|
||||
pub toast_type: ToastType,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Toasts {
|
||||
items: Signal<Vec<ToastMessage>>,
|
||||
next_id: Signal<usize>,
|
||||
}
|
||||
|
||||
impl Toasts {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: Signal::new(vec![]),
|
||||
next_id: Signal::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, toast_type: ToastType, message: impl Into<String>) {
|
||||
let id = *self.next_id.read();
|
||||
*self.next_id.write() = id + 1;
|
||||
self.items.write().push(ToastMessage {
|
||||
id,
|
||||
message: message.into(),
|
||||
toast_type,
|
||||
});
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
let mut items = self.items;
|
||||
spawn(async move {
|
||||
gloo_timers::future::TimeoutFuture::new(4_000).await;
|
||||
items.write().retain(|t| t.id != id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: usize) {
|
||||
self.items.write().retain(|t| t.id != id);
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn ToastContainer() -> Element {
|
||||
let mut toasts = use_context::<Toasts>();
|
||||
let items = toasts.items.read();
|
||||
|
||||
rsx! {
|
||||
div { class: "toast-container",
|
||||
for toast in items.iter() {
|
||||
{
|
||||
let toast_id = toast.id;
|
||||
let type_class = match toast.toast_type {
|
||||
ToastType::Success => "toast-success",
|
||||
ToastType::Error => "toast-error",
|
||||
ToastType::Info => "toast-info",
|
||||
};
|
||||
rsx! {
|
||||
div {
|
||||
key: "{toast_id}",
|
||||
class: "toast {type_class}",
|
||||
span { "{toast.message}" }
|
||||
button {
|
||||
class: "toast-dismiss",
|
||||
onclick: move |_| toasts.remove(toast_id),
|
||||
"\u{00d7}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
125
compliance-dashboard/src/infrastructure/dast.rs
Normal file
125
compliance-dashboard/src/infrastructure/dast.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DastTargetsResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DastScanRunsResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DastFindingsResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct DastFindingDetailResponse {
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_dast_targets() -> Result<DastTargetsResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/dast/targets", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: DastTargetsResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_dast_scan_runs() -> Result<DastScanRunsResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/dast/scan-runs", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: DastScanRunsResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_dast_findings() -> Result<DastFindingsResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/dast/findings", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: DastFindingsResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_dast_finding_detail(
|
||||
id: String,
|
||||
) -> Result<DastFindingDetailResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/dast/findings/{id}", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: DastFindingDetailResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn add_dast_target(
|
||||
name: String,
|
||||
base_url: String,
|
||||
) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/dast/targets", state.agent_api_url);
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"name": name,
|
||||
"base_url": base_url,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn trigger_dast_scan(target_id: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/dast/targets/{target_id}/scan",
|
||||
state.agent_api_url
|
||||
);
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
96
compliance-dashboard/src/infrastructure/graph.rs
Normal file
96
compliance-dashboard/src/infrastructure/graph.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct GraphDataResponse {
|
||||
pub data: GraphData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct GraphData {
|
||||
pub build: Option<serde_json::Value>,
|
||||
pub nodes: Vec<serde_json::Value>,
|
||||
pub edges: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ImpactResponse {
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct CommunitiesResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct NodesResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
pub total: Option<u64>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_graph(repo_id: String) -> Result<GraphDataResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/graph/{repo_id}", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: GraphDataResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_impact(
|
||||
repo_id: String,
|
||||
finding_id: String,
|
||||
) -> Result<ImpactResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!(
|
||||
"{}/api/v1/graph/{repo_id}/impact/{finding_id}",
|
||||
state.agent_api_url
|
||||
);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: ImpactResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_communities(repo_id: String) -> Result<CommunitiesResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/graph/{repo_id}/communities", state.agent_api_url);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: CommunitiesResponse = resp
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn trigger_graph_build(repo_id: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/graph/{repo_id}/build", state.agent_api_url);
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Server function modules (compiled for both web and server;
|
||||
// the #[server] macro generates client stubs for the web target)
|
||||
pub mod dast;
|
||||
pub mod findings;
|
||||
pub mod graph;
|
||||
pub mod issues;
|
||||
pub mod repositories;
|
||||
pub mod sbom;
|
||||
|
||||
113
compliance-dashboard/src/pages/dast_finding_detail.rs
Normal file
113
compliance-dashboard/src/pages/dast_finding_detail.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::severity_badge::SeverityBadge;
|
||||
use crate::infrastructure::dast::fetch_dast_finding_detail;
|
||||
|
||||
#[component]
|
||||
pub fn DastFindingDetailPage(id: String) -> Element {
|
||||
let finding = use_resource(move || {
|
||||
let fid = id.clone();
|
||||
async move { fetch_dast_finding_detail(fid).await.ok() }
|
||||
});
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "DAST Finding Detail",
|
||||
description: "Full evidence and details for a dynamic security finding",
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
match &*finding.read() {
|
||||
Some(Some(resp)) => {
|
||||
let f = resp.data.clone();
|
||||
let severity = f.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
|
||||
rsx! {
|
||||
div { class: "flex items-center gap-4 mb-4",
|
||||
SeverityBadge { severity: severity }
|
||||
h2 { "{f.get(\"title\").and_then(|v| v.as_str()).unwrap_or(\"Unknown Finding\")}" }
|
||||
}
|
||||
|
||||
div { class: "grid grid-cols-2 gap-4 mb-4",
|
||||
div {
|
||||
strong { "Vulnerability Type: " }
|
||||
span { class: "badge", "{f.get(\"vuln_type\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
}
|
||||
div {
|
||||
strong { "CWE: " }
|
||||
span { "{f.get(\"cwe\").and_then(|v| v.as_str()).unwrap_or(\"N/A\")}" }
|
||||
}
|
||||
div {
|
||||
strong { "Endpoint: " }
|
||||
code { "{f.get(\"endpoint\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
}
|
||||
div {
|
||||
strong { "Method: " }
|
||||
span { "{f.get(\"method\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
}
|
||||
div {
|
||||
strong { "Parameter: " }
|
||||
code { "{f.get(\"parameter\").and_then(|v| v.as_str()).unwrap_or(\"N/A\")}" }
|
||||
}
|
||||
div {
|
||||
strong { "Exploitable: " }
|
||||
if f.get("exploitable").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
span { class: "badge badge-danger", "Confirmed" }
|
||||
} else {
|
||||
span { class: "badge", "Unconfirmed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 { "Description" }
|
||||
p { "{f.get(\"description\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
|
||||
if let Some(remediation) = f.get("remediation").and_then(|v| v.as_str()) {
|
||||
h3 { class: "mt-4", "Remediation" }
|
||||
p { "{remediation}" }
|
||||
}
|
||||
|
||||
h3 { class: "mt-4", "Evidence" }
|
||||
if let Some(evidence_list) = f.get("evidence").and_then(|v| v.as_array()) {
|
||||
for (i, evidence) in evidence_list.iter().enumerate() {
|
||||
div { class: "card mb-3",
|
||||
h4 { "Evidence #{i + 1}" }
|
||||
div { class: "grid grid-cols-2 gap-2",
|
||||
div {
|
||||
strong { "Request: " }
|
||||
code { "{evidence.get(\"request_method\").and_then(|v| v.as_str()).unwrap_or(\"-\")} {evidence.get(\"request_url\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
}
|
||||
div {
|
||||
strong { "Response Status: " }
|
||||
span { "{evidence.get(\"response_status\").and_then(|v| v.as_u64()).unwrap_or(0)}" }
|
||||
}
|
||||
}
|
||||
if let Some(payload) = evidence.get("payload").and_then(|v| v.as_str()) {
|
||||
div { class: "mt-2",
|
||||
strong { "Payload: " }
|
||||
code { class: "block bg-gray-900 text-green-400 p-2 rounded mt-1",
|
||||
"{payload}"
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(snippet) = evidence.get("response_snippet").and_then(|v| v.as_str()) {
|
||||
div { class: "mt-2",
|
||||
strong { "Response Snippet: " }
|
||||
pre { class: "block bg-gray-900 text-gray-300 p-2 rounded mt-1 overflow-x-auto text-sm",
|
||||
"{snippet}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
p { "No evidence collected." }
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! { p { "Finding not found." } },
|
||||
None => rsx! { p { "Loading..." } },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
compliance-dashboard/src/pages/dast_findings.rs
Normal file
79
compliance-dashboard/src/pages/dast_findings.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::app::Route;
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::severity_badge::SeverityBadge;
|
||||
use crate::infrastructure::dast::fetch_dast_findings;
|
||||
|
||||
#[component]
|
||||
pub fn DastFindingsPage() -> Element {
|
||||
let findings = use_resource(|| async { fetch_dast_findings().await.ok() });
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "DAST Findings",
|
||||
description: "Vulnerabilities discovered through dynamic application security testing",
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
match &*findings.read() {
|
||||
Some(Some(data)) => {
|
||||
let finding_list = &data.data;
|
||||
if finding_list.is_empty() {
|
||||
rsx! { p { "No DAST findings yet. Run a scan to discover vulnerabilities." } }
|
||||
} else {
|
||||
rsx! {
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Severity" }
|
||||
th { "Type" }
|
||||
th { "Title" }
|
||||
th { "Endpoint" }
|
||||
th { "Method" }
|
||||
th { "Exploitable" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for finding in finding_list {
|
||||
{
|
||||
let id = finding.get("_id").and_then(|v| v.get("$oid")).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let severity = finding.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
|
||||
rsx! {
|
||||
tr {
|
||||
td { SeverityBadge { severity: severity } }
|
||||
td {
|
||||
span { class: "badge",
|
||||
"{finding.get(\"vuln_type\").and_then(|v| v.as_str()).unwrap_or(\"-\")}"
|
||||
}
|
||||
}
|
||||
td {
|
||||
Link {
|
||||
to: Route::DastFindingDetailPage { id: id },
|
||||
"{finding.get(\"title\").and_then(|v| v.as_str()).unwrap_or(\"-\")}"
|
||||
}
|
||||
}
|
||||
td { code { class: "text-sm", "{finding.get(\"endpoint\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" } }
|
||||
td { "{finding.get(\"method\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
td {
|
||||
if finding.get("exploitable").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
span { class: "badge badge-danger", "Confirmed" }
|
||||
} else {
|
||||
span { class: "badge", "Unconfirmed" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! { p { "Failed to load findings." } },
|
||||
None => rsx! { p { "Loading..." } },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
107
compliance-dashboard/src/pages/dast_overview.rs
Normal file
107
compliance-dashboard/src/pages/dast_overview.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::app::Route;
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::infrastructure::dast::{fetch_dast_findings, fetch_dast_scan_runs};
|
||||
|
||||
#[component]
|
||||
pub fn DastOverviewPage() -> Element {
|
||||
let scan_runs = use_resource(|| async { fetch_dast_scan_runs().await.ok() });
|
||||
let findings = use_resource(|| async { fetch_dast_findings().await.ok() });
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "DAST Overview",
|
||||
description: "Dynamic Application Security Testing — scan running applications for vulnerabilities",
|
||||
}
|
||||
|
||||
div { class: "grid grid-cols-3 gap-4 mb-6",
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value",
|
||||
match &*scan_runs.read() {
|
||||
Some(Some(data)) => {
|
||||
let count = data.total.unwrap_or(0);
|
||||
rsx! { "{count}" }
|
||||
},
|
||||
_ => rsx! { "—" },
|
||||
}
|
||||
}
|
||||
div { class: "stat-label", "Total Scans" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value",
|
||||
match &*findings.read() {
|
||||
Some(Some(data)) => {
|
||||
let count = data.total.unwrap_or(0);
|
||||
rsx! { "{count}" }
|
||||
},
|
||||
_ => rsx! { "—" },
|
||||
}
|
||||
}
|
||||
div { class: "stat-label", "DAST Findings" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value", "—" }
|
||||
div { class: "stat-label", "Active Targets" }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "flex gap-4 mb-4",
|
||||
Link {
|
||||
to: Route::DastTargetsPage {},
|
||||
class: "btn btn-primary",
|
||||
"Manage Targets"
|
||||
}
|
||||
Link {
|
||||
to: Route::DastFindingsPage {},
|
||||
class: "btn btn-secondary",
|
||||
"View Findings"
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
h3 { "Recent Scan Runs" }
|
||||
match &*scan_runs.read() {
|
||||
Some(Some(data)) => {
|
||||
let runs = &data.data;
|
||||
if runs.is_empty() {
|
||||
rsx! { p { "No scan runs yet." } }
|
||||
} else {
|
||||
rsx! {
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Target" }
|
||||
th { "Status" }
|
||||
th { "Phase" }
|
||||
th { "Findings" }
|
||||
th { "Exploitable" }
|
||||
th { "Started" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for run in runs {
|
||||
tr {
|
||||
td { "{run.get(\"target_id\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
td {
|
||||
span { class: "badge",
|
||||
"{run.get(\"status\").and_then(|v| v.as_str()).unwrap_or(\"unknown\")}"
|
||||
}
|
||||
}
|
||||
td { "{run.get(\"current_phase\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
td { "{run.get(\"findings_count\").and_then(|v| v.as_u64()).unwrap_or(0)}" }
|
||||
td { "{run.get(\"exploitable_count\").and_then(|v| v.as_u64()).unwrap_or(0)}" }
|
||||
td { "{run.get(\"started_at\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! { p { "Failed to load scan runs." } },
|
||||
None => rsx! { p { "Loading..." } },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
145
compliance-dashboard/src/pages/dast_targets.rs
Normal file
145
compliance-dashboard/src/pages/dast_targets.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::toast::{ToastType, Toasts};
|
||||
use crate::infrastructure::dast::{add_dast_target, fetch_dast_targets, trigger_dast_scan};
|
||||
|
||||
#[component]
|
||||
pub fn DastTargetsPage() -> Element {
|
||||
let mut targets = use_resource(|| async { fetch_dast_targets().await.ok() });
|
||||
let mut toasts = use_context::<Toasts>();
|
||||
|
||||
let mut show_form = use_signal(|| false);
|
||||
let mut new_name = use_signal(String::new);
|
||||
let mut new_url = use_signal(String::new);
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "DAST Targets",
|
||||
description: "Configure target applications for dynamic security testing",
|
||||
}
|
||||
|
||||
div { class: "mb-4",
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: move |_| show_form.set(!show_form()),
|
||||
if show_form() { "Cancel" } else { "Add Target" }
|
||||
}
|
||||
}
|
||||
|
||||
if show_form() {
|
||||
div { class: "card mb-4",
|
||||
h3 { "Add New Target" }
|
||||
div { class: "form-group",
|
||||
label { "Name" }
|
||||
input {
|
||||
class: "input",
|
||||
r#type: "text",
|
||||
placeholder: "My Web App",
|
||||
value: "{new_name}",
|
||||
oninput: move |e| new_name.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Base URL" }
|
||||
input {
|
||||
class: "input",
|
||||
r#type: "text",
|
||||
placeholder: "https://example.com",
|
||||
value: "{new_url}",
|
||||
oninput: move |e| new_url.set(e.value()),
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: move |_| {
|
||||
let name = new_name();
|
||||
let url = new_url();
|
||||
spawn(async move {
|
||||
match add_dast_target(name, url).await {
|
||||
Ok(_) => {
|
||||
toasts.push(ToastType::Success, "Target created");
|
||||
targets.restart();
|
||||
}
|
||||
Err(e) => toasts.push(ToastType::Error, e.to_string()),
|
||||
}
|
||||
});
|
||||
show_form.set(false);
|
||||
new_name.set(String::new());
|
||||
new_url.set(String::new());
|
||||
},
|
||||
"Create Target"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
h3 { "Configured Targets" }
|
||||
match &*targets.read() {
|
||||
Some(Some(data)) => {
|
||||
let target_list = &data.data;
|
||||
if target_list.is_empty() {
|
||||
rsx! { p { "No DAST targets configured. Add one to get started." } }
|
||||
} else {
|
||||
rsx! {
|
||||
table { class: "table",
|
||||
thead {
|
||||
tr {
|
||||
th { "Name" }
|
||||
th { "URL" }
|
||||
th { "Type" }
|
||||
th { "Rate Limit" }
|
||||
th { "Destructive" }
|
||||
th { "Actions" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for target in target_list {
|
||||
{
|
||||
let target_id = target.get("_id").and_then(|v| v.get("$oid")).and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
rsx! {
|
||||
tr {
|
||||
td { "{target.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
td { code { "{target.get(\"base_url\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" } }
|
||||
td { "{target.get(\"target_type\").and_then(|v| v.as_str()).unwrap_or(\"-\")}" }
|
||||
td { "{target.get(\"rate_limit\").and_then(|v| v.as_u64()).unwrap_or(0)} req/s" }
|
||||
td {
|
||||
if target.get("allow_destructive").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
span { class: "badge badge-danger", "Yes" }
|
||||
} else {
|
||||
span { class: "badge badge-success", "No" }
|
||||
}
|
||||
}
|
||||
td {
|
||||
button {
|
||||
class: "btn btn-sm",
|
||||
onclick: {
|
||||
let tid = target_id.clone();
|
||||
move |_| {
|
||||
let tid = tid.clone();
|
||||
spawn(async move {
|
||||
match trigger_dast_scan(tid).await {
|
||||
Ok(_) => toasts.push(ToastType::Success, "DAST scan triggered"),
|
||||
Err(e) => toasts.push(ToastType::Error, e.to_string()),
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
"Scan"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! { p { "Failed to load targets." } },
|
||||
None => rsx! { p { "Loading..." } },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
105
compliance-dashboard/src/pages/graph_explorer.rs
Normal file
105
compliance-dashboard/src/pages/graph_explorer.rs
Normal file
@@ -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..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
compliance-dashboard/src/pages/graph_index.rs
Normal file
53
compliance-dashboard/src/pages/graph_index.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::app::Route;
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::infrastructure::repositories::fetch_repositories;
|
||||
|
||||
#[component]
|
||||
pub fn GraphIndexPage() -> Element {
|
||||
let repos = use_resource(|| async { fetch_repositories(1).await.ok() });
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "Code Knowledge Graph",
|
||||
description: "Select a repository to explore its code graph",
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
h3 { "Repositories" }
|
||||
match &*repos.read() {
|
||||
Some(Some(data)) => {
|
||||
let repo_list = &data.data;
|
||||
if repo_list.is_empty() {
|
||||
rsx! { p { "No repositories found. Add a repository first." } }
|
||||
} else {
|
||||
rsx! {
|
||||
div { class: "grid grid-cols-1 gap-3",
|
||||
for repo in repo_list {
|
||||
{
|
||||
let repo_id = repo.id.map(|id| id.to_hex()).unwrap_or_default();
|
||||
let name = repo.name.clone();
|
||||
let url = repo.git_url.clone();
|
||||
rsx! {
|
||||
Link {
|
||||
to: Route::GraphExplorerPage { repo_id: repo_id },
|
||||
class: "card hover:bg-gray-800 transition-colors cursor-pointer",
|
||||
h4 { "{name}" }
|
||||
if !url.is_empty() {
|
||||
p { class: "text-sm text-muted", "{url}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! { p { "Failed to load repositories." } },
|
||||
None => rsx! { p { "Loading repositories..." } },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
97
compliance-dashboard/src/pages/impact_analysis.rs
Normal file
97
compliance-dashboard/src/pages/impact_analysis.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::infrastructure::graph::fetch_impact;
|
||||
|
||||
#[component]
|
||||
pub fn ImpactAnalysisPage(repo_id: String, finding_id: String) -> Element {
|
||||
let impact_data = use_resource(move || {
|
||||
let rid = repo_id.clone();
|
||||
let fid = finding_id.clone();
|
||||
async move { fetch_impact(rid, fid).await.ok() }
|
||||
});
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "Impact Analysis",
|
||||
description: "Blast radius and affected entry points for a security finding",
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
match &*impact_data.read() {
|
||||
Some(Some(resp)) => {
|
||||
let impact = resp.data.clone().unwrap_or_default();
|
||||
rsx! {
|
||||
div { class: "grid grid-cols-2 gap-4 mb-4",
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value",
|
||||
"{impact.get(\"blast_radius\").and_then(|v| v.as_u64()).unwrap_or(0)}"
|
||||
}
|
||||
div { class: "stat-label", "Blast Radius (nodes affected)" }
|
||||
}
|
||||
div { class: "stat-card",
|
||||
div { class: "stat-value",
|
||||
"{impact.get(\"affected_entry_points\").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0)}"
|
||||
}
|
||||
div { class: "stat-label", "Entry Points Affected" }
|
||||
}
|
||||
}
|
||||
|
||||
h3 { "Affected Entry Points" }
|
||||
if let Some(entries) = impact.get("affected_entry_points").and_then(|v| v.as_array()) {
|
||||
if entries.is_empty() {
|
||||
p { class: "text-muted", "No entry points affected." }
|
||||
} else {
|
||||
ul { class: "list",
|
||||
for entry in entries {
|
||||
li { "{entry.as_str().unwrap_or(\"-\")}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 { class: "mt-4", "Call Chains" }
|
||||
if let Some(chains) = impact.get("call_chains").and_then(|v| v.as_array()) {
|
||||
if chains.is_empty() {
|
||||
p { class: "text-muted", "No call chains found." }
|
||||
} else {
|
||||
for (i, chain) in chains.iter().enumerate() {
|
||||
div { class: "card mb-2",
|
||||
strong { "Chain {i + 1}: " }
|
||||
if let Some(steps) = chain.as_array() {
|
||||
for (j, step) in steps.iter().enumerate() {
|
||||
span { "{step.as_str().unwrap_or(\"-\")}" }
|
||||
if j < steps.len() - 1 {
|
||||
span { class: "text-muted", " → " }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 { class: "mt-4", "Direct Callers" }
|
||||
if let Some(callers) = impact.get("direct_callers").and_then(|v| v.as_array()) {
|
||||
if callers.is_empty() {
|
||||
p { class: "text-muted", "No direct callers." }
|
||||
} else {
|
||||
ul { class: "list",
|
||||
for caller in callers {
|
||||
li { code { "{caller.as_str().unwrap_or(\"-\")}" } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! {
|
||||
p { "No impact analysis data available for this finding." }
|
||||
},
|
||||
None => rsx! {
|
||||
p { "Loading impact analysis..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
pub mod dast_finding_detail;
|
||||
pub mod dast_findings;
|
||||
pub mod dast_overview;
|
||||
pub mod dast_targets;
|
||||
pub mod finding_detail;
|
||||
pub mod findings;
|
||||
pub mod graph_explorer;
|
||||
pub mod graph_index;
|
||||
pub mod impact_analysis;
|
||||
pub mod issues;
|
||||
pub mod overview;
|
||||
pub mod repositories;
|
||||
pub mod sbom;
|
||||
pub mod settings;
|
||||
|
||||
pub use dast_finding_detail::DastFindingDetailPage;
|
||||
pub use dast_findings::DastFindingsPage;
|
||||
pub use dast_overview::DastOverviewPage;
|
||||
pub use dast_targets::DastTargetsPage;
|
||||
pub use finding_detail::FindingDetailPage;
|
||||
pub use findings::FindingsPage;
|
||||
pub use graph_explorer::GraphExplorerPage;
|
||||
pub use graph_index::GraphIndexPage;
|
||||
pub use impact_analysis::ImpactAnalysisPage;
|
||||
pub use issues::IssuesPage;
|
||||
pub use overview::OverviewPage;
|
||||
pub use repositories::RepositoriesPage;
|
||||
|
||||
@@ -2,6 +2,7 @@ use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::pagination::Pagination;
|
||||
use crate::components::toast::{ToastType, Toasts};
|
||||
|
||||
#[component]
|
||||
pub fn RepositoriesPage() -> Element {
|
||||
@@ -10,8 +11,9 @@ pub fn RepositoriesPage() -> Element {
|
||||
let mut name = use_signal(String::new);
|
||||
let mut git_url = use_signal(String::new);
|
||||
let mut branch = use_signal(|| "main".to_string());
|
||||
let mut toasts = use_context::<Toasts>();
|
||||
|
||||
let repos = use_resource(move || {
|
||||
let mut repos = use_resource(move || {
|
||||
let p = page();
|
||||
async move {
|
||||
crate::infrastructure::repositories::fetch_repositories(p)
|
||||
@@ -71,7 +73,13 @@ pub fn RepositoriesPage() -> Element {
|
||||
let u = git_url();
|
||||
let b = branch();
|
||||
spawn(async move {
|
||||
let _ = crate::infrastructure::repositories::add_repository(n, u, b).await;
|
||||
match crate::infrastructure::repositories::add_repository(n, u, b).await {
|
||||
Ok(_) => {
|
||||
toasts.push(ToastType::Success, "Repository added");
|
||||
repos.restart();
|
||||
}
|
||||
Err(e) => toasts.push(ToastType::Error, e.to_string()),
|
||||
}
|
||||
});
|
||||
show_add_form.set(false);
|
||||
name.set(String::new());
|
||||
@@ -125,7 +133,10 @@ pub fn RepositoriesPage() -> Element {
|
||||
onclick: move |_| {
|
||||
let id = repo_id_clone.clone();
|
||||
spawn(async move {
|
||||
let _ = crate::infrastructure::repositories::trigger_repo_scan(id).await;
|
||||
match crate::infrastructure::repositories::trigger_repo_scan(id).await {
|
||||
Ok(_) => toasts.push(ToastType::Success, "Scan triggered"),
|
||||
Err(e) => toasts.push(ToastType::Error, e.to_string()),
|
||||
}
|
||||
});
|
||||
},
|
||||
"Scan"
|
||||
|
||||
Reference in New Issue
Block a user