use std::rc::Rc; use gpui::{ App, ElementId, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, }; use jiff::Timestamp; use crate::{ buttons::{Button, ButtonVariant, IconButton}, forms::{Choice, Select}, primitives::IconName, theme::{ActiveTheme, TextSize}, typography::format, }; /// How often a refresh may come, in seconds, with its words; none is off. const EVERY: [(u32, &str); 7] = [ (0, "Off"), (5, "5s"), (10, "10s"), (30, "30s"), (60, "1m"), (300, "5m"), (900, "15m"), ]; type OnInterval = Rc, &mut Window, &mut App)>; type Run = Rc; /// How often a dashboard refreshes, from off to every fifteen minutes, beside a button that refreshes it now and when it last did. The owner runs the timer. #[derive(IntoElement)] pub struct RefreshIntervalSelector { id: ElementId, every: Option, last: Timestamp, now: Timestamp, on_change: OnInterval, on_refresh: Run, } impl RefreshIntervalSelector { /// `every` is in seconds, one the selector offers, or none for off; `last` is when it last refreshed. pub fn new( id: impl Into, every: Option, last: Timestamp, now: Timestamp, on_change: impl Fn(Option, &mut Window, &mut App) + 'static, on_refresh: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { if let Some(every) = every { assert!( EVERY .iter() .any(|(seconds, _)| *seconds == every && every > 0), "no refresh every {every}s" ); } Self { id: id.into(), every, last, now, on_change: Rc::new(on_change), on_refresh: Rc::new(on_refresh), } } } /// The seconds a choice of the selector names; its 0 is Off. pub(crate) fn interval(value: &str) -> Option { let seconds: u32 = value.parse().expect("an interval the select offers"); (seconds > 0).then_some(seconds) } impl RenderOnce for RefreshIntervalSelector { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let id = self.id; let on_change = self.on_change; let on_refresh = self.on_refresh; div() .flex() .flex_wrap() .items_center() .gap_2() .child( IconButton::new((id.clone(), "now"), IconName::RefreshCw) .tooltip("Refresh now") .on_click(move |_, window, cx| { log::info!("refresh: now"); on_refresh(window, cx); }), ) .child( div().w(theme.label_width()).child( Select::new( (id, "every"), EVERY .iter() .map(|(seconds, words)| Choice::new(seconds.to_string(), *words)), ) .selected(self.every.unwrap_or(0).to_string()) .on_change(move |value, window, cx| { let every = interval(value); log::info!("refresh: every {every:?}"); on_change(every, window, cx); }), ), ) .child( div() .text_size(theme.text_size(TextSize::Xs)) .text_color(theme.colors.fg_subtle) .child(format!("Updated {}", format::relative(self.last, self.now))), ) } } /// A window of time a dashboard shows, back from now. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TimeWindow { Minutes15, Hour, Hours6, Day, Week, Month, } impl TimeWindow { pub const ALL: [TimeWindow; 6] = [ TimeWindow::Minutes15, TimeWindow::Hour, TimeWindow::Hours6, TimeWindow::Day, TimeWindow::Week, TimeWindow::Month, ]; pub fn words(self) -> &'static str { match self { TimeWindow::Minutes15 => "Last 15 minutes", TimeWindow::Hour => "Last hour", TimeWindow::Hours6 => "Last 6 hours", TimeWindow::Day => "Last 24 hours", TimeWindow::Week => "Last 7 days", TimeWindow::Month => "Last 30 days", } } /// How far back it reaches, in seconds. pub fn seconds(self) -> i64 { match self { TimeWindow::Minutes15 => 15 * 60, TimeWindow::Hour => 3_600, TimeWindow::Hours6 => 6 * 3_600, TimeWindow::Day => 86_400, TimeWindow::Week => 7 * 86_400, TimeWindow::Month => 30 * 86_400, } } } /// A filter the bar offers: its key, its name, its choices, and the one picked, if any. #[derive(Clone, Debug, PartialEq)] pub struct DashboardFilter { pub key: SharedString, pub name: SharedString, pub choices: Vec, pub picked: Option, } type OnWindow = Rc; type OnFilter = Rc, &mut Window, &mut App)>; /// A dashboard's filters in a row that wraps: a window of time, a select for each filter showing its name until one is picked, and Clear while any is picked. #[derive(IntoElement)] pub struct DashboardFilterBar { id: ElementId, window: TimeWindow, filters: Vec, on_window: OnWindow, on_filter: OnFilter, } impl DashboardFilterBar { pub fn new( id: impl Into, window: TimeWindow, filters: impl IntoIterator, on_window: impl Fn(TimeWindow, &mut Window, &mut App) + 'static, on_filter: impl Fn(&SharedString, Option, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), window, filters: filters.into_iter().collect(), on_window: Rc::new(on_window), on_filter: Rc::new(on_filter), } } } impl RenderOnce for DashboardFilterBar { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let id = self.id; let theme = cx.theme(); let on_window = self.on_window; let on_filter = self.on_filter; let picked: Vec = self .filters .iter() .filter(|filter| filter.picked.is_some()) .map(|filter| filter.key.clone()) .collect(); let selects = self.filters.iter().map(|filter| { let (key, on_filter) = (filter.key.clone(), on_filter.clone()); let mut select = Select::new( (id.clone(), format!("filter-{}", filter.key)), filter.choices.clone(), ) .placeholder(filter.name.clone()) .on_change(move |value, window, cx| { log::info!("filter bar: {key} is {value}"); on_filter(&key, Some(value.clone()), window, cx); }); if let Some(value) = &filter.picked { select = select.selected(value.clone()); } div().w(theme.label_width()).child(select) }); let clear = (!picked.is_empty()).then(|| { let on_filter = on_filter.clone(); Button::new((id.clone(), "clear"), "Clear") .variant(ButtonVariant::Ghost) .on_click(move |_, window, cx| { log::info!("filter bar: clear"); for key in &picked { on_filter(key, None, window, cx); } }) }); div() .flex() .flex_wrap() .items_center() .gap_2() .child( div().w(theme.label_width()).child( Select::new( (id.clone(), "window"), TimeWindow::ALL .iter() .map(|window| Choice::new(window.words(), window.words())), ) .selected(self.window.words()) .on_change(move |value, window, cx| { let chosen = *TimeWindow::ALL .iter() .find(|each| each.words() == value.as_ref()) .expect("a window the select offers"); log::info!("filter bar: window {}", chosen.words()); on_window(chosen, window, cx); }), ), ) .children(selects) .children(clear) } } #[cfg(test)] mod tests { use super::interval; #[test] fn off_is_no_interval() { assert_eq!(interval("0"), None); assert_eq!(interval("30"), Some(30)); } }