more grouping prep

This commit is contained in:
Robin Appelman 2025-08-10 12:35:23 +02:00
commit 0fa90510cb
14 changed files with 251 additions and 185 deletions

View file

@ -1,4 +1,4 @@
use crate::grouping::LogGrouping; use crate::grouping::{group_lines_by, FilterGrouping, LogGrouping};
use crate::logfile::{LogFile, LogLine, LogLineNumber}; use crate::logfile::{LogFile, LogLine, LogLineNumber};
use crate::logs::ParsedLogs; use crate::logs::ParsedLogs;
use crate::matcher::MatchResult; use crate::matcher::MatchResult;
@ -12,8 +12,7 @@ use time::OffsetDateTime;
pub struct App<'logs> { pub struct App<'logs> {
pub lines: &'logs ParsedLogs<'logs>, pub lines: &'logs ParsedLogs<'logs>,
pub matches: Vec<LogGrouping<'logs, MatchResult>>, pub matches: &'logs [LogGrouping<'logs, MatchResult>],
pub all: LogGrouping<'logs, MatchResult>,
pub log_file: &'logs LogFile, pub log_file: &'logs LogFile,
pub unmatched_count: usize, pub unmatched_count: usize,
} }
@ -27,15 +26,15 @@ impl<'logs> App<'logs> {
self.log_file.nth(index) self.log_file.nth(index)
} }
pub fn lines_by_request<'a: 'logs>( pub fn lines_by_request<'s>(
&'a self, &self,
request_id: &'a str, request_id: &'s str,
) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'a, 'logs> { ) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'logs, 's> {
self.lines self.lines
.find_lines(move |line| line.request_id == request_id) .find_lines(move |line| line.request_id == request_id)
} }
pub fn error_lines(&self) -> impl Iterator<Item = (&'logs str, &JsonError)> { pub fn error_lines(&self) -> impl Iterator<Item = (&'logs str, &'logs JsonError)> + use<'logs> {
self.lines.errors().iter().map(|(line_number, error)| { self.lines.errors().iter().map(|(line_number, error)| {
(self.log_file.nth(*line_number).unwrap_or_default(), error) (self.log_file.nth(*line_number).unwrap_or_default(), error)
}) })
@ -96,6 +95,15 @@ impl<'logs> LineSet<'logs> {
.parts() .parts()
.all(|filter_part| filter_part.is_match(&line.message)) .all(|filter_part| filter_part.is_match(&line.message))
} }
pub fn iter<'a>(&'a self) -> impl Iterator<Item = &'logs LogLine<'logs>> + use<'a, 'logs> {
self.lines.iter().copied()
}
pub fn group_by<G: FilterGrouping>(&self) -> Vec<LogGrouping<G::Result>> {
let _grouped = group_lines_by(self.lines.iter().copied(), G::filter);
todo!()
}
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]

View file

