diff --git a/Cargo.lock b/Cargo.lock index 6fc8ba2..abd5a28 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -235,9 +235,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "ed2490600f404f2b94c167e31d3ed1d5f3c225a0f3b80230053b3e0b7b962bd9" [[package]] name = "byteorder" @@ -357,9 +357,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" dependencies = [ "android-tzdata", "iana-time-zone", @@ -885,10 +885,13 @@ dependencies = [ "chart-js-rs", "console_error_panic_hook", "console_log", + "humantime", "leptos", "leptos-use", "leptos_router", "log", + "strum", + "ubyte", ] [[package]] @@ -1564,6 +1567,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -3423,6 +3432,28 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.48", +] + [[package]] name = "syn" version = "1.0.109" @@ -4074,6 +4105,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4644,9 +4681,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.34" +version = "0.5.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d" dependencies = [ "memchr", ] diff --git a/crates/web-ui/Cargo.toml b/crates/web-ui/Cargo.toml index 0fda809..cffa8e2 100644 --- a/crates/web-ui/Cargo.toml +++ b/crates/web-ui/Cargo.toml @@ -7,14 +7,16 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chart-js-rs = { version = "0.0.13" , git = "https://github.com/DDtKey/chart-js-rs.git", branch = "support-chart-updates"} console_error_panic_hook = { version = "0.1.7", optional = true } console_log = { version = "1", optional = true } -log = { version = "0.4"} +humantime = { version = "2.1.0" } leptos = { version = "0.5.7", features = ["csr"] } -leptos_router = { version = "0.5.7", features = ["csr"] } leptos-use = { version = "0.9.0", git = "https://github.com/synphonyte/leptos-use", rev = "67fa44a5acc8a228350820fd612e2d25a7fb64a8" } -chart-js-rs = { version = "0.0.13" , git = "https://github.com/DDtKey/chart-js-rs.git", branch = "support-chart-updates"} - +leptos_router = { version = "0.5.7", features = ["csr"] } +log = { version = "0.4"} +ubyte = { version = "0.10.4" } +strum = { version = "0.25.0", features = ["derive"] } [features] default = [] diff --git a/crates/web-ui/src/components/buckets_chart.rs b/crates/web-ui/src/components/buckets_chart.rs index 98f08fd..c205571 100644 --- a/crates/web-ui/src/components/buckets_chart.rs +++ b/crates/web-ui/src/components/buckets_chart.rs @@ -1,4 +1,4 @@ -use crate::types::Buckets; +use crate::types::buckets::Buckets; use chart_js_rs::scatter::Scatter; use chart_js_rs::{ ChartOptions, ChartScale, Dataset, DatasetDataExt, NoAnnotations, XYDataset, XYPoint, diff --git a/crates/web-ui/src/components/buckets_table.rs b/crates/web-ui/src/components/buckets_table.rs index 072df0c..2975211 100644 --- a/crates/web-ui/src/components/buckets_table.rs +++ b/crates/web-ui/src/components/buckets_table.rs @@ -1,16 +1,33 @@ -use crate::types::Buckets; -use leptos::{component, view, CollectView, IntoView, ReadSignal, SignalWith}; +use crate::types::buckets::Buckets; +use crate::types::units::Unit; +use leptos::{ + component, view, CollectView, IntoView, ReadSignal, SignalGet, SignalGetUntracked, SignalWith, +}; use leptos_use::{use_intl_number_format, Notation, UseIntlNumberFormatOptions}; +use std::time::Duration; +use ubyte::ToByteUnit; #[component] -pub(crate) fn BucketsTable(buckets: ReadSignal) -> impl IntoView { - let rounded_float_format = use_intl_number_format( +pub(crate) fn BucketsTable(buckets: ReadSignal, unit: ReadSignal) -> impl IntoView { + let intl_float_format = use_intl_number_format( UseIntlNumberFormatOptions::default() .notation(Notation::Compact) .minimum_fraction_digits(1) .maximum_fraction_digits(3), ); + let rounded_float_format = move |val: f64| { + if val.is_infinite() { + return "∞".to_string(); + } + + match unit.get() { + Unit::Number => intl_float_format.format(val).get_untracked(), + Unit::Bytes => (val.round() as u64).bytes().to_string(), + Unit::Seconds => humantime::format_duration(Duration::from_secs_f64(val)).to_string(), + } + }; + let buckets_view = move || { buckets.with(|buckets| { buckets @@ -19,7 +36,7 @@ pub(crate) fn BucketsTable(buckets: ReadSignal) -> impl IntoView { view! { {bucket.number()} - {rounded_float_format.format(bucket.le())} + {rounded_float_format(bucket.le())} } }) diff --git a/crates/web-ui/src/components/input/mod.rs b/crates/web-ui/src/components/input/mod.rs new file mode 100644 index 0000000..49cb401 --- /dev/null +++ b/crates/web-ui/src/components/input/mod.rs @@ -0,0 +1,5 @@ +mod number_input; +mod select_input; + +pub(crate) use number_input::NumberInput; +pub(crate) use select_input::{SelectInput, SelectOption}; diff --git a/crates/web-ui/src/components/input.rs b/crates/web-ui/src/components/input/number_input.rs similarity index 96% rename from crates/web-ui/src/components/input.rs rename to crates/web-ui/src/components/input/number_input.rs index 0052a04..41dcd7e 100644 --- a/crates/web-ui/src/components/input.rs +++ b/crates/web-ui/src/components/input/number_input.rs @@ -33,7 +33,7 @@ where .and_then(|v| T::from_str(&v).map_err(|err| err.to_string())) .and_then(|value| validate_value(value, min, max)); - log::debug!("parsed result of `on_change`: {:?}", result); + log::debug!("[number-input] parsed result of `on_change`: {:?}", result); match result { Ok(new_value) => { diff --git a/crates/web-ui/src/components/input/select_input.rs b/crates/web-ui/src/components/input/select_input.rs new file mode 100644 index 0000000..04effc2 --- /dev/null +++ b/crates/web-ui/src/components/input/select_input.rs @@ -0,0 +1,58 @@ +use leptos::ev::Event; +use leptos::{ + component, event_target_value, view, Callable, Callback, CollectView, IntoView, ReadSignal, + SignalSet, SignalWith, WriteSignal, +}; +use std::fmt::Display; +use std::str::FromStr; + +pub(crate) trait SelectOption { + fn label(&self) -> &'static str; +} + +#[component] +pub(crate) fn SelectInput( + #[prop(into)] get: ReadSignal, + #[prop(into)] set: WriteSignal, + #[prop(into)] options: Vec, + #[prop(into, optional)] on_change: Option>, + #[prop(into)] label: String, +) -> impl IntoView +where + T: SelectOption + Display + PartialEq + FromStr + Copy + 'static, +{ + let on_change = move |ev| { + let Ok(new_value) = event_target_value(&ev).parse::() else { + // impossible until somebody changes selector in their browser or wrong impl of `T: FromStr` + log::debug!("[select-input] failed to parse value"); + return; + }; + log::debug!("[select-input] new value is: {new_value}"); + + set.set(new_value); + if let Some(callback) = on_change { + callback.call(ev); + } + }; + + let options_view = options + .into_iter() + .map(|option| { + let is_selected = move || get.with(|v| v == &option); + view! { + + } + }) + .collect_view(); + + view! { + + } +} diff --git a/crates/web-ui/src/pages/buckets_explorer_page.rs b/crates/web-ui/src/pages/buckets_explorer_page.rs index 2c5d7fd..d6fc4f1 100644 --- a/crates/web-ui/src/pages/buckets_explorer_page.rs +++ b/crates/web-ui/src/pages/buckets_explorer_page.rs @@ -1,10 +1,12 @@ use crate::components::buckets_chart::BucketsChart; use crate::components::buckets_table::BucketsTable; -use crate::components::input::NumberInput; -use crate::types::Buckets; +use crate::components::input::{NumberInput, SelectInput, SelectOption}; +use crate::types::buckets::Buckets; +use crate::types::units::Unit; use leptos::{component, create_signal, view, IntoView, SignalGet, SignalGetUntracked, SignalSet}; use leptos_router::use_query; use leptos_router::Params; +use strum::IntoEnumIterator; const DEFAULT_INITIAL_VALUE: f64 = 1.0; const DEFAULT_FACTOR: f64 = 1.5; @@ -31,6 +33,7 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView { let (factor, set_factor) = create_signal(query.factor.unwrap_or(DEFAULT_FACTOR)); let (buckets_num, set_buckets_num) = create_signal(query.buckets_num.unwrap_or(DEFAULT_BUCKETS_NUM)); + let (unit, set_unit) = create_signal(Unit::Number); let (buckets, set_buckets) = create_signal(Buckets::calculate( initial_value.get_untracked(), @@ -46,6 +49,8 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView { )) }; + let options: Vec<_> = Unit::iter().collect(); + view! {

