feat: hourly CVE alerting with notification bell and API (#53)
This commit was merged in pull request #53.
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_free_icons::icons::bs_icons::*;
|
||||
use dioxus_free_icons::Icon;
|
||||
|
||||
use crate::infrastructure::notifications::{
|
||||
dismiss_notification, fetch_notification_count, fetch_notifications,
|
||||
mark_all_notifications_read,
|
||||
};
|
||||
|
||||
#[component]
|
||||
pub fn NotificationBell() -> Element {
|
||||
let mut is_open = use_signal(|| false);
|
||||
let mut count = use_signal(|| 0u64);
|
||||
let mut notifications = use_signal(Vec::new);
|
||||
let mut is_loading = use_signal(|| false);
|
||||
|
||||
// Poll notification count every 30 seconds
|
||||
use_resource(move || async move {
|
||||
loop {
|
||||
if let Ok(c) = fetch_notification_count().await {
|
||||
count.set(c);
|
||||
}
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
gloo_timers::future::TimeoutFuture::new(30_000).await;
|
||||
}
|
||||
#[cfg(not(feature = "web"))]
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load notifications when panel opens
|
||||
let load_notifications = move |_| {
|
||||
is_open.set(!is_open());
|
||||
if !is_open() {
|
||||
return;
|
||||
}
|
||||
is_loading.set(true);
|
||||
spawn(async move {
|
||||
if let Ok(resp) = fetch_notifications().await {
|
||||
notifications.set(resp.data);
|
||||
}
|
||||
// Mark all as read when panel opens
|
||||
let _ = mark_all_notifications_read().await;
|
||||
count.set(0);
|
||||
is_loading.set(false);
|
||||
});
|
||||
};
|
||||
|
||||
let on_dismiss = move |id: String| {
|
||||
spawn(async move {
|
||||
let _ = dismiss_notification(id.clone()).await;
|
||||
notifications.write().retain(|n| {
|
||||
n.id.as_ref()
|
||||
.and_then(|v| v.get("$oid"))
|
||||
.and_then(|v| v.as_str())
|
||||
!= Some(&id)
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div { class: "notification-bell-wrapper",
|
||||
// Bell button
|
||||
button {
|
||||
class: "notification-bell-btn",
|
||||
onclick: load_notifications,
|
||||
title: "CVE Alerts",
|
||||
Icon { icon: BsBell, width: 18, height: 18 }
|
||||
if count() > 0 {
|
||||
span { class: "notification-badge", "{count()}" }
|
||||
}
|
||||
}
|
||||
|
||||
// Dropdown panel
|
||||
if is_open() {
|
||||
div { class: "notification-panel",
|
||||
div { class: "notification-panel-header",
|
||||
span { "CVE Alerts" }
|
||||
button {
|
||||
class: "notification-close-btn",
|
||||
onclick: move |_| is_open.set(false),
|
||||
Icon { icon: BsX, width: 16, height: 16 }
|
||||
}
|
||||
}
|
||||
div { class: "notification-panel-body",
|
||||
if is_loading() {
|
||||
div { class: "notification-loading", "Loading..." }
|
||||
} else if notifications().is_empty() {
|
||||
div { class: "notification-empty",
|
||||
Icon { icon: BsShieldCheck, width: 32, height: 32 }
|
||||
p { "No CVE alerts" }
|
||||
}
|
||||
} else {
|
||||
for notif in notifications().iter() {
|
||||
{
|
||||
let id = notif.id.as_ref()
|
||||
.and_then(|v| v.get("$oid"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let sev_class = match notif.severity.as_str() {
|
||||
"critical" => "sev-critical",
|
||||
"high" => "sev-high",
|
||||
"medium" => "sev-medium",
|
||||
_ => "sev-low",
|
||||
};
|
||||
let dismiss_id = id.clone();
|
||||
rsx! {
|
||||
div { class: "notification-item",
|
||||
div { class: "notification-item-header",
|
||||
span { class: "notification-sev {sev_class}",
|
||||
"{notif.severity.to_uppercase()}"
|
||||
}
|
||||
span { class: "notification-cve-id",
|
||||
if let Some(ref url) = notif.url {
|
||||
a { href: "{url}", target: "_blank", "{notif.cve_id}" }
|
||||
} else {
|
||||
"{notif.cve_id}"
|
||||
}
|
||||
}
|
||||
if let Some(score) = notif.cvss_score {
|
||||
span { class: "notification-cvss", "CVSS {score:.1}" }
|
||||
}
|
||||
button {
|
||||
class: "notification-dismiss-btn",
|
||||
title: "Dismiss",
|
||||
onclick: move |_| on_dismiss(dismiss_id.clone()),
|
||||
Icon { icon: BsXCircle, width: 14, height: 14 }
|
||||
}
|
||||
}
|
||||
div { class: "notification-item-pkg",
|
||||
"{notif.package_name} {notif.package_version}"
|
||||
}
|
||||
div { class: "notification-item-repo",
|
||||
"{notif.repo_name}"
|
||||
}
|
||||
if let Some(ref summary) = notif.summary {
|
||||
div { class: "notification-item-summary",
|
||||
"{summary}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user