@ -1,26 +1,37 @@
mod unique;
use crate::app::{Filter, LineSet}; use crate::app::{Filter, LineSet};
use crate::logfile::LogLine; use crate::logfile::LogLine;
use crate::timegraph::{SparkLine, TimeGraph}; use crate::timegraph::{SparkLine, TimeGraph};
use ratatui::layout::{Alignment, Constraint}; use ratatui::layout::{Alignment, Constraint};
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::OnceCell;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use time::OffsetDateTime; use time::OffsetDateTime;
pub use unique::*;
pub trait GroupedLine { pub trait Grouping {
const HEADER: &'static [(&'static str, Alignment)]; const HEADER: &'static [(&'static str, Alignment)];
const WIDTHS: &'static [Constraint]; const WIDTHS: &'static [Constraint];
} }
pub trait FilterGrouping: Grouping {
type Result: Grouping;
type Identifier: Ord;
fn filter(line: &LogLine) -> Self::Identifier;
}
#[allow(clippy::len_without_is_empty)] #[allow(clippy::len_without_is_empty)]
pub trait GroupingResult { pub trait GroupingResult {
type Lines: GroupedLine; type Grouping: Grouping;
type Next: FilterGrouping;
fn len(&self) -> usize; fn height(&self) -> usize {
fn lines<'a>(&'a self) -> impl Iterator<Item = &'a Self::Lines> + use<'a, Self> 1
where }
<Self as GroupingResult>::Lines: 'a;
fn matches(&self, filter: &Filter) -> bool; fn matches(&self, filter: &Filter) -> bool;
@ -31,53 +42,47 @@ pub struct LogGrouping<'logs, G> {
pub name: Option<&'static str>, pub name: Option<&'static str>,
pub result: Option<G>, pub result: Option<G>,
pub count: usize, pub count: usize,
pub all: LineSet<'logs>, pub lines: LineSet<'logs>,
pub grouped: Vec<LineSet<'logs>>, pub by_identifier: OnceCell<Vec<LineSet<'logs>>>,
} }
impl<'logs, G: GroupingResult> LogGrouping<'logs, G> { impl<'logs, G: GroupingResult> LogGrouping<'logs, G> {
pub fn new(result: G, lines: Vec<&'logs LogLine<'logs>>) -> Self { pub fn new(result: G, lines: Vec<&'logs LogLine<'logs>>) -> Self {
let count = lines.len(); let count = lines.len();
let grouped = group_lines_by(lines.iter().copied(), LogLine::identity); let lines = LineSet::new(lines);
let all = LineSet::new(lines);
LogGrouping { LogGrouping {
name: None, name: None,
result: Some(result), result: Some(result),
count, count,
grouped, lines,
all, by_identifier: OnceCell::new(),
} }
} }
pub fn named(name: &'static str, lines: Vec<&'logs LogLine<'logs>>) -> Self { pub fn named(name: &'static str, lines: Vec<&'logs LogLine<'logs>>) -> Self {
let count = lines.len(); let count = lines.len();
let grouped = group_lines_by(lines.iter().copied(), LogLine::identity); let lines = LineSet::new(lines);
let all = LineSet::new(lines);
LogGrouping { LogGrouping {
name: Some(name), name: Some(name),
result: None, result: None,
count, count,
grouped, lines,
all, by_identifier: OnceCell::new(),
} }
} }
pub fn sparkline(&self, time_range: RangeInclusive<OffsetDateTime>) -> &SparkLine { pub fn sparkline(&self, time_range: RangeInclusive<OffsetDateTime>) -> &SparkLine {
self.all.sparkline(time_range) self.lines.sparkline(time_range)
} }
pub fn histogram(&self, time_range: RangeInclusive<OffsetDateTime>) -> &TimeGraph { pub fn histogram(&self, time_range: RangeInclusive<OffsetDateTime>) -> &TimeGraph {
self.all.histogram(time_range) self.lines.histogram(time_range)
} }
pub fn row_count(&self) -> usize { pub fn row_count(&self) -> usize {
self.result.as_ref().map(|res| res.len()).unwrap_or(1) self.result.as_ref().map(|res| res.height()).unwrap_or(1)
}
pub fn iter(&self) -> impl Iterator<Item = &LineSet<'logs>> {
self.grouped.iter()
} }
pub fn matches(&self, filter: &Filter) -> bool { pub fn matches(&self, filter: &Filter) -> bool {
@ -93,6 +98,11 @@ impl<'logs, G: GroupingResult> LogGrouping<'logs, G> {
pub fn count(&self) -> usize { pub fn count(&self) -> usize {
self.count self.count
} }
pub fn by_identifier(&self) -> &[LineSet<'logs>] {
self.by_identifier
.get_or_init(|| group_lines_by(self.lines.iter(), LogLine::identity))
}
} }
pub fn group_lines_by<'logs, I, F, K>(indices: I, f: F) -> Vec<LineSet<'logs>> pub fn group_lines_by<'logs, I, F, K>(indices: I, f: F) -> Vec<LineSet<'logs>>

57
src/grouping/unique.rs Normal file
View file

@ -0,0 +1,57 @@
use crate::app::Filter;
use crate::grouping::{FilterGrouping, Grouping, GroupingResult};
use crate::logfile::LogLine;
use ratatui::layout::{Alignment, Constraint};
use std::borrow::Cow;
pub struct UniqueLog;
impl Grouping for UniqueLog {
const HEADER: &'static [(&'static str, Alignment)] = &[
("Level", Alignment::Left),
("App", Alignment::Left),
("Message", Alignment::Left),
];
const WIDTHS: &'static [Constraint] = &[
Constraint::Min(10),
Constraint::Min(20),
Constraint::Percentage(100),
];
}
impl FilterGrouping for UniqueLog {
type Result = UniqueLog;
type Identifier = u64;
fn filter(line: &LogLine) -> Self::Identifier {
line.identity()
}
}
pub struct UniqueGrouping<'a> {
pub line: &'a LogLine<'a>,
}
impl<'a> GroupingResult for UniqueGrouping<'a> {
type Grouping = UniqueLog;
type Next = UniqueLog;
fn matches(&self, filter: &Filter) -> bool {
if filter.is_empty() {
return true;
}
filter
.parts()
.all(|filter_part| filter_part.is_match(&self.line.message))
}
fn render(&self) -> impl Iterator<Item = Cow<str>> {
[
Cow::from(self.line.level.as_str()),
Cow::from(self.line.app.as_ref()),
self.line.display(),
]
.into_iter()
}
}

View file

@ -32,10 +32,10 @@ impl<'logfile> ParsedLogs<'logfile> {
&self.error_lines &self.error_lines
} }
pub fn find_lines<'a: 'logfile, F: Fn(&'logfile LogLine<'logfile>) -> bool + 'logfile>( pub fn find_lines<'a: 'f, 'f, F: Fn(&'a LogLine<'logfile>) -> bool + 'f>(
&'a self, &'a self,
filter: F, filter: F,
) -> impl Iterator<Item = &'logfile LogLine<'logfile>> + use<'a, 'logfile, F> { ) -> impl Iterator<Item = &'a LogLine<'logfile>> + use<'a, 'logfile, F> {
self.parsed.iter().filter(move |line| filter(line)) self.parsed.iter().filter(move |line| filter(line))
} }

View file

