feat: made beacon search and list better

This commit is contained in:
Andrew Rioux 2025-02-22 20:00:28 -05:00
parent faaa4d2d1a
commit f284cf47eb
Signed by: andrew.rioux
GPG Key ID: 9B8BAC47C17ABB94
7 changed files with 497 additions and 70 deletions

40
Cargo.lock generated
View File

@ -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",
] ]

View File

@ -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"]

View File

@ -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 = []

View File

@ -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,34 +533,112 @@ 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()
<div class="beacon-partition"> let:partition
{partition.title.as_ref().map(|title| view! { >
<div class="partition-title"> <div class="beacon-partition">
{title.clone()} {partition.title.as_ref().map(|title| view! {
</div> <div class="partition-title">
})} {title.clone()}
</div>
})}
<For <For
each={ each={
let beacons = Arc::clone(&partition.beacons); let beacons = Arc::clone(&partition.beacons);
move || (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() {(sort_method.get() != Some(SortMethod::Template))
let:beacon .then(|| -> Option<String> {
> let BeaconResources {
<div>{beacon.beacon_id.clone()}</div> templates,
</For> ..
</div> } = expect_context::<BeaconResources>();
})
.collect_view()} 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>

View File

@ -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 |_, _, _| {

View File

@ -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 {
}
} }

View File

@ -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;