feat: made beacon search and list better
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use leptos::prelude::*;
|
||||
use leptos::{either::Either, prelude::*};
|
||||
#[cfg(feature = "hydrate")]
|
||||
use leptos_use::{use_websocket, UseWebSocketReturn};
|
||||
use leptos_use::use_websocket;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::beacons::BeaconResources;
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
use super::templates::BeaconTemplate;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
enum SortMethod {
|
||||
Listener,
|
||||
Config,
|
||||
@@ -69,11 +66,13 @@ unsafe impl Send for SidebarEvents {}
|
||||
pub enum SidebarEvents {
|
||||
BeaconList(Vec<CurrentBeaconInstance>),
|
||||
NewBeacon(CurrentBeaconInstance),
|
||||
BeaconUpdate(String, CurrentBeaconInstance),
|
||||
Checkin(String),
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn BeaconSidebar() -> impl IntoView {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let BeaconResources {
|
||||
listeners,
|
||||
templates,
|
||||
@@ -82,16 +81,17 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
..
|
||||
} = expect_context::<BeaconResources>();
|
||||
|
||||
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||
let current_beacons = RwSignal::new(None::<Vec<CurrentBeaconInstance>>);
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let (web_socket, rebuild_websocket) = signal(use_websocket::<
|
||||
(),
|
||||
SidebarEvents,
|
||||
codee::string::JsonSerdeCodec,
|
||||
>("/api/subscribe/listener"));
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
Effect::new(move |_| {
|
||||
web_socket.with(move |uwsr| {
|
||||
uwsr.message.with(move |message| {
|
||||
@@ -112,6 +112,14 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
}
|
||||
});
|
||||
}
|
||||
SidebarEvents::BeaconUpdate(bid, bnew) => current_beacons.update(|bs| {
|
||||
let Some(ref mut bs) = bs else {
|
||||
return;
|
||||
};
|
||||
if let Some(ref mut bold) = bs.iter_mut().find(|b| b.beacon_id == *bid) {
|
||||
**bold = bnew.clone();
|
||||
}
|
||||
}),
|
||||
SidebarEvents::Checkin(bid) => current_beacons.update(|bs| {
|
||||
let Some(ref mut bs) = bs else {
|
||||
return;
|
||||
@@ -125,7 +133,7 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
});
|
||||
});
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
Effect::new(move |_| {
|
||||
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
|
||||
user.with(move |_| {
|
||||
@@ -139,9 +147,11 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
|
||||
let (sort_method, set_sort_method) = signal(None::<SortMethod>);
|
||||
let search_input = RwSignal::new("".to_string());
|
||||
let checkin_filter = RwSignal::new("".to_string());
|
||||
|
||||
struct BeaconPartition {
|
||||
title: Option<String>,
|
||||
key: String,
|
||||
beacons: Arc<dyn Fn() -> Vec<CurrentBeaconInstance> + Send + Sync>,
|
||||
}
|
||||
|
||||
@@ -151,38 +161,103 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let partitions = move || -> Vec<BeaconPartition> {
|
||||
use regex::Regex;
|
||||
|
||||
leptos::logging::log!(
|
||||
"There are {:?} beacons",
|
||||
current_beacons.read().as_ref().map(Vec::len)
|
||||
);
|
||||
let sm = sort_method.read();
|
||||
let search_regex = Regex::new(&*search_input.read());
|
||||
let str_match_search = {
|
||||
let search_inp = search_input.get();
|
||||
move |inp: &str| {
|
||||
search_inp.is_empty()
|
||||
|| search_regex
|
||||
.as_ref()
|
||||
.map(|re| re.is_match(inp))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
};
|
||||
|
||||
//let Some(Ok(ref listeners)) = *listeners.read() else {
|
||||
// return vec![];
|
||||
//};
|
||||
//let Some(Ok(ref templates)) = *templates.read() else {
|
||||
// return vec![];
|
||||
//};
|
||||
//let Some(Ok(ref categories)) = *categories.read() else {
|
||||
// return vec![];
|
||||
//};
|
||||
let partition_filter = {
|
||||
let str_match_search = str_match_search.clone();
|
||||
move |partition: &BeaconPartition| {
|
||||
partition
|
||||
.title
|
||||
.as_deref()
|
||||
.map(|title| (str_match_search)(&title))
|
||||
.unwrap_or_default()
|
||||
|| !(partition.beacons)().is_empty()
|
||||
}
|
||||
};
|
||||
|
||||
macro_rules! beacon_filter {
|
||||
($part_title:expr, $search_input:expr, $beacon:expr, $templates:expr, $categories:expr) => {{
|
||||
let search_inp = &*search_input.read();
|
||||
let duration_inp = &*checkin_filter.read();
|
||||
|
||||
let query_empty = search_inp.is_empty();
|
||||
let duration_empty = duration_inp.is_empty();
|
||||
|
||||
let search_regex = ::regex::Regex::new(search_inp);
|
||||
let matches = |inp: &str| {
|
||||
search_regex
|
||||
.as_ref()
|
||||
.map(|re| re.is_match(inp))
|
||||
.unwrap_or(true)
|
||||
};
|
||||
|
||||
let match_search = query_empty
|
||||
|| $part_title.as_deref().map(matches).unwrap_or_default()
|
||||
|| matches(&$beacon.beacon_id)
|
||||
|| matches(&$beacon.nickname)
|
||||
|| matches(&$beacon.ip)
|
||||
|| matches(&$beacon.cwd)
|
||||
|| matches(&$beacon.userent)
|
||||
|| matches(&$beacon.hostname)
|
||||
|| $templates
|
||||
.iter()
|
||||
.find(|t| t.template_id == $beacon.template_id)
|
||||
.map(|t| matches(&t.template_name))
|
||||
.unwrap_or(false)
|
||||
|| $categories
|
||||
.iter()
|
||||
.filter(|c| $beacon.category_ids.contains(&c.category_id))
|
||||
.any(|c| matches(&c.category_name));
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let pass_timefilter = duration_empty
|
||||
|| duration_str::parse_chrono(duration_inp)
|
||||
.map(|dur| $beacon.last_checkin + dur > now)
|
||||
.unwrap_or(true);
|
||||
|
||||
match_search && pass_timefilter
|
||||
}};
|
||||
}
|
||||
|
||||
match *sm {
|
||||
Some(SortMethod::Config) => {
|
||||
let Some(Ok(ref configs)) = *configs.read() else {
|
||||
let Some(Ok(ref configs_super)) = *configs.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
configs
|
||||
configs_super
|
||||
.iter()
|
||||
.map(|config| {
|
||||
let config = config.clone();
|
||||
BeaconPartition {
|
||||
title: Some(config.config_name.clone()),
|
||||
key: format!("config-{}", &config.config_id),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(Ok(ref categories)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
current_beacons
|
||||
.get()
|
||||
.unwrap_or(vec![])
|
||||
@@ -194,28 +269,42 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
.map(|t| t.config_id))
|
||||
== Some(config.config_id)
|
||||
})
|
||||
.filter(|beacon| {
|
||||
beacon_filter!(
|
||||
Some(config.config_name.clone()),
|
||||
search_input,
|
||||
beacon,
|
||||
templates,
|
||||
categories
|
||||
)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.filter(partition_filter)
|
||||
.collect()
|
||||
}
|
||||
Some(SortMethod::Listener) => {
|
||||
let Some(Ok(ref listeners)) = *listeners.read() else {
|
||||
let Some(Ok(ref listeners_super)) = *listeners.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
listeners
|
||||
listeners_super
|
||||
.iter()
|
||||
.map(|listener| {
|
||||
let listener = listener.clone();
|
||||
BeaconPartition {
|
||||
title: Some(listener.domain_name.clone()),
|
||||
key: format!("template-{}", &listener.listener_id),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(Ok(ref categories)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
current_beacons
|
||||
.get()
|
||||
@@ -228,16 +317,167 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
.map(|t| t.listener_id == listener.listener_id)
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.filter(|beacon| {
|
||||
beacon_filter!(
|
||||
Some(listener.domain_name.clone()),
|
||||
search_input,
|
||||
beacon,
|
||||
templates,
|
||||
categories
|
||||
)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
})
|
||||
.filter(partition_filter)
|
||||
.collect()
|
||||
}
|
||||
Some(SortMethod::Category) => {
|
||||
let Some(Ok(ref categories_super)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut category_partitions = vec![BeaconPartition {
|
||||
title: None,
|
||||
key: "uncategorized".to_string(),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(Ok(ref categories)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
current_beacons
|
||||
.get()
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.filter(|b| b.category_ids.is_empty())
|
||||
.filter(|beacon| {
|
||||
beacon_filter!(
|
||||
None::<String>,
|
||||
search_input,
|
||||
beacon,
|
||||
templates,
|
||||
categories
|
||||
)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}];
|
||||
|
||||
category_partitions.extend(
|
||||
categories_super
|
||||
.iter()
|
||||
.map(|category| {
|
||||
let category = category.clone();
|
||||
BeaconPartition {
|
||||
title: Some(category.category_name.clone()),
|
||||
key: format!("category-{}", category.category_id),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(Ok(ref categories)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
current_beacons
|
||||
.get()
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.filter(|b| b.category_ids.contains(&category.category_id))
|
||||
.filter(|beacon| {
|
||||
beacon_filter!(
|
||||
Some(category.category_name.clone()),
|
||||
search_input,
|
||||
beacon,
|
||||
templates,
|
||||
categories
|
||||
)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
})
|
||||
.filter(partition_filter),
|
||||
);
|
||||
|
||||
category_partitions
|
||||
}
|
||||
Some(SortMethod::Template) => {
|
||||
let Some(Ok(ref templates_super)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
templates_super
|
||||
.iter()
|
||||
.map(|template| {
|
||||
let template = template.clone();
|
||||
BeaconPartition {
|
||||
title: Some(template.template_name.clone()),
|
||||
key: format!("template-{}", &template.template_id),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(Ok(ref categories)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
current_beacons
|
||||
.get()
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.filter(|b| b.template_id == template.template_id)
|
||||
.filter(|beacon| {
|
||||
beacon_filter!(
|
||||
Some(template.template_name.clone()),
|
||||
search_input,
|
||||
beacon,
|
||||
templates,
|
||||
categories
|
||||
)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
})
|
||||
.filter(partition_filter)
|
||||
.collect()
|
||||
}
|
||||
_ => vec![BeaconPartition {
|
||||
title: None,
|
||||
beacons: Arc::new(move || current_beacons.get().unwrap_or(vec![]).clone()),
|
||||
key: "unsorted".to_string(),
|
||||
beacons: Arc::new(move || {
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return vec![];
|
||||
};
|
||||
let Some(Ok(ref categories)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
current_beacons
|
||||
.get()
|
||||
.unwrap_or(vec![])
|
||||
.iter()
|
||||
.filter(|beacon| {
|
||||
beacon_filter!(
|
||||
None::<String>,
|
||||
search_input,
|
||||
beacon,
|
||||
templates,
|
||||
categories
|
||||
)
|
||||
})
|
||||
.map(Clone::clone)
|
||||
.collect()
|
||||
}),
|
||||
}],
|
||||
}
|
||||
};
|
||||
@@ -248,14 +488,31 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
let partitions = || {
|
||||
vec![BeaconPartition {
|
||||
title: None,
|
||||
key: "unsorted".to_string(),
|
||||
beacons: Arc::new(move || vec![]),
|
||||
}]
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let counter = {
|
||||
use leptos_use::{use_interval, UseIntervalReturn};
|
||||
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
||||
counter
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let now = move || {
|
||||
counter.get();
|
||||
chrono::Utc::now()
|
||||
};
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
let now = move || chrono::Utc::now();
|
||||
|
||||
view! {
|
||||
<aside class="beacons">
|
||||
<div class="sort-method">
|
||||
<p>"Sort beacons by:"</p>
|
||||
<p>"Group beacons by:"</p>
|
||||
<select
|
||||
name="beacon-sort"
|
||||
on:change:target=move |ev| {
|
||||
@@ -276,34 +533,112 @@ pub fn BeaconSidebar() -> impl IntoView {
|
||||
</div>
|
||||
|
||||
<div class="search">
|
||||
<p>"Search for beacon:"</p>
|
||||
<input bind:value=search_input name="beacon-search" placeholder="Search..." />
|
||||
</div>
|
||||
|
||||
<div class="checkin-filter">
|
||||
<p>"Filter by last checkin:"</p>
|
||||
<input bind:value=checkin_filter name="beacon-checkin-filter" placeholder="15m" />
|
||||
</div>
|
||||
|
||||
<Suspense fallback=|| view! { "Loading..." }>
|
||||
<div class="beacon-list">
|
||||
{move || partitions()
|
||||
.iter()
|
||||
.map(|partition| view! {
|
||||
<div class="beacon-partition">
|
||||
{partition.title.as_ref().map(|title| view! {
|
||||
<div class="partition-title">
|
||||
{title.clone()}
|
||||
</div>
|
||||
})}
|
||||
<For
|
||||
each=partitions
|
||||
key=|partition| partition.key.clone()
|
||||
let:partition
|
||||
>
|
||||
<div class="beacon-partition">
|
||||
{partition.title.as_ref().map(|title| view! {
|
||||
<div class="partition-title">
|
||||
{title.clone()}
|
||||
</div>
|
||||
})}
|
||||
|
||||
<For
|
||||
each={
|
||||
let beacons = Arc::clone(&partition.beacons);
|
||||
move || (beacons)()
|
||||
<For
|
||||
each={
|
||||
let beacons = Arc::clone(&partition.beacons);
|
||||
move || (beacons)()
|
||||
}
|
||||
key=|b| b.beacon_id.clone()
|
||||
let:beacon
|
||||
>
|
||||
<div class="beacon-instance">
|
||||
<div class="beacon-instance-id">
|
||||
{match &*beacon.nickname {
|
||||
"" => Either::Left(view! {
|
||||
<span>{beacon.beacon_id.clone()}</span>
|
||||
}),
|
||||
nick => {
|
||||
let nick = nick.to_string();
|
||||
Either::Right(view! {
|
||||
{nick}
|
||||
<span>"(" {beacon.beacon_id.clone()} ")"</span>
|
||||
})
|
||||
}
|
||||
}}
|
||||
</div>
|
||||
<div class="beacon-instance-checkin">
|
||||
<span>"Last checkin: "</span> {move || crate::users::format_delta(now() - beacon.last_checkin)}
|
||||
</div>
|
||||
<div class="beacon-instance-peer-ip">
|
||||
<span>"Peer IP: "</span> {beacon.ip.clone()}
|
||||
</div>
|
||||
<div class="beacon-instance-os">
|
||||
<span>"OS: "</span> {beacon.operating_system.clone()}
|
||||
</div>
|
||||
{(sort_method.get() != Some(SortMethod::Category))
|
||||
.then(|| -> Vec<String> {
|
||||
let BeaconResources {
|
||||
categories,
|
||||
..
|
||||
} = expect_context::<BeaconResources>();
|
||||
|
||||
let Some(Ok(ref categories)) = *categories.read() else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
categories
|
||||
.iter()
|
||||
.filter(|cat| beacon.category_ids.contains(&cat.category_id))
|
||||
.map(|cat| cat.category_name.clone())
|
||||
.collect()
|
||||
})
|
||||
.filter(|categories| !categories.is_empty())
|
||||
.map(|categories| view! {
|
||||
<div class="beacon-instance-categories">
|
||||
<span>"Categories: "</span> {categories.join(", ")}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
key=|b| b.beacon_id.clone()
|
||||
let:beacon
|
||||
>
|
||||
<div>{beacon.beacon_id.clone()}</div>
|
||||
</For>
|
||||
</div>
|
||||
})
|
||||
.collect_view()}
|
||||
{(sort_method.get() != Some(SortMethod::Template))
|
||||
.then(|| -> Option<String> {
|
||||
let BeaconResources {
|
||||
templates,
|
||||
..
|
||||
} = expect_context::<BeaconResources>();
|
||||
|
||||
let Some(Ok(ref templates)) = *templates.read() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
templates
|
||||
.iter()
|
||||
.find(|t| t.template_id == beacon.template_id)
|
||||
.map(|t| t.template_name.clone())
|
||||
})
|
||||
.flatten()
|
||||
.map(|template_name| view! {
|
||||
<div class="beacon-instance-template">
|
||||
<span>"Template: "</span> {template_name}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
</For>
|
||||
</div>
|
||||
</Suspense>
|
||||
</aside>
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn format_delta(time: chrono::TimeDelta) -> String {
|
||||
}
|
||||
3600..=86399 => {
|
||||
let hours = seconds / 3600;
|
||||
format!("{} hours{} ago", hours, if hours == 1 { "" } else { "s" })
|
||||
format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" })
|
||||
}
|
||||
_ => {
|
||||
let days = seconds / 86400;
|
||||
@@ -96,9 +96,10 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr
|
||||
|
||||
#[component]
|
||||
pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView {
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
use leptos_use::{use_interval, UseIntervalReturn};
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
||||
#[cfg_attr(not(feature = "hydrate"), allow(unused_variables))]
|
||||
let (time_ago, set_time_ago) = signal(
|
||||
@@ -106,7 +107,7 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into
|
||||
.map(|active| format_delta(Utc::now() - active)),
|
||||
);
|
||||
|
||||
#[cfg(feature = "hydrate")]
|
||||
#[cfg(not(feature = "ssr"))]
|
||||
Effect::watch(
|
||||
move || counter.get(),
|
||||
move |_, _, _| {
|
||||
|
||||
Reference in New Issue
Block a user