@ -160,6 +160,8 @@ fn main() -> MainResult {
.map(|(result, lines)| LogGrouping::new(result, lines)) .map(|(result, lines)| LogGrouping::new(result, lines))
.collect(); .collect();
matches.insert(0, all); // todo: seems unoptimized
let unmatched_count = unmatched.count(); let unmatched_count = unmatched.count();
if unmatched_count > 0 { if unmatched_count > 0 {
matches.push(unmatched); matches.push(unmatched);
@ -167,8 +169,7 @@ fn main() -> MainResult {
let app = App { let app = App {
lines: &parsed_log, lines: &parsed_log,
matches, matches: &matches,
all,
log_file: &log_file, log_file: &log_file,
unmatched_count, unmatched_count,
}; };

View file

@ -1,5 +1,5 @@
use crate::app::Filter; use crate::app::Filter;
use crate::grouping::{GroupedLine, GroupingResult}; use crate::grouping::{Grouping, GroupingResult, UniqueLog};
use crate::logfile::logline::{Exception, LogLine}; use crate::logfile::logline::{Exception, LogLine};
use crate::logfile::LineNumber; use crate::logfile::LineNumber;
use itertools::Either; use itertools::Either;
@ -211,7 +211,7 @@ impl From<Vec<LoggingStatementWithPathPrefix>> for MatchResult {
} }
} }
impl GroupedLine for LoggingStatementWithPathPrefix { impl Grouping for LoggingStatementWithPathPrefix {
const HEADER: &'static [(&'static str, Alignment)] = &[ const HEADER: &'static [(&'static str, Alignment)] = &[
("Statement", Alignment::Left), ("Statement", Alignment::Left),
("File", Alignment::Left), ("File", Alignment::Left),
@ -222,25 +222,17 @@ impl GroupedLine for LoggingStatementWithPathPrefix {
Constraint::Percentage(70), Constraint::Percentage(70),
Constraint::Percentage(30), Constraint::Percentage(30),
Constraint::Min(6), Constraint::Min(6),
Constraint::Length(10),
Constraint::Min(10),
]; ];
} }
impl GroupingResult for MatchResult { impl GroupingResult for MatchResult {
type Lines = LoggingStatementWithPathPrefix; type Grouping = LoggingStatementWithPathPrefix;
type Next = UniqueLog;
fn len(&self) -> usize { fn height(&self) -> usize {
self.count() self.count()
} }
fn lines<'a>(&'a self) -> impl Iterator<Item = &'a Self::Lines> + use<'a>
where
<Self as GroupingResult>::Lines: 'a,
{
self.iter()
}
fn matches(&self, filter: &Filter) -> bool { fn matches(&self, filter: &Filter) -> bool {
if filter.is_empty() { if filter.is_empty() {
return true; return true;
@ -269,7 +261,7 @@ impl GroupingResult for MatchResult {
let mut paths = String::new(); let mut paths = String::new();
let mut lines = String::new(); let mut lines = String::new();
for statement in self.lines() { for statement in self.iter() {
writeln!(&mut message, "{}", statement.message()).ok(); writeln!(&mut message, "{}", statement.message()).ok();
writeln!(&mut paths, "{}", statement.path()).ok(); writeln!(&mut paths, "{}", statement.path()).ok();
writeln!(&mut lines, "{}", statement.line()).ok(); writeln!(&mut lines, "{}", statement.line()).ok();

View file

@ -1,4 +1,4 @@
use crate::app::{App, Filter}; use crate::app::Filter;
use crate::logfile::logline::{format_time, LogLine}; use crate::logfile::logline::{format_time, LogLine};
use crate::ui::state::GroupedLogGrouping; use crate::ui::state::GroupedLogGrouping;
use crate::ui::style::TABLE_HEADER_STYLE; use crate::ui::style::TABLE_HEADER_STYLE;
@ -10,28 +10,25 @@ use ratatui::prelude::{StatefulWidget, Widget};
use ratatui::text::Text; use ratatui::text::Text;
use ratatui::widgets::{Cell, Paragraph, Row, Wrap}; use ratatui::widgets::{Cell, Paragraph, Row, Wrap};
pub struct GroupedLogs<'a> { pub struct LogsByIdentifier<'a> {
lines: &'a [&'a LogLine<'a>], lines: &'a [&'a LogLine<'a>],
app: &'a App<'a>,
filter: &'a Filter, filter: &'a Filter,
grouping: GroupedLogGrouping, grouping: GroupedLogGrouping,
} }
pub fn grouped_logs<'a>( pub fn logs_by_identifier<'a>(
app: &'a App<'a>,
lines: &'a [&'a LogLine<'a>], lines: &'a [&'a LogLine<'a>],
filter: &'a Filter, filter: &'a Filter,
grouping: GroupedLogGrouping, grouping: GroupedLogGrouping,
) -> GroupedLogs<'a> { ) -> LogsByIdentifier<'a> {
GroupedLogs { LogsByIdentifier {
lines, lines,
app,
filter, filter,
grouping, grouping,
} }
} }
impl StatefulWidget for GroupedLogs<'_> { impl StatefulWidget for LogsByIdentifier<'_> {
type State = ScrollbarTableState; type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
@ -45,7 +42,7 @@ impl StatefulWidget for GroupedLogs<'_> {
.copied() .copied()
.filter(|line| line.matches(self.filter)) .filter(|line| line.matches(self.filter))
.nth(state.selected()) .nth(state.selected())
.unwrap_or(self.app.lines.first()); .unwrap_or(self.lines.first().unwrap());
let par = match self.grouping { let par = match self.grouping {
GroupedLogGrouping::Message => Paragraph::new(format!( GroupedLogGrouping::Message => Paragraph::new(format!(

View file

@ -5,7 +5,7 @@ use ratatui::layout::Constraint;
use ratatui::text::Text; use ratatui::text::Text;
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
pub fn error_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> { pub fn error_list<'a>(app: &App<'a>) -> ScrollbarTable<'a> {
let header = [Text::from("Error"), Text::from("Line")] let header = [Text::from("Error"), Text::from("Line")]
.into_iter() .into_iter()
.map(Cell::from) .map(Cell::from)

View file

@ -11,7 +11,7 @@ pub enum FooterParams<'a> {
FilterInput { page: UiPage, filter: &'a Filter }, FilterInput { page: UiPage, filter: &'a Filter },
} }
pub fn footer<'a>(app: &App<'a>, params: FooterParams<'a>) -> Table<'a> { pub fn footer<'a>(app: &App, params: FooterParams<'a>) -> Table<'a> {
let footer_style = Style::default() let footer_style = Style::default()
.bg(tailwind::BLACK) .bg(tailwind::BLACK)
.fg(tailwind::GREEN.c600); .fg(tailwind::GREEN.c600);

View file

@ -1,21 +1,19 @@
use crate::app::Filter; use crate::app::Filter;
use crate::grouping::{GroupedLine, GroupingResult, LogGrouping}; use crate::grouping::{Grouping, GroupingResult, LogGrouping};
use crate::ui::style::TABLE_HEADER_STYLE; use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable; use crate::ui::table::ScrollbarTable;
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::{Cell, Row}; use ratatui::widgets::{Cell, Row};
use std::borrow::Cow; use std::borrow::Cow;
use std::iter::once;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use time::OffsetDateTime; use time::OffsetDateTime;
pub fn grouping_list<'a, G: GroupingResult>( pub fn grouping_list<'a, G: GroupingResult>(
all: &'a LogGrouping<'a, G>,
items: &'a [LogGrouping<'a, G>], items: &'a [LogGrouping<'a, G>],
time_range: RangeInclusive<OffsetDateTime>, time_range: RangeInclusive<OffsetDateTime>,
filter: &Filter, filter: &Filter,
) -> ScrollbarTable<'a> { ) -> ScrollbarTable<'a> {
let header = G::Lines::HEADER let header = G::Grouping::HEADER
.iter() .iter()
.copied() .copied()
.chain([("Time", Alignment::Left), ("Count", Alignment::Left)]); .chain([("Time", Alignment::Left), ("Count", Alignment::Left)]);
@ -28,10 +26,14 @@ pub fn grouping_list<'a, G: GroupingResult>(
.height(1); .height(1);
ScrollbarTable::new( ScrollbarTable::new(
once(all) items
.chain(items.iter().filter(|result| result.matches(filter))) .iter()
.filter(|result| result.matches(filter))
.map(|result| grouped_row(result, time_range.clone())), .map(|result| grouped_row(result, time_range.clone())),
G::Lines::WIDTHS, G::Grouping::WIDTHS
.iter()
.copied()
.chain([Constraint::Length(10), Constraint::Min(10)]),
) )
.header(header) .header(header)
} }
@ -46,7 +48,7 @@ fn grouped_row<'a, G: GroupingResult>(
Cow::from(grouping.sparkline(time_range)), Cow::from(grouping.sparkline(time_range)),
Cow::from(grouping.count().to_string()), Cow::from(grouping.count().to_string()),
]); ]);
Row::new(columns).height(match_result.len() as u16) Row::new(columns).height(match_result.height() as u16)
} else { } else {
Row::new([ Row::new([
Text::from(grouping.name.unwrap_or_default()), Text::from(grouping.name.unwrap_or_default()),

View file

@ -1,3 +1,4 @@
use crate::app::App;
use crate::ui::find_hit_row; use crate::ui::find_hit_row;
use crate::ui::state::{Mode, UiPage, UiState}; use crate::ui::state::{Mode, UiPage, UiState};
use ratatui::crossterm::event; use ratatui::crossterm::event;
@ -29,7 +30,7 @@ pub enum PopMode {
Word, Word,
} }
pub fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEvent>> { pub fn handle_events(page: UiPage, ui_state: &UiState, app: &App) -> io::Result<Option<UiEvent>> {
if event::poll(Duration::from_millis(50))? { if event::poll(Duration::from_millis(50))? {
match event::read()? { match event::read()? {
Event::Key(key) if key.kind == event::KeyEventKind::Press => { Event::Key(key) if key.kind == event::KeyEventKind::Press => {
@ -76,7 +77,7 @@ pub fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEv
MouseEventKind::ScrollUp => Some(UiEvent::Scroll(-1)), MouseEventKind::ScrollUp => Some(UiEvent::Scroll(-1)),
MouseEventKind::ScrollDown => Some(UiEvent::Scroll(1)), MouseEventKind::ScrollDown => Some(UiEvent::Scroll(1)),
MouseEventKind::Down(MouseButton::Left) => { MouseEventKind::Down(MouseButton::Left) => {
find_hit_row(mouse.row, ui_state).map(UiEvent::Enter) find_hit_row(mouse.row, ui_state, app).map(UiEvent::Enter)
} }
_ => None, _ => None,
}) })

View file

@ -1,16 +1,17 @@
use crate::app::App; use crate::app::App;
use crate::error::UiError; use crate::error::UiError;
use crate::matcher::MatchResult; use crate::matcher::MatchResult;
use crate::ui::by_identifier::logs_by_identifier;
use crate::ui::error_list::error_list; use crate::ui::error_list::error_list;
use crate::ui::footer::footer; use crate::ui::footer::footer;
use crate::ui::grouped_logs::grouped_logs;
use crate::ui::grouping_list::grouping_list; use crate::ui::grouping_list::grouping_list;
use crate::ui::histogram::UiHistogram; use crate::ui::histogram::UiHistogram;
use crate::ui::input::handle_events; use crate::ui::input::handle_events;
use crate::ui::single_group::single_group;
use crate::ui::single_log::single_log; use crate::ui::single_log::single_log;
use crate::ui::single_match::grouped_lines;
use crate::ui::state::{ use crate::ui::state::{
ErrorLinesState, ErrorState, GroupedLogsState, LogState, MatchListState, MatchState, UiState, ErrorLinesState, ErrorState, GroupListState, GroupState, LogState, LogsByIdentifierState,
UiState,
}; };
use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use ratatui::crossterm::terminal::{ use ratatui::crossterm::terminal::{
@ -25,24 +26,24 @@ use std::io;
use std::io::stdout; use std::io::stdout;
use std::panic::{set_hook, take_hook}; use std::panic::{set_hook, take_hook};
mod by_identifier;
mod error_list; mod error_list;
mod footer; mod footer;
mod grouped_logs;
mod grouping_list; mod grouping_list;
mod histogram; mod histogram;
mod input; mod input;
mod single_group;
mod single_log; mod single_log;
mod single_match;
mod state; mod state;
pub mod style; pub mod style;
mod table; mod table;
pub fn run_ui(app: App) -> Result<(), UiError> { pub fn run_ui<'a>(app: App<'a>) -> Result<(), UiError> {
init_panic_hook(); init_panic_hook();
let mut terminal = init_tui()?; let mut terminal = init_tui()?;
terminal.clear().ok(); terminal.clear().ok();
let mut ui_state = UiState::new(&app); let mut ui_state: UiState = UiState::new(&app);
let mut update = true; let mut update = true;
while !matches!(ui_state, UiState::Quit) { while !matches!(ui_state, UiState::Quit) {
@ -50,7 +51,7 @@ pub fn run_ui(app: App) -> Result<(), UiError> {
terminal.draw(|frame| ui(frame, &app, &mut ui_state))?; terminal.draw(|frame| ui(frame, &app, &mut ui_state))?;
} }
update = false; update = false;
if let Some(event) = handle_events(ui_state.page(), &ui_state)? { if let Some(event) = handle_events(ui_state.page(), &ui_state, &app)? {
(update, ui_state) = ui_state.process(event, &app); (update, ui_state) = ui_state.process(event, &app);
} }
} }
@ -83,9 +84,9 @@ pub fn restore_tui() -> io::Result<()> {
Ok(()) Ok(())
} }
fn find_hit_row(row: u16, ui_state: &UiState) -> Option<usize> { fn find_hit_row(row: u16, ui_state: &UiState, app: &App) -> Option<usize> {
if let Some(table_row) = row.checked_sub(ui_state.content_offset()) { if let Some(table_row) = row.checked_sub(ui_state.content_offset()) {
let selected = ui_state.index_for_row(table_row as usize); let selected = ui_state.index_for_row(table_row as usize, app);
if selected < ui_state.row_count() { if selected < ui_state.row_count() {
Some(selected) Some(selected)
} else { } else {
@ -110,28 +111,23 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state { match state {
UiState::Quit => {} UiState::Quit => {}
UiState::MatchList(MatchListState { UiState::GroupList(GroupListState {
table_state, table_state,
filter, filter,
.. ..
}) => { }) => {
let selected = table_state.selected(); let selected = table_state.selected();
let histogram = if selected == 0 { let histogram = app.matches[selected].histogram(app.time_range());
app.all.histogram(app.time_range())
} else {
let log_match = &app.matches[selected - 1];
log_match.histogram(app.time_range())
};
frame.render_widget(UiHistogram::new(histogram), layout[0]); frame.render_widget(UiHistogram::new(histogram), layout[0]);
frame.render_stateful_widget( frame.render_stateful_widget(
grouping_list::<MatchResult>(&app.all, &app.matches, app.time_range(), filter), grouping_list::<MatchResult>(app.matches, app.time_range(), filter),
layout[1], layout[1],
table_state, table_state,
); );
frame.render_widget(footer(app, state.footer_params()), layout[2]); frame.render_widget(footer(app, state.footer_params()), layout[2]);
} }
UiState::Match(MatchState { UiState::Group(GroupState {
result, result,
table_state, table_state,
filter, filter,
@ -139,9 +135,9 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
}) => { }) => {
let selected = table_state.selected(); let selected = table_state.selected();
let selected_group = if selected == 0 { let selected_group = if selected == 0 {
&result.all &result.lines
} else { } else {
&result.grouped[selected - 1] &result.by_identifier()[selected - 1]
}; };
frame.render_widget( frame.render_widget(
@ -149,13 +145,18 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
layout[0], layout[0],
); );
frame.render_stateful_widget( frame.render_stateful_widget(
grouped_lines(app, result, filter), single_group(
app.time_range(),
&result.lines,
result.by_identifier(),
filter,
),
layout[1], layout[1],
table_state, table_state,
); );
frame.render_widget(footer(app, state.footer_params()), layout[2]); frame.render_widget(footer(app, state.footer_params()), layout[2]);
} }
UiState::GroupedLogs(GroupedLogsState { UiState::ByIdentifier(LogsByIdentifierState {
lines, lines,
table_state, table_state,
filter, filter,
@ -163,7 +164,7 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
.. ..
}) => { }) => {
frame.render_stateful_widget( frame.render_stateful_widget(
grouped_logs(app, lines, filter, *grouping), logs_by_identifier(lines, filter, *grouping),
layout[0].union(layout[1]), layout[0].union(layout[1]),
table_state, table_state,
); );

View file

@ -1,6 +1,4 @@
use crate::app::{App, Filter, LineSet}; use crate::app::{Filter, LineSet};
use crate::grouping::LogGrouping;
use crate::matcher::MatchResult;
use crate::ui::style::TABLE_HEADER_STYLE; use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::{ScrollbarTable, ScrollbarTableState}; use crate::ui::table::{ScrollbarTable, ScrollbarTableState};
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
@ -12,32 +10,35 @@ use std::iter::once;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use time::OffsetDateTime; use time::OffsetDateTime;
pub fn grouped_lines<'a>( pub fn single_group<'a>(
app: &'a App<'a>, time_range: RangeInclusive<OffsetDateTime>,
log_match: &'a LogGrouping<'a, MatchResult>, all: &'a LineSet<'a>,
unique: &'a [LineSet<'a>],
filter: &'a Filter, filter: &'a Filter,
) -> SingleMatchTable<'a> { ) -> SingleGroup<'a> {
SingleMatchTable { SingleGroup {
app, time_range,
log_match, all,
unique,
filter, filter,
} }
} }
pub struct SingleMatchTable<'a> { pub struct SingleGroup<'a> {
app: &'a App<'a>, time_range: RangeInclusive<OffsetDateTime>,
log_match: &'a LogGrouping<'a, MatchResult>, all: &'a LineSet<'a>,
unique: &'a [LineSet<'a>],
filter: &'a Filter, filter: &'a Filter,
} }
impl StatefulWidget for SingleMatchTable<'_> { impl StatefulWidget for SingleGroup<'_> {
type State = ScrollbarTableState; type State = ScrollbarTableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
where where
Self: Sized, Self: Sized,
{ {
let grouped = &self.log_match.grouped; let grouped = self.unique;
let header = [ let header = [
Text::from("Level"), Text::from("Level"),
Text::from("App"), Text::from("App"),
@ -63,8 +64,8 @@ impl StatefulWidget for SingleMatchTable<'_> {
Text::from("All lines"), Text::from("All lines"),
Text::from(""), Text::from(""),
Text::from(""), Text::from(""),
Text::from(self.log_match.sparkline(self.app.time_range())), Text::from(self.all.sparkline(self.time_range.clone())),
Text::from(self.log_match.count().to_string()), Text::from(self.all.len().to_string()),
])) ]))
.chain( .chain(
grouped grouped
@ -73,7 +74,7 @@ impl StatefulWidget for SingleMatchTable<'_> {
.enumerate() .enumerate()
.map(|(i, group)| { .map(|(i, group)| {
group_row( group_row(
self.app.time_range(), self.time_range.clone(),
group, group,
i.abs_diff(state.selected()) < 100, i.abs_diff(state.selected()) < 100,
) )

View file

@ -16,9 +16,9 @@ use std::sync::Arc;
#[derive(Clone, From, PartialEq)] #[derive(Clone, From, PartialEq)]
pub enum UiState<'a> { pub enum UiState<'a> {
MatchList(MatchListState<'a>), GroupList(GroupListState),
Match(MatchState<'a>), Group(GroupState<'a>),
GroupedLogs(GroupedLogsState<'a>), ByIdentifier(LogsByIdentifierState<'a>),
Log(LogState<'a>), Log(LogState<'a>),
Errors(ErrorLinesState<'a>), Errors(ErrorLinesState<'a>),
Error(ErrorState<'a>), Error(ErrorState<'a>),
@ -32,22 +32,19 @@ pub enum Mode {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct MatchListState<'a> { pub struct GroupListState {
app: &'a App<'a>,
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub filter: Filter, pub filter: Filter,
mode: Mode, mode: Mode,
} }
impl<'a> MatchListState<'a> { impl GroupListState {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected() self.table_state.selected()
} }
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> { fn enter<'a>(self, selected: usize, app: &App<'a>) -> UiState<'a> {
let result = if selected == 0 { let result = if self.filter.is_empty() {
&app.all
} else if self.filter.is_empty() {
&app.matches[selected - 1] &app.matches[selected - 1]
} else { } else {
app.matches app.matches
@ -57,8 +54,8 @@ impl<'a> MatchListState<'a> {
.unwrap_or(app.matches.last().unwrap()) .unwrap_or(app.matches.last().unwrap())
}; };
let table_state = ScrollbarTableState::new(result.grouped.len() + 1); let table_state = ScrollbarTableState::new(result.by_identifier().len() + 1);
UiState::Match(MatchState { UiState::Group(GroupState {
result, result,
table_state, table_state,
previous: Box::new(self.into()), previous: Box::new(self.into()),
@ -68,14 +65,14 @@ impl<'a> MatchListState<'a> {
} }
} }
impl PartialEq for MatchListState<'_> { impl PartialEq for GroupListState {
fn eq(&self, _other: &Self) -> bool { fn eq(&self, _other: &Self) -> bool {
true true
} }
} }
#[derive(Clone)] #[derive(Clone)]
pub struct MatchState<'a> { pub struct GroupState<'a> {
pub result: &'a LogGrouping<'a, MatchResult>, pub result: &'a LogGrouping<'a, MatchResult>,
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
@ -83,7 +80,7 @@ pub struct MatchState<'a> {
mode: Mode, mode: Mode,
} }
impl<'a> MatchState<'a> { impl<'a> GroupState<'a> {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected() self.table_state.selected()
} }
@ -93,12 +90,12 @@ impl<'a> MatchState<'a> {
table_state.select(Some(0)); table_state.select(Some(0));
let selected_line = if selected == 0 { let selected_line = if selected == 0 {
&self.result.all &self.result.lines
} else if self.filter.is_empty() { } else if self.filter.is_empty() {
&self.result.grouped[selected - 1] &self.result.by_identifier()[selected - 1]
} else { } else {
self.result self.result
.grouped .by_identifier()
.iter() .iter()
.filter(|grouped| grouped.matches(&self.filter)) .filter(|grouped| grouped.matches(&self.filter))
.nth(selected - 1) .nth(selected - 1)
@ -106,7 +103,7 @@ impl<'a> MatchState<'a> {
}; };
let lines = selected_line.lines.as_slice(); let lines = selected_line.lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len()); let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState { UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(), lines: lines.into(),
table_state, table_state,
previous: Box::new(self.into()), previous: Box::new(self.into()),
@ -117,7 +114,7 @@ impl<'a> MatchState<'a> {
} }
} }
impl PartialEq for MatchState<'_> { impl PartialEq for GroupState<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.result.result == other.result.result self.result.result == other.result.result
} }
@ -130,7 +127,7 @@ pub enum GroupedLogGrouping {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct GroupedLogsState<'a> { pub struct LogsByIdentifierState<'a> {
pub lines: Cow<'a, [&'a LogLine<'a>]>, pub lines: Cow<'a, [&'a LogLine<'a>]>,
pub table_state: ScrollbarTableState, pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>, pub previous: Box<UiState<'a>>,
@ -139,7 +136,7 @@ pub struct GroupedLogsState<'a> {
pub grouping: GroupedLogGrouping, pub grouping: GroupedLogGrouping,
} }
impl<'a> GroupedLogsState<'a> { impl<'a> LogsByIdentifierState<'a> {
fn selected(&self) -> usize { fn selected(&self) -> usize {
self.table_state.selected() self.table_state.selected()
} }
@ -157,7 +154,7 @@ impl<'a> GroupedLogsState<'a> {
} }
} }
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> { fn enter(self, selected: usize, app: &App<'a>) -> UiState<'a> {
let log = self.get_selected(selected); let log = self.get_selected(selected);
let raw_line = app.get_source_line(log.line_number).unwrap(); let raw_line = app.get_source_line(log.line_number).unwrap();
let full_line = match parse_line_full(raw_line) { let full_line = match parse_line_full(raw_line) {
@ -187,12 +184,12 @@ impl<'a> GroupedLogsState<'a> {
}) })
} }
fn by_request(self, selected: usize, app: &'a App<'a>) -> UiState<'a> { fn by_request(self, selected: usize, app: &App<'a>) -> UiState<'a> {
let log = self.get_selected(selected); let log = self.get_selected(selected);
let lines: Vec<_> = app.lines_by_request(&log.request_id).collect(); let lines: Vec<_> = app.lines_by_request(&log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len()); let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState { UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(), lines: lines.into(),
mode: Mode::Normal, mode: Mode::Normal,
filter: Filter::default(), filter: Filter::default(),
@ -203,7 +200,7 @@ impl<'a> GroupedLogsState<'a> {
} }
} }
impl PartialEq for GroupedLogsState<'_> { impl PartialEq for LogsByIdentifierState<'_> {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.lines == other.lines self.lines == other.lines
} }
@ -230,11 +227,11 @@ pub struct LogState<'a> {
} }
impl<'a> LogState<'a> { impl<'a> LogState<'a> {
fn by_request(self, app: &'a App<'a>) -> UiState<'a> { fn by_request(self, app: &App<'a>) -> UiState<'a> {
let lines: Vec<_> = app.lines_by_request(&self.log.request_id).collect(); let lines: Vec<_> = app.lines_by_request(&self.log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len()); let table_state = ScrollbarTableState::new(lines.len());
UiState::GroupedLogs(GroupedLogsState { UiState::ByIdentifier(LogsByIdentifierState {
lines: lines.into(), lines: lines.into(),
mode: Mode::Normal, mode: Mode::Normal,
filter: Filter::default(), filter: Filter::default(),
@ -252,11 +249,10 @@ impl PartialEq for LogState<'_> {
} }
impl<'a> UiState<'a> { impl<'a> UiState<'a> {
pub fn new(app: &'a App<'a>) -> Self { pub fn new(app: &App) -> Self {
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
UiState::MatchList(MatchListState { UiState::GroupList(GroupListState {
app,
table_state: ScrollbarTableState::new(app.match_lines()), table_state: ScrollbarTableState::new(app.match_lines()),
filter: Filter::default(), filter: Filter::default(),
mode: Mode::Normal, mode: Mode::Normal,
@ -265,9 +261,9 @@ impl<'a> UiState<'a> {
pub fn page(&self) -> UiPage { pub fn page(&self) -> UiPage {
match self { match self {
UiState::Quit | UiState::MatchList(_) => UiPage::MatchList, UiState::Quit | UiState::GroupList(_) => UiPage::MatchList,
UiState::Match(_) => UiPage::Match, UiState::Group(_) => UiPage::Match,
UiState::GroupedLogs(_) => UiPage::Logs, UiState::ByIdentifier(_) => UiPage::Logs,
UiState::Log(_) => UiPage::Log, UiState::Log(_) => UiPage::Log,
UiState::Errors(_) => UiPage::Errors, UiState::Errors(_) => UiPage::Errors,
UiState::Error(_) => UiPage::Error, UiState::Error(_) => UiPage::Error,
@ -276,45 +272,45 @@ impl<'a> UiState<'a> {
pub fn mode(&self) -> Mode { pub fn mode(&self) -> Mode {
match self { match self {
UiState::MatchList(state) => state.mode, UiState::GroupList(state) => state.mode,
UiState::Match(state) => state.mode, UiState::Group(state) => state.mode,
UiState::GroupedLogs(state) => state.mode, UiState::ByIdentifier(state) => state.mode,
_ => Mode::Normal, _ => Mode::Normal,
} }
} }
pub fn set_mode(&mut self, mode: Mode) { pub fn set_mode(&mut self, mode: Mode) {
match self { match self {
UiState::MatchList(state) => state.mode = mode, UiState::GroupList(state) => state.mode = mode,
UiState::Match(state) => state.mode = mode, UiState::Group(state) => state.mode = mode,
UiState::GroupedLogs(state) => state.mode = mode, UiState::ByIdentifier(state) => state.mode = mode,
_ => {} _ => {}
} }
} }
pub fn filter(&self) -> Option<&Filter> { pub fn filter(&self) -> Option<&Filter> {
match self { match self {
UiState::MatchList(state) => Some(&state.filter), UiState::GroupList(state) => Some(&state.filter),
UiState::Match(state) => Some(&state.filter), UiState::Group(state) => Some(&state.filter),
UiState::GroupedLogs(state) => Some(&state.filter), UiState::ByIdentifier(state) => Some(&state.filter),
_ => None, _ => None,
} }
} }
pub fn filter_mut(&mut self) -> Option<&mut Filter> { pub fn filter_mut(&mut self) -> Option<&mut Filter> {
match self { match self {
UiState::MatchList(state) => Some(&mut state.filter), UiState::GroupList(state) => Some(&mut state.filter),
UiState::Match(state) => Some(&mut state.filter), UiState::Group(state) => Some(&mut state.filter),
UiState::GroupedLogs(state) => Some(&mut state.filter), UiState::ByIdentifier(state) => Some(&mut state.filter),
_ => None, _ => None,
} }
} }
fn table_state(&self) -> Option<&ScrollbarTableState> { fn table_state(&self) -> Option<&ScrollbarTableState> {
match self { match self {
UiState::MatchList(state) => Some(&state.table_state), UiState::GroupList(state) => Some(&state.table_state),
UiState::Match(state) => Some(&state.table_state), UiState::Group(state) => Some(&state.table_state),
UiState::GroupedLogs(state) => Some(&state.table_state), UiState::ByIdentifier(state) => Some(&state.table_state),
UiState::Log(state) => Some(&state.table_state), UiState::Log(state) => Some(&state.table_state),
UiState::Errors(state) => Some(&state.table_state), UiState::Errors(state) => Some(&state.table_state),
_ => None, _ => None,
@ -323,9 +319,9 @@ impl<'a> UiState<'a> {
fn table_state_mut(&mut self) -> Option<&mut ScrollbarTableState> { fn table_state_mut(&mut self) -> Option<&mut ScrollbarTableState> {
match self { match self {
UiState::MatchList(state) => Some(&mut state.table_state), UiState::GroupList(state) => Some(&mut state.table_state),
UiState::Match(state) => Some(&mut state.table_state), UiState::Group(state) => Some(&mut state.table_state),
UiState::GroupedLogs(state) => Some(&mut state.table_state), UiState::ByIdentifier(state) => Some(&mut state.table_state),
UiState::Log(state) => Some(&mut state.table_state), UiState::Log(state) => Some(&mut state.table_state),
UiState::Errors(state) => Some(&mut state.table_state), UiState::Errors(state) => Some(&mut state.table_state),
_ => None, _ => None,
@ -348,9 +344,9 @@ impl<'a> UiState<'a> {
} }
} }
pub fn index_for_row(&self, row: usize) -> usize { pub fn index_for_row(&self, row: usize, app: &App) -> usize {
match self { match self {
UiState::MatchList(MatchListState { app, filter, .. }) => { UiState::GroupList(GroupListState { filter, .. }) => {
let mut total_height = 0; let mut total_height = 0;
let match_row_counts = app let match_row_counts = app
.matches .matches
@ -381,9 +377,9 @@ impl<'a> UiState<'a> {
/// get the offset of the "main content" from the top of the screen /// get the offset of the "main content" from the top of the screen
pub fn content_offset(&self) -> u16 { pub fn content_offset(&self) -> u16 {
match self { match self {
UiState::MatchList(_) => UI_HEADER_SIZE + 1, UiState::GroupList(_) => UI_HEADER_SIZE + 1,
UiState::Match(_) => UI_HEADER_SIZE + 1, UiState::Group(_) => UI_HEADER_SIZE + 1,
UiState::GroupedLogs(_) => UI_HEADER_SIZE + 1, UiState::ByIdentifier(_) => UI_HEADER_SIZE + 1,
UiState::Log(_) => 0, UiState::Log(_) => 0,
UiState::Errors(_) => 0, UiState::Errors(_) => 0,
UiState::Error(_) => 0, UiState::Error(_) => 0,
@ -391,12 +387,12 @@ impl<'a> UiState<'a> {
} }
} }
pub fn process(self, event: UiEvent, app: &'a App<'a>) -> (bool, UiState<'a>) { pub fn process(self, event: UiEvent, app: &App<'a>) -> (bool, UiState<'a>) {
match (self, event) { match (self, event) {
(UiState::Quit, _) => (true, UiState::Quit), (UiState::Quit, _) => (true, UiState::Quit),
(_, UiEvent::Quit) => (true, UiState::Quit), (_, UiEvent::Quit) => (true, UiState::Quit),
( (
UiState::MatchList(MatchListState { UiState::GroupList(GroupListState {
mode: Mode::Normal, .. mode: Mode::Normal, ..
}), }),
UiEvent::Back, UiEvent::Back,
@ -425,14 +421,14 @@ impl<'a> UiState<'a> {
} }
(true, state) (true, state)
} }
(UiState::MatchList(state), UiEvent::Select) => { (UiState::GroupList(state), UiEvent::Select) => {
let selected = state.selected(); let selected = state.selected();
(true, state.enter(selected, app)) (true, state.enter(selected, app))
} }
(UiState::MatchList(state), UiEvent::Enter(selected)) => { (UiState::GroupList(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected, app)) (true, state.enter(selected, app))
} }
(UiState::MatchList(state), UiEvent::Errors) => { (UiState::GroupList(state), UiEvent::Errors) => {
let table_state = ScrollbarTableState::new(app.error_count()); let table_state = ScrollbarTableState::new(app.error_count());
( (
true, true,
@ -442,19 +438,19 @@ impl<'a> UiState<'a> {
}), }),
) )
} }
(UiState::Match(state), UiEvent::Select) => { (UiState::Group(state), UiEvent::Select) => {
let selected = state.selected(); let selected = state.selected();
(true, state.enter(selected)) (true, state.enter(selected))
} }
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected)), (UiState::Group(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
(UiState::GroupedLogs(state), UiEvent::Select) => { (UiState::ByIdentifier(state), UiEvent::Select) => {
let selected = state.selected(); let selected = state.selected();
(true, state.enter(selected, app)) (true, state.enter(selected, app))
} }
(UiState::GroupedLogs(state), UiEvent::Enter(selected)) => { (UiState::ByIdentifier(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected, app)) (true, state.enter(selected, app))
} }
(UiState::GroupedLogs(state), UiEvent::Copy) => { (UiState::ByIdentifier(state), UiEvent::Copy) => {
let selected = state.selected(); let selected = state.selected();
let mut table_state = TableState::default(); let mut table_state = TableState::default();
table_state.select(Some(0)); table_state.select(Some(0));
@ -462,7 +458,7 @@ impl<'a> UiState<'a> {
let line = state.lines[selected]; let line = state.lines[selected];
let raw = app.get_source_line(line.line_number).unwrap_or_default(); let raw = app.get_source_line(line.line_number).unwrap_or_default();
copy_osc(raw); copy_osc(raw);
(false, UiState::GroupedLogs(state)) (false, UiState::ByIdentifier(state))
} }
(UiState::Log(state), UiEvent::Copy) => { (UiState::Log(state), UiEvent::Copy) => {
let raw = app let raw = app
@ -471,7 +467,7 @@ impl<'a> UiState<'a> {
copy_osc(raw); copy_osc(raw);
(false, UiState::Log(state)) (false, UiState::Log(state))
} }
(UiState::GroupedLogs(state), UiEvent::ByRequest) => { (UiState::ByIdentifier(state), UiEvent::ByRequest) => {
let selected = state.selected(); let selected = state.selected();
(true, state.by_request(selected, app)) (true, state.by_request(selected, app))
} }
@ -512,7 +508,7 @@ impl<'a> UiState<'a> {
(true, ui) (true, ui)
} }
( (
mut ui @ UiState::MatchList(MatchListState { mut ui @ UiState::GroupList(GroupListState {
mode: Mode::FilterInput, mode: Mode::FilterInput,
.. ..
}), }),
@ -526,8 +522,8 @@ impl<'a> UiState<'a> {
} }
( (
UiState::Match(MatchState { previous, .. }) UiState::Group(GroupState { previous, .. })
| UiState::GroupedLogs(GroupedLogsState { previous, .. }) | UiState::ByIdentifier(LogsByIdentifierState { previous, .. })
| UiState::Log(LogState { previous, .. }) | UiState::Log(LogState { previous, .. })
| UiState::Errors(ErrorLinesState { previous, .. }), | UiState::Errors(ErrorLinesState { previous, .. }),
UiEvent::Back, UiEvent::Back,