156 lines
6.7 KiB
Rust
156 lines
6.7 KiB
Rust
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}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|