Exponential Buckets Explorer

@@ -67,6 +72,12 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView { get=buckets_num set=set_buckets_num on_change=update_buckets_callback label="Number of buckets" min=1_u32 max=100_u32 />
+
+ +
@@ -74,10 +85,10 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView {

Buckets

- +
{/* Chart */} -
+

Chart

@@ -90,3 +101,13 @@ pub(crate) fn BucketsExplorerPage() -> impl IntoView { } } + +impl SelectOption for Unit { + fn label(&self) -> &'static str { + match self { + Unit::Number => "Number", + Unit::Bytes => "Bytes", + Unit::Seconds => "Seconds", + } + } +} diff --git a/crates/web-ui/src/types/buckets.rs b/crates/web-ui/src/types/buckets.rs new file mode 100644 index 0000000..8450e77 --- /dev/null +++ b/crates/web-ui/src/types/buckets.rs @@ -0,0 +1,64 @@ +use std::ops::Deref; + +#[derive(Debug, Clone)] +pub(crate) struct Buckets(Vec); + +#[derive(Debug, Clone)] +pub(crate) struct Bucket { + number: u32, + le: f64, +} + +impl Buckets { + pub(crate) fn calculate(initial_value: f64, factor: f64, num_of_buckets: u32) -> Self { + // Include last bucket, which is open ended + // TODO: use bounded integer type to restrict passing too large values. + // Overflow isn't possible due to validations, but this api is isolated + let num_of_buckets = num_of_buckets + .checked_add(1) + .expect("number of buckets - overflow"); + let mut buckets = Vec::with_capacity(num_of_buckets as usize); + let mut next_value = initial_value; + + // starting from second bucket, since first one is already added + for bucket_num in 1..num_of_buckets { + buckets.push(Bucket::new(bucket_num, next_value)); + next_value *= factor; + } + // last bucket is open ended + buckets.push(Bucket::new(num_of_buckets, f64::INFINITY)); + + Self(buckets) + } +} + +impl Bucket { + fn new(number: u32, le: f64) -> Self { + Self { number, le } + } + + pub fn number(&self) -> u32 { + self.number + } + + pub fn le(&self) -> f64 { + self.le + } +} + +impl Deref for Buckets { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl IntoIterator for Buckets { + type Item = Bucket; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/crates/web-ui/src/types/mod.rs b/crates/web-ui/src/types/mod.rs index 8450e77..6816842 100644 --- a/crates/web-ui/src/types/mod.rs +++ b/crates/web-ui/src/types/mod.rs @@ -1,64 +1,2 @@ -use std::ops::Deref; - -#[derive(Debug, Clone)] -pub(crate) struct Buckets(Vec); - -#[derive(Debug, Clone)] -pub(crate) struct Bucket { - number: u32, - le: f64, -} - -impl Buckets { - pub(crate) fn calculate(initial_value: f64, factor: f64, num_of_buckets: u32) -> Self { - // Include last bucket, which is open ended - // TODO: use bounded integer type to restrict passing too large values. - // Overflow isn't possible due to validations, but this api is isolated - let num_of_buckets = num_of_buckets - .checked_add(1) - .expect("number of buckets - overflow"); - let mut buckets = Vec::with_capacity(num_of_buckets as usize); - let mut next_value = initial_value; - - // starting from second bucket, since first one is already added - for bucket_num in 1..num_of_buckets { - buckets.push(Bucket::new(bucket_num, next_value)); - next_value *= factor; - } - // last bucket is open ended - buckets.push(Bucket::new(num_of_buckets, f64::INFINITY)); - - Self(buckets) - } -} - -impl Bucket { - fn new(number: u32, le: f64) -> Self { - Self { number, le } - } - - pub fn number(&self) -> u32 { - self.number - } - - pub fn le(&self) -> f64 { - self.le - } -} - -impl Deref for Buckets { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl IntoIterator for Buckets { - type Item = Bucket; - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} +pub(crate) mod buckets; +pub(crate) mod units; diff --git a/crates/web-ui/src/types/units.rs b/crates/web-ui/src/types/units.rs new file mode 100644 index 0000000..d4d74bf --- /dev/null +++ b/crates/web-ui/src/types/units.rs @@ -0,0 +1,9 @@ +use strum::{EnumIter, EnumString}; + +#[derive(strum::Display, EnumString, EnumIter, Debug, Clone, Copy, PartialEq, Eq)] +#[strum(serialize_all = "snake_case")] +pub(crate) enum Unit { + Number, + Bytes, + Seconds, +}