feat: made beacon search and list better
This commit is contained in:
parent
faaa4d2d1a
commit
f284cf47eb
40
Cargo.lock
generated
40
Cargo.lock
generated
@ -95,6 +95,12 @@ version = "1.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "asn1-rs"
|
name = "asn1-rs"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -725,13 +731,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cron"
|
name = "cron"
|
||||||
version = "0.15.0"
|
version = "0.13.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5877d3fbf742507b66bc2a1945106bd30dd8504019d596901ddd012a4dd01740"
|
checksum = "eee8b2b4516038bc0f1d3c9934bcb4a13dd316e04abbc63c96757a6d75978532"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"nom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"winnow 0.6.26",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -938,6 +944,18 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
|
checksum = "669a445ee724c5c69b1b06fe0b63e70a1c84bc9bb7d9696cd4f4e3ec45050408"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "duration-str"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99b55e40ba8fc1ef074c9f9031b4cb88bb1f30c946f80a9305df44973c0b9a2d"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"rust_decimal",
|
||||||
|
"thiserror 2.0.11",
|
||||||
|
"winnow 0.6.8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dyn-clone"
|
name = "dyn-clone"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
@ -2984,6 +3002,16 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust_decimal"
|
||||||
|
version = "1.36.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.24"
|
version = "0.1.24"
|
||||||
@ -3497,6 +3525,7 @@ dependencies = [
|
|||||||
"codee 0.2.0",
|
"codee 0.2.0",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"cron",
|
"cron",
|
||||||
|
"duration-str",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
@ -3509,6 +3538,7 @@ dependencies = [
|
|||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
|
"regex",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
@ -5026,9 +5056,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.6.26"
|
version = "0.6.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
|
checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -23,15 +23,15 @@ simple_logger = "5.0.0"
|
|||||||
http = "1.2.0"
|
http = "1.2.0"
|
||||||
bytes = "1.10.0"
|
bytes = "1.10.0"
|
||||||
http-body-util = "0.1.2"
|
http-body-util = "0.1.2"
|
||||||
|
|
||||||
pcap-sys = { version = "0.1.0", path = "../pcap-sys" }
|
|
||||||
sparse-actions = { version = "2.0.0", path = "../sparse-actions" }
|
|
||||||
packets = { version = "0.1.0", path = "../packets" }
|
|
||||||
serde_json = "1.0.139"
|
serde_json = "1.0.139"
|
||||||
serde = "1.0.217"
|
serde = "1.0.217"
|
||||||
http-body = "1.0.1"
|
http-body = "1.0.1"
|
||||||
rmp-serde = "1.3.0"
|
rmp-serde = "1.3.0"
|
||||||
cron = "0.15.0"
|
cron = "0.13.0"
|
||||||
|
|
||||||
|
pcap-sys = { version = "0.1.0", path = "../pcap-sys" }
|
||||||
|
sparse-actions = { version = "2.0.0", path = "../sparse-actions" }
|
||||||
|
packets = { version = "0.1.0", path = "../packets" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
openssl = ["dep:rustls-openssl"]
|
openssl = ["dep:rustls-openssl"]
|
||||||
|
|||||||
@ -40,14 +40,16 @@ hex = { version = "0.4", optional = true }
|
|||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"], optional = true }
|
rcgen = { version = "0.13.2", features = ["pem", "x509-parser", "crypto"], optional = true }
|
||||||
cron = { version = "0.15.0", optional = true }
|
cron = { version = "0.13.0", optional = true }
|
||||||
rustls-pki-types = { version = "1.7", optional = true }
|
rustls-pki-types = { version = "1.7", optional = true }
|
||||||
rand = { version = "0.9", optional = true }
|
rand = { version = "0.9", optional = true }
|
||||||
|
serde_json = "1.0.139"
|
||||||
|
send_wrapper = "0.6.0"
|
||||||
|
duration-str = { version = "0.13.0", default-features = false, features = ["chrono"] }
|
||||||
|
regex = "1.11.1"
|
||||||
|
|
||||||
sparse-actions = { path = "../sparse-actions", optional = true }
|
sparse-actions = { path = "../sparse-actions", optional = true }
|
||||||
sparse-handler = { path = "../sparse-handler", optional = true }
|
sparse-handler = { path = "../sparse-handler", optional = true }
|
||||||
serde_json = "1.0.139"
|
|
||||||
send_wrapper = "0.6.0"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
embed-beacons = []
|
embed-beacons = []
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use leptos::prelude::*;
|
use leptos::{either::Either, prelude::*};
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use leptos_use::{use_websocket, UseWebSocketReturn};
|
use leptos_use::use_websocket;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::beacons::BeaconResources;
|
use crate::beacons::BeaconResources;
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
use super::templates::BeaconTemplate;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
enum SortMethod {
|
enum SortMethod {
|
||||||
Listener,
|
Listener,
|
||||||
Config,
|
Config,
|
||||||
@ -69,11 +66,13 @@ unsafe impl Send for SidebarEvents {}
|
|||||||
pub enum SidebarEvents {
|
pub enum SidebarEvents {
|
||||||
BeaconList(Vec<CurrentBeaconInstance>),
|
BeaconList(Vec<CurrentBeaconInstance>),
|
||||||
NewBeacon(CurrentBeaconInstance),
|
NewBeacon(CurrentBeaconInstance),
|
||||||
|
BeaconUpdate(String, CurrentBeaconInstance),
|
||||||
Checkin(String),
|
Checkin(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn BeaconSidebar() -> impl IntoView {
|
pub fn BeaconSidebar() -> impl IntoView {
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
let BeaconResources {
|
let BeaconResources {
|
||||||
listeners,
|
listeners,
|
||||||
templates,
|
templates,
|
||||||
@ -82,16 +81,17 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
..
|
..
|
||||||
} = expect_context::<BeaconResources>();
|
} = expect_context::<BeaconResources>();
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "ssr", allow(unused_variables))]
|
||||||
let current_beacons = RwSignal::new(None::<Vec<CurrentBeaconInstance>>);
|
let current_beacons = RwSignal::new(None::<Vec<CurrentBeaconInstance>>);
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(not(feature = "ssr"))]
|
||||||
let (web_socket, rebuild_websocket) = signal(use_websocket::<
|
let (web_socket, rebuild_websocket) = signal(use_websocket::<
|
||||||
(),
|
(),
|
||||||
SidebarEvents,
|
SidebarEvents,
|
||||||
codee::string::JsonSerdeCodec,
|
codee::string::JsonSerdeCodec,
|
||||||
>("/api/subscribe/listener"));
|
>("/api/subscribe/listener"));
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(not(feature = "ssr"))]
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
web_socket.with(move |uwsr| {
|
web_socket.with(move |uwsr| {
|
||||||
uwsr.message.with(move |message| {
|
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| {
|
SidebarEvents::Checkin(bid) => current_beacons.update(|bs| {
|
||||||
let Some(ref mut bs) = bs else {
|
let Some(ref mut bs) = bs else {
|
||||||
return;
|
return;
|
||||||
@ -125,7 +133,7 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(not(feature = "ssr"))]
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
|
let user = expect_context::<ReadSignal<Option<crate::users::User>>>();
|
||||||
user.with(move |_| {
|
user.with(move |_| {
|
||||||
@ -139,9 +147,11 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
|
|
||||||
let (sort_method, set_sort_method) = signal(None::<SortMethod>);
|
let (sort_method, set_sort_method) = signal(None::<SortMethod>);
|
||||||
let search_input = RwSignal::new("".to_string());
|
let search_input = RwSignal::new("".to_string());
|
||||||
|
let checkin_filter = RwSignal::new("".to_string());
|
||||||
|
|
||||||
struct BeaconPartition {
|
struct BeaconPartition {
|
||||||
title: Option<String>,
|
title: Option<String>,
|
||||||
|
key: String,
|
||||||
beacons: Arc<dyn Fn() -> Vec<CurrentBeaconInstance> + Send + Sync>,
|
beacons: Arc<dyn Fn() -> Vec<CurrentBeaconInstance> + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,38 +161,103 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
|
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
let partitions = move || -> Vec<BeaconPartition> {
|
let partitions = move || -> Vec<BeaconPartition> {
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
leptos::logging::log!(
|
leptos::logging::log!(
|
||||||
"There are {:?} beacons",
|
"There are {:?} beacons",
|
||||||
current_beacons.read().as_ref().map(Vec::len)
|
current_beacons.read().as_ref().map(Vec::len)
|
||||||
);
|
);
|
||||||
let sm = sort_method.read();
|
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 {
|
let partition_filter = {
|
||||||
// return vec![];
|
let str_match_search = str_match_search.clone();
|
||||||
//};
|
move |partition: &BeaconPartition| {
|
||||||
//let Some(Ok(ref templates)) = *templates.read() else {
|
partition
|
||||||
// return vec![];
|
.title
|
||||||
//};
|
.as_deref()
|
||||||
//let Some(Ok(ref categories)) = *categories.read() else {
|
.map(|title| (str_match_search)(&title))
|
||||||
// return vec![];
|
.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 {
|
match *sm {
|
||||||
Some(SortMethod::Config) => {
|
Some(SortMethod::Config) => {
|
||||||
let Some(Ok(ref configs)) = *configs.read() else {
|
let Some(Ok(ref configs_super)) = *configs.read() else {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
|
|
||||||
configs
|
configs_super
|
||||||
.iter()
|
.iter()
|
||||||
.map(|config| {
|
.map(|config| {
|
||||||
let config = config.clone();
|
let config = config.clone();
|
||||||
BeaconPartition {
|
BeaconPartition {
|
||||||
title: Some(config.config_name.clone()),
|
title: Some(config.config_name.clone()),
|
||||||
|
key: format!("config-{}", &config.config_id),
|
||||||
beacons: Arc::new(move || {
|
beacons: Arc::new(move || {
|
||||||
let Some(Ok(ref templates)) = *templates.read() else {
|
let Some(Ok(ref templates)) = *templates.read() else {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
|
let Some(Ok(ref categories)) = *categories.read() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
current_beacons
|
current_beacons
|
||||||
.get()
|
.get()
|
||||||
.unwrap_or(vec![])
|
.unwrap_or(vec![])
|
||||||
@ -194,28 +269,42 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
.map(|t| t.config_id))
|
.map(|t| t.config_id))
|
||||||
== Some(config.config_id)
|
== Some(config.config_id)
|
||||||
})
|
})
|
||||||
|
.filter(|beacon| {
|
||||||
|
beacon_filter!(
|
||||||
|
Some(config.config_name.clone()),
|
||||||
|
search_input,
|
||||||
|
beacon,
|
||||||
|
templates,
|
||||||
|
categories
|
||||||
|
)
|
||||||
|
})
|
||||||
.map(Clone::clone)
|
.map(Clone::clone)
|
||||||
.collect()
|
.collect()
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.filter(partition_filter)
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
Some(SortMethod::Listener) => {
|
Some(SortMethod::Listener) => {
|
||||||
let Some(Ok(ref listeners)) = *listeners.read() else {
|
let Some(Ok(ref listeners_super)) = *listeners.read() else {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
|
|
||||||
listeners
|
listeners_super
|
||||||
.iter()
|
.iter()
|
||||||
.map(|listener| {
|
.map(|listener| {
|
||||||
let listener = listener.clone();
|
let listener = listener.clone();
|
||||||
BeaconPartition {
|
BeaconPartition {
|
||||||
title: Some(listener.domain_name.clone()),
|
title: Some(listener.domain_name.clone()),
|
||||||
|
key: format!("template-{}", &listener.listener_id),
|
||||||
beacons: Arc::new(move || {
|
beacons: Arc::new(move || {
|
||||||
let Some(Ok(ref templates)) = *templates.read() else {
|
let Some(Ok(ref templates)) = *templates.read() else {
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
|
let Some(Ok(ref categories)) = *categories.read() else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
|
||||||
current_beacons
|
current_beacons
|
||||||
.get()
|
.get()
|
||||||
@ -228,16 +317,167 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
.map(|t| t.listener_id == listener.listener_id)
|
.map(|t| t.listener_id == listener.listener_id)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
})
|
})
|
||||||
|
.filter(|beacon| {
|
||||||
|
beacon_filter!(
|
||||||
|
Some(listener.domain_name.clone()),
|
||||||
|
search_input,
|
||||||
|
beacon,
|
||||||
|
templates,
|
||||||
|
categories
|
||||||
|
)
|
||||||
|
})
|
||||||
.map(Clone::clone)
|
.map(Clone::clone)
|
||||||
.collect()
|
.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()
|
.collect()
|
||||||
}
|
}
|
||||||
_ => vec![BeaconPartition {
|
_ => vec![BeaconPartition {
|
||||||
title: None,
|
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 = || {
|
let partitions = || {
|
||||||
vec![BeaconPartition {
|
vec![BeaconPartition {
|
||||||
title: None,
|
title: None,
|
||||||
|
key: "unsorted".to_string(),
|
||||||
beacons: Arc::new(move || vec![]),
|
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! {
|
view! {
|
||||||
<aside class="beacons">
|
<aside class="beacons">
|
||||||
<div class="sort-method">
|
<div class="sort-method">
|
||||||
<p>"Sort beacons by:"</p>
|
<p>"Group beacons by:"</p>
|
||||||
<select
|
<select
|
||||||
name="beacon-sort"
|
name="beacon-sort"
|
||||||
on:change:target=move |ev| {
|
on:change:target=move |ev| {
|
||||||
@ -276,14 +533,22 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search">
|
<div class="search">
|
||||||
|
<p>"Search for beacon:"</p>
|
||||||
<input bind:value=search_input name="beacon-search" placeholder="Search..." />
|
<input bind:value=search_input name="beacon-search" placeholder="Search..." />
|
||||||
</div>
|
</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..." }>
|
<Suspense fallback=|| view! { "Loading..." }>
|
||||||
<div class="beacon-list">
|
<div class="beacon-list">
|
||||||
{move || partitions()
|
<For
|
||||||
.iter()
|
each=partitions
|
||||||
.map(|partition| view! {
|
key=|partition| partition.key.clone()
|
||||||
|
let:partition
|
||||||
|
>
|
||||||
<div class="beacon-partition">
|
<div class="beacon-partition">
|
||||||
{partition.title.as_ref().map(|title| view! {
|
{partition.title.as_ref().map(|title| view! {
|
||||||
<div class="partition-title">
|
<div class="partition-title">
|
||||||
@ -299,11 +564,81 @@ pub fn BeaconSidebar() -> impl IntoView {
|
|||||||
key=|b| b.beacon_id.clone()
|
key=|b| b.beacon_id.clone()
|
||||||
let:beacon
|
let:beacon
|
||||||
>
|
>
|
||||||
<div>{beacon.beacon_id.clone()}</div>
|
<div class="beacon-instance">
|
||||||
</For>
|
<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>
|
</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>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ pub fn format_delta(time: chrono::TimeDelta) -> String {
|
|||||||
}
|
}
|
||||||
3600..=86399 => {
|
3600..=86399 => {
|
||||||
let hours = seconds / 3600;
|
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;
|
let days = seconds / 86400;
|
||||||
@ -96,9 +96,10 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView {
|
pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl IntoView {
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
use leptos_use::{use_interval, UseIntervalReturn};
|
use leptos_use::{use_interval, UseIntervalReturn};
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(not(feature = "ssr"))]
|
||||||
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
let UseIntervalReturn { counter, .. } = use_interval(1000);
|
||||||
#[cfg_attr(not(feature = "hydrate"), allow(unused_variables))]
|
#[cfg_attr(not(feature = "hydrate"), allow(unused_variables))]
|
||||||
let (time_ago, set_time_ago) = signal(
|
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)),
|
.map(|active| format_delta(Utc::now() - active)),
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(not(feature = "ssr"))]
|
||||||
Effect::watch(
|
Effect::watch(
|
||||||
move || counter.get(),
|
move || counter.get(),
|
||||||
move |_, _, _| {
|
move |_, _, _| {
|
||||||
|
|||||||
@ -3,4 +3,70 @@ aside.beacons {
|
|||||||
background-color: #11111c;
|
background-color: #11111c;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-method, .search, .checkin-filter {
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, input {
|
||||||
|
display: inline-block;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkin-filter {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #2e2e59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beacon-list {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beacon-partition {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.partition-title {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background-color: #11111c;
|
||||||
|
border-bottom: 1px solid #2e2e59;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beacon-instance {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beacon-instance span {
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beacon-instance {
|
||||||
|
border-bottom: 1px solid #2e2e59;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-match {
|
||||||
|
color: yellow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.beacon-instance-id {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,7 +20,7 @@ html, body {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 350px 1fr;
|
grid-template-columns: 450px 1fr;
|
||||||
grid-template-rows: 79px 1fr;
|
grid-template-rows: 79px 1fr;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"nav nav"
|
"nav nav"
|
||||||
@ -53,13 +53,6 @@ nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aside.beacons {
|
|
||||||
grid-area: beacons;
|
|
||||||
background-color: #11111c;
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
main {
|
||||||
grid-area: main;
|
grid-area: main;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user