Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #1
155 lines
6.5 KiB
Rust
155 lines
6.5 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
use crate::app::Route;
|
|
use crate::components::page_header::PageHeader;
|
|
use crate::components::pagination::Pagination;
|
|
use crate::components::severity_badge::SeverityBadge;
|
|
|
|
#[component]
|
|
pub fn FindingsPage() -> Element {
|
|
let mut page = use_signal(|| 1u64);
|
|
let mut severity_filter = use_signal(String::new);
|
|
let mut type_filter = use_signal(String::new);
|
|
let mut status_filter = use_signal(String::new);
|
|
let mut repo_filter = use_signal(String::new);
|
|
|
|
let repos = use_resource(|| async {
|
|
crate::infrastructure::repositories::fetch_repositories(1)
|
|
.await
|
|
.ok()
|
|
});
|
|
|
|
let findings = use_resource(move || {
|
|
let p = page();
|
|
let sev = severity_filter();
|
|
let typ = type_filter();
|
|
let stat = status_filter();
|
|
let repo = repo_filter();
|
|
async move {
|
|
crate::infrastructure::findings::fetch_findings(p, sev, typ, stat, repo)
|
|
.await
|
|
.ok()
|
|
}
|
|
});
|
|
|
|
rsx! {
|
|
PageHeader {
|
|
title: "Findings",
|
|
description: "Security and compliance findings across all repositories",
|
|
}
|
|
|
|
div { class: "filter-bar",
|
|
select {
|
|
onchange: move |e| { repo_filter.set(e.value()); page.set(1); },
|
|
option { value: "", "All Repositories" }
|
|
{
|
|
match &*repos.read() {
|
|
Some(Some(resp)) => rsx! {
|
|
for repo in &resp.data {
|
|
{
|
|
let id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
|
let name = repo.name.clone();
|
|
rsx! {
|
|
option { value: "{id}", "{name}" }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_ => rsx! {},
|
|
}
|
|
}
|
|
}
|
|
select {
|
|
onchange: move |e| { severity_filter.set(e.value()); page.set(1); },
|
|
option { value: "", "All Severities" }
|
|
option { value: "critical", "Critical" }
|
|
option { value: "high", "High" }
|
|
option { value: "medium", "Medium" }
|
|
option { value: "low", "Low" }
|
|
option { value: "info", "Info" }
|
|
}
|
|
select {
|
|
onchange: move |e| { type_filter.set(e.value()); page.set(1); },
|
|
option { value: "", "All Types" }
|
|
option { value: "sast", "SAST" }
|
|
option { value: "sbom", "SBOM" }
|
|
option { value: "cve", "CVE" }
|
|
option { value: "gdpr", "GDPR" }
|
|
option { value: "oauth", "OAuth" }
|
|
}
|
|
select {
|
|
onchange: move |e| { status_filter.set(e.value()); page.set(1); },
|
|
option { value: "", "All Statuses" }
|
|
option { value: "open", "Open" }
|
|
option { value: "triaged", "Triaged" }
|
|
option { value: "resolved", "Resolved" }
|
|
option { value: "false_positive", "False Positive" }
|
|
option { value: "ignored", "Ignored" }
|
|
}
|
|
}
|
|
|
|
match &*findings.read() {
|
|
Some(Some(resp)) => {
|
|
let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1);
|
|
rsx! {
|
|
div { class: "card",
|
|
div { class: "table-wrapper",
|
|
table {
|
|
thead {
|
|
tr {
|
|
th { "Severity" }
|
|
th { "Title" }
|
|
th { "Type" }
|
|
th { "Scanner" }
|
|
th { "File" }
|
|
th { "Status" }
|
|
}
|
|
}
|
|
tbody {
|
|
for finding in &resp.data {
|
|
{
|
|
let id = finding.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
|
rsx! {
|
|
tr {
|
|
td { SeverityBadge { severity: finding.severity.to_string() } }
|
|
td {
|
|
Link {
|
|
to: Route::FindingDetailPage { id },
|
|
style: "color: var(--accent); text-decoration: none;",
|
|
"{finding.title}"
|
|
}
|
|
}
|
|
td { "{finding.scan_type}" }
|
|
td { "{finding.scanner}" }
|
|
td {
|
|
style: "font-family: monospace; font-size: 12px;",
|
|
"{finding.file_path.as_deref().unwrap_or(\"-\")}"
|
|
}
|
|
td {
|
|
span { class: "badge badge-info", "{finding.status}" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Pagination {
|
|
current_page: page(),
|
|
total_pages: total_pages,
|
|
on_page_change: move |p| page.set(p),
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Some(None) => rsx! {
|
|
div { class: "card", p { "Failed to load findings." } }
|
|
},
|
|
None => rsx! {
|
|
div { class: "loading", "Loading findings..." }
|
|
},
|
|
}
|
|
}
|
|
}
|