work towards generic grouping
All checks were successful
CI / build (push) Successful in 48s
CI / checks (push) Successful in 1m1s
CI / build-nixpkgs (push) Successful in 42s

This commit is contained in:
Robin Appelman 2025-08-09 17:27:25 +02:00
commit 3943407b9b
13 changed files with 309 additions and 202 deletions

View file

@ -27,7 +27,7 @@ pub fn footer<'a>(app: &App<'a>, params: FooterParams<'a>) -> Table<'a> {
Table::new(
[Row::new([
Text::from(help(page)),
Text::from(format!("{} unmatched items", app.unmatched.count())),
Text::from(format!("{} unmatched items", app.unmatched_count)),
Text::from(format!("{} parse errors", app.error_count())),
])],
widths,

59
src/ui/grouping_list.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::app::Filter;
use crate::grouping::{GroupedLine, GroupingResult, LogGrouping};
use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable;
use ratatui::prelude::*;
use ratatui::widgets::{Cell, Row};
use std::borrow::Cow;
use std::iter::once;
use std::ops::RangeInclusive;
use time::OffsetDateTime;
pub fn grouping_list<'a, G: GroupingResult>(
all: &'a LogGrouping<'a, G>,
items: &'a [LogGrouping<'a, G>],
time_range: RangeInclusive<OffsetDateTime>,
filter: &Filter,
) -> ScrollbarTable<'a> {
let header = G::Lines::HEADER
.iter()
.copied()
.chain([("Time", Alignment::Left), ("Count", Alignment::Left)]);
let header = header
.map(|(text, align)| Text::from(text).alignment(align))
.map(Cell::from)
.collect::<Row>()
.style(TABLE_HEADER_STYLE)
.height(1);
ScrollbarTable::new(
once(all)
.chain(items.iter().filter(|result| result.matches(filter)))
.map(|result| grouped_row(result, time_range.clone())),
G::Lines::WIDTHS,
)
.header(header)
}
fn grouped_row<'a, G: GroupingResult>(
grouping: &'a LogGrouping<'a, G>,
time_range: RangeInclusive<OffsetDateTime>,
) -> Row<'a> {
if let Some(match_result) = &grouping.result {
let grouping_columns = match_result.render();
let columns = grouping_columns.chain([
Cow::from(grouping.sparkline(time_range)),
Cow::from(grouping.count().to_string()),
]);
Row::new(columns).height(match_result.len() as u16)
} else {
Row::new([
Text::from(grouping.name.unwrap_or_default()),
Text::from(""),
Text::from(""),
Text::from(grouping.sparkline(time_range)),
Text::from(grouping.count().to_string()),
])
}
}

View file

@ -1,80 +0,0 @@
use crate::app::{App, Filter, LogMatch};
use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::ScrollbarTable;
use itertools::Either;
use ratatui::prelude::*;
use ratatui::widgets::{Cell, Row};
use std::fmt::Write;
use std::iter::{empty, once};
pub fn match_list<'a>(app: &'a App<'a>, filter: &Filter) -> ScrollbarTable<'a> {
let header = [
Text::from("Statement"),
Text::from("File"),
Text::from("Line").alignment(Alignment::Right),
Text::from("Time"),
Text::from("Count"),
]
.into_iter()
.map(Cell::from)
.collect::<Row>()
.style(TABLE_HEADER_STYLE)
.height(1);
let widths = [
Constraint::Percentage(70),
Constraint::Percentage(30),
Constraint::Min(6),
Constraint::Length(10),
Constraint::Min(10),
];
let all = log_row(&app.all, app, "All lines");
let unmatched = if app.unmatched.count() == 0 {
Either::Right(empty())
} else {
Either::Left(once(log_row(&app.unmatched, app, "Unmatched lines")))
};
ScrollbarTable::new(
once(all)
.chain(
app.matches
.iter()
.filter(|result| result.matches(filter))
.map(|result| log_row(result, app, "")),
)
.chain(unmatched),
widths,
)
.header(header)
}
fn log_row<'a>(result: &'a LogMatch, app: &'a App, name: &'static str) -> Row<'a> {
if let Some(match_result) = &result.result {
let mut message = String::new();
let mut paths = String::new();
let mut lines = String::new();
for statement in match_result.iter() {
writeln!(&mut message, "{}", statement.message()).ok();
writeln!(&mut paths, "{}", statement.path()).ok();
writeln!(&mut lines, "{}", statement.line()).ok();
}
Row::new([
Text::from(message),
Text::from(paths),
Text::from(lines).alignment(Alignment::Right),
Text::from(result.sparkline(app)),
Text::from(result.count().to_string()),
])
.height(match_result.count() as u16)
} else {
Row::new([
Text::from(name),
Text::from(""),
Text::from(""),
Text::from(result.sparkline(app)),
Text::from(result.count().to_string()),
])
}
}

View file

