Skip to main content

datui_lib/widgets/
sample_form.rs

1//! The Sample form: which rows every analysis tool reads, and how they are picked.
2//!
3//! Only settings. A value names itself ("All rows (36.8M)", "Random", "100,000
4//! rows"), and a kind of rows that needs telling what to type (partitions, files, a
5//! time range) carries its context on the lines under it. The keys are on the
6//! footer, like everywhere else.
7
8use crate::analysis::sample_modal::{SampleField, SampleForm};
9use crate::render::context::RenderContext;
10use crate::widgets::ui::{FormLine, FormValue, FormView};
11use ratatui::buffer::Buffer;
12use ratatui::layout::Rect;
13use ratatui::style::Style;
14use ratatui::text::{Line, Span};
15
16/// How many source files the list under the Files row shows at once.
17pub const FILES_SHOWN: usize = 6;
18
19/// Where values start, past the rail gutter and the longest label.
20const LABEL_WIDTH: u16 = 15;
21
22/// `focused` is whether the form has the cursor; a form waiting beside a focused
23/// tool list is drawn without the rail, so one thing looks focused.
24pub fn render(form: &SampleForm, focused: bool, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
25    let dimmed = Style::default().fg(ctx.dimmed);
26    let g = crate::glyphs::get();
27    // The form, top to bottom: a row per setting, and the context lines a setting
28    // brings with it, directly under it.
29    let choices: Vec<(SampleField, String)> = form
30        .fields()
31        .into_iter()
32        .map(|field| (field, form.choice(field)))
33        .collect();
34    let mut items = Vec::new();
35    for (field, choice) in &choices {
36        let field = *field;
37        let value = match form.input(field) {
38            Some(input) => FormValue::Input(input),
39            None => FormValue::Choice(choice),
40        };
41        items.push(FormLine::Field(field, field.label(), value));
42        match field {
43            SampleField::PartitionValues => {
44                let known = form.partition_values_known();
45                if !known.is_empty() {
46                    let shown = if known.len() > 4 {
47                        format!(
48                            "{}, {}, {} {}",
49                            known[0],
50                            known[1],
51                            g.ellipsis,
52                            known[known.len() - 1]
53                        )
54                    } else {
55                        known.join(", ")
56                    };
57                    items.push(FormLine::Note(Line::styled(
58                        format!("Holds {shown}"),
59                        dimmed,
60                    )));
61                }
62                // The forms a value can take, shown with values this column holds.
63                let (list, range) = match known.as_slice() {
64                    [first, .., last] if known.len() >= 3 => {
65                        (format!("{first},{last}"), format!("{}..{last}", known[1]))
66                    }
67                    _ => ("2019,2021".to_string(), "2020..2022".to_string()),
68                };
69                items.push(FormLine::Note(Line::styled(
70                    format!("One, a list {list}, or a range {range}"),
71                    dimmed,
72                )));
73            }
74            SampleField::Files => {
75                let chosen = form.files_chosen();
76                let files = &form.context.files;
77                for (index, name) in files
78                    .iter()
79                    .enumerate()
80                    .skip(form.file_offset)
81                    .take(FILES_SHOWN)
82                {
83                    let mark = if chosen.contains(&(index + 1)) {
84                        g.checkbox_on
85                    } else {
86                        g.checkbox_off
87                    };
88                    items.push(FormLine::Note(Line::from(vec![
89                        Span::styled(format!("{mark} {:>3}  ", index + 1), dimmed),
90                        Span::styled(short_path(name), Style::default().fg(ctx.text_primary)),
91                    ])));
92                }
93                let rest = files.len().saturating_sub(form.file_offset + FILES_SHOWN);
94                if rest > 0 {
95                    items.push(FormLine::Note(Line::styled(
96                        format!("{rest} more {}", g.ellipsis),
97                        dimmed,
98                    )));
99                }
100            }
101            SampleField::RangeTo => {
102                if let Some(rows) = form.context.view_rows {
103                    items.push(FormLine::Note(Line::styled(
104                        format!("Table: {} rows", crate::numfmt::group_chrome(rows)),
105                        dimmed,
106                    )));
107                }
108            }
109            SampleField::TimeBefore => {
110                items.push(FormLine::Note(Line::styled(
111                    format!("Dates as 2024-01-31 {} Before excluded", g.middot),
112                    dimmed,
113                )));
114            }
115            SampleField::Size => {
116                let per = if matches!(
117                    form.draft.method,
118                    crate::analysis::sampling::SampleMethod::PerPartition { .. }
119                ) {
120                    "Rows per value"
121                } else {
122                    "Rows"
123                };
124                // The cost, when the table has measured its rows; the forms a size
125                // takes otherwise.
126                let said = match (form.rows_expected(), form.estimate()) {
127                    (Some(rows), Some(bytes)) => format!(
128                        "{} rows {} ~{}",
129                        crate::numfmt::group_chrome(rows),
130                        g.middot,
131                        crate::numfmt::bytes(bytes)
132                    ),
133                    _ => format!("{per}, like 50000, 50k or 2m"),
134                };
135                items.push(FormLine::Note(Line::styled(said, dimmed)));
136            }
137            _ => {}
138        }
139    }
140
141    // The error under a blank, in up to two lines.
142    let error_height = u16::from(form.error.is_some()) * 3;
143    let height = items.len() as u16 + error_height + 2;
144    let width = area.width.saturating_sub(4).clamp(40, 72).min(area.width);
145    let frame = crate::render::layout::centered_rect(area, width, height);
146    FormView {
147        title: "Sample",
148        screen: datui_cli::keys::Context::Sample,
149        footer: None,
150        label_width: LABEL_WIDTH,
151        rows: items,
152        focused: focused.then_some(form.field),
153        picker: None,
154        status: form
155            .error
156            .clone()
157            .map(|error| (error, Style::default().fg(ctx.warning))),
158        // Inline it takes clicks beside the tools; floating, its caller shields the
159        // pane it covers.
160        shields: false,
161    }
162    .render::<SampleForm>(frame, buf, ctx);
163}
164
165/// A file's path from its last two parts: a hive file is told apart by its
166/// partition directory, and the root every file shares says nothing.
167fn short_path(path: &str) -> String {
168    let parts: Vec<&str> = path.rsplit(['/', '\\']).take(2).collect();
169    parts.into_iter().rev().collect::<Vec<_>>().join("/")
170}