@ -1,11 +1,12 @@
use crate::app::App;
use crate::error::UiError;
use crate::matcher::MatchResult;
use crate::ui::error_list::error_list;
use crate::ui::footer::footer;
use crate::ui::grouped_logs::grouped_logs;
use crate::ui::grouping_list::grouping_list;
use crate::ui::histogram::UiHistogram;
use crate::ui::input::handle_events;
use crate::ui::match_list::match_list;
use crate::ui::single_log::single_log;
use crate::ui::single_match::grouped_lines;
use crate::ui::state::{
@ -27,9 +28,9 @@ use std::panic::{set_hook, take_hook};
mod error_list;
mod footer;
mod grouped_logs;
mod grouping_list;
mod histogram;
mod input;
mod match_list;
mod single_log;
mod single_match;
mod state;
@ -116,16 +117,18 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
}) => {
let selected = table_state.selected();
let histogram = if selected == 0 {
app.all.histogram(app)
} else if selected < app.matches.len() + 1 {
let log_match = &app.matches[selected - 1];
log_match.histogram(app)
app.all.histogram(app.time_range())
} else {
app.unmatched.histogram(app)
let log_match = &app.matches[selected - 1];
log_match.histogram(app.time_range())
};
frame.render_widget(UiHistogram::new(histogram), layout[0]);
frame.render_stateful_widget(match_list(app, filter), layout[1], table_state);
frame.render_stateful_widget(
grouping_list::<MatchResult>(&app.all, &app.matches, app.time_range(), filter),
layout[1],
table_state,
);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::Match(MatchState {
@ -141,7 +144,10 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
&result.grouped[selected - 1]
};
frame.render_widget(UiHistogram::new(selected_group.histogram(app)), layout[0]);
frame.render_widget(
UiHistogram::new(selected_group.histogram(app.time_range())),
layout[0],
);
frame.render_stateful_widget(
grouped_lines(app, result, filter),
layout[1],

View file

@ -1,4 +1,6 @@
use crate::app::{App, Filter, LineSet, LogMatch};
use crate::app::{App, Filter, LineSet};
use crate::grouping::LogGrouping;
use crate::matcher::MatchResult;
use crate::ui::style::TABLE_HEADER_STYLE;
use crate::ui::table::{ScrollbarTable, ScrollbarTableState};
use ratatui::buffer::Buffer;
@ -7,10 +9,12 @@ use ratatui::prelude::StatefulWidget;
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
use std::iter::once;
use std::ops::RangeInclusive;
use time::OffsetDateTime;
pub fn grouped_lines<'a>(
app: &'a App<'a>,
log_match: &'a LogMatch,
log_match: &'a LogGrouping<'a, MatchResult>,
filter: &'a Filter,
) -> SingleMatchTable<'a> {
SingleMatchTable {
@ -22,7 +26,7 @@ pub fn grouped_lines<'a>(
pub struct SingleMatchTable<'a> {
app: &'a App<'a>,
log_match: &'a LogMatch<'a>,
log_match: &'a LogGrouping<'a, MatchResult>,
filter: &'a Filter,
}
@ -59,7 +63,7 @@ impl StatefulWidget for SingleMatchTable<'_> {
Text::from("All lines"),
Text::from(""),
Text::from(""),
Text::from(self.log_match.sparkline(self.app)),
Text::from(self.log_match.sparkline(self.app.time_range())),
Text::from(self.log_match.count().to_string()),
]))
.chain(
@ -68,7 +72,11 @@ impl StatefulWidget for SingleMatchTable<'_> {
.filter(|group| group.matches(self.filter))
.enumerate()
.map(|(i, group)| {
group_row(self.app, group, i.abs_diff(state.selected()) < 100)
group_row(
self.app.time_range(),
group,
i.abs_diff(state.selected()) < 100,
)
}),
),
widths,
@ -78,7 +86,11 @@ impl StatefulWidget for SingleMatchTable<'_> {
}
}
fn group_row<'a>(app: &'a App, group: &'a LineSet, is_in_view: bool) -> Row<'a> {
fn group_row<'a>(
time_range: RangeInclusive<OffsetDateTime>,
group: &'a LineSet,
is_in_view: bool,
) -> Row<'a> {
if is_in_view {
let line = group.lines[0];
@ -86,7 +98,7 @@ fn group_row<'a>(app: &'a App, group: &'a LineSet, is_in_view: bool) -> Row<'a>
Text::from(line.level.as_str()),
Text::from(line.app.as_ref()),
Text::from(line.display()),
Text::from(group.sparkline(app)),
Text::from(group.sparkline(time_range)),
Text::from(group.len().to_string()),
])
} else {

View file

@ -1,6 +1,8 @@
use crate::app::{App, Filter, LogMatch, EMPTY_FILTER};
use crate::app::{App, Filter, EMPTY_FILTER};
use crate::error::ParseError;
use crate::grouping::LogGrouping;
use crate::logfile::logline::{FullLogLine, LogLine};
use crate::matcher::MatchResult;
use crate::ui::footer::FooterParams;
use crate::ui::input::{PopMode, UiEvent};
use crate::ui::table::ScrollbarTableState;
@ -45,19 +47,16 @@ impl<'a> MatchListState<'a> {
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> {
let result = if selected == 0 {
&app.all
} else if selected <= app.matches.len() {
if self.filter.is_empty() {
&app.matches[selected - 1]
} else {
app.matches
.iter()
.filter(|log_match| log_match.matches(&self.filter))
.nth(selected - 1)
.unwrap_or(&app.unmatched)
}
} else if self.filter.is_empty() {
&app.matches[selected - 1]
} else {
&app.unmatched
app.matches
.iter()
.filter(|log_match| log_match.matches(&self.filter))
.nth(selected - 1)
.unwrap_or(app.matches.last().unwrap())
};
let table_state = ScrollbarTableState::new(result.grouped.len() + 1);
UiState::Match(MatchState {
result,
@ -77,7 +76,7 @@ impl PartialEq for MatchListState<'_> {
#[derive(Clone)]
pub struct MatchState<'a> {
pub result: &'a LogMatch<'a>,
pub result: &'a LogGrouping<'a, MatchResult>,
pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>,
pub filter: Filter,