add filtering

This commit is contained in:
Robin Appelman 2024-12-17 00:30:13 +01:00
commit ec19270ecd
9 changed files with 307 additions and 62 deletions

View file

@ -2,7 +2,7 @@ use crate::logfile::LogFile;
use crate::logline::LogLine;
use crate::matcher::MatchResult;
use crate::timegraph::TimeGraph;
use logsmash_data::StatementList;
use logsmash_data::{LoggingStatementWithPathPrefix, StatementList};
use std::collections::BTreeMap;
pub struct App<'a> {
@ -62,6 +62,24 @@ impl LogMatch {
pub fn row_count(&self) -> usize {
self.result.as_ref().map(|res| res.len()).unwrap_or(1)
}
pub fn statements<'a>(
&'a self,
app: &'a App,
) -> impl Iterator<Item = LoggingStatementWithPathPrefix> + 'a {
self.result
.iter()
.flat_map(|res| res.iter())
.filter_map(|index| app.log_statements.get(index))
}
pub fn matches(&self, app: &App, filter: &str) -> bool {
if filter.is_empty() {
return true;
}
self.statements(app)
.any(|statement| statement.pattern.contains(filter))
}
}
impl LogMatch {
@ -111,4 +129,12 @@ impl GroupedLines {
pub fn len(&self) -> usize {
self.lines.len()
}
pub fn matches(&self, app: &App, filter: &str) -> bool {
if filter.is_empty() {
return true;
}
let line = &app.lines[self.lines[0]];
line.message.contains(filter)
}
}

View file

@ -60,7 +60,10 @@ impl<R: Read + Seek> ArchiveEntry for ZipEntry<'_, R> {
}
impl<R: Read + Seek> Archive for ZipArchive<R> {
type Entry<'a> = ZipEntry<'a, R> where R: 'a;
type Entry<'a>
= ZipEntry<'a, R>
where
R: 'a;
fn entries(&mut self) -> impl Iterator<Item = Self::Entry<'_>> {
let names = self
@ -114,7 +117,10 @@ impl ArchiveEntry for TarEntry {
}
impl<R: Read> Archive for TarArchive<R> {
type Entry<'a> = TarEntry where R: 'a;
type Entry<'a>
= TarEntry
where
R: 'a;
fn entries(&mut self) -> impl Iterator<Item = Self::Entry<'_>> {
match self.0.entries() {

View file

@ -125,6 +125,14 @@ impl<'a> LogLine<'a> {
Cow::Borrowed(&self.message)
}
}
pub fn matches(&self, filter: &str) -> bool {
if filter.is_empty() {
return true;
}
// todo: reqid, more?
self.app.contains(filter) || self.message.contains(filter)
}
}
#[derive(Deserialize, Debug, Hash)]

View file

@ -6,33 +6,58 @@ use ratatui::style::palette::tailwind;
use ratatui::text::Text;
use ratatui::widgets::{Row, Table};
pub fn footer<'a>(app: &App<'a>, page: UiPage) -> Table<'a> {
pub enum FooterParams<'a> {
Normal(UiPage),
FilterInput(&'a str),
}
pub fn footer<'a>(app: &App<'a>, params: FooterParams<'a>) -> Table<'a> {
let footer_style = Style::default()
.bg(tailwind::BLACK)
.fg(tailwind::GREEN.c600);
let widths = [
Constraint::Percentage(100),
Constraint::Min(25),
Constraint::Min(20),
];
match params {
FooterParams::Normal(page) => {
let widths = [
Constraint::Percentage(100),
Constraint::Min(25),
Constraint::Min(20),
];
Table::new(
[Row::new([
Text::from(help(page)),
Text::from(format!("{} unmatched items", app.unmatched.lines.len())),
Text::from(format!("{} parse errors", app.error_count)),
])],
widths,
)
.style(footer_style)
Table::new(
[Row::new([
Text::from(help(page)),
Text::from(format!("{} unmatched items", app.unmatched.lines.len())),
Text::from(format!("{} parse errors", app.error_count)),
])],
widths,
)
.style(footer_style)
}
FooterParams::FilterInput(filter_input) => {
let help = "«Esc» Clear - «Left» Back";
let widths = [
Constraint::Min(u16::try_from(help.chars().count()).unwrap()),
Constraint::Percentage(100),
];
Table::new(
[Row::new([
Text::from(help),
Text::from(format!("- Filter: {}", filter_input)),
])],
widths,
)
.style(footer_style)
}
}
}
fn help(page: UiPage) -> &'static str {
match page {
UiPage::MatchList => "«Q» Exit - «Enter» Select - «E» Show parse errors",
UiPage::Match => "«Q» Exit - «Enter» Select - «Esc» Back",
UiPage::Logs => "«Q» Exit - «Esc» Back - «C» Copy log line",
UiPage::MatchList => "«Q» Exit - «Enter» Select - «F4» Filter - «E» Show parse errors",
UiPage::Match => "«Q» Exit - «Enter» Select - «F4» Filter - «Esc» Back",
UiPage::Logs => "«Q» Exit - «F4» Filter - «Esc» Back - «C» Copy log line",
UiPage::Log => "«Q» Exit - «Esc» Back - «R» Toggle raw - «C» Copy log line",
UiPage::Errors => "«Q» Exit - «Esc» Back - «C» Copy log line",
}

View file

@ -7,7 +7,7 @@ use ratatui::widgets::{Cell, Row};
use std::fmt::Write;
use std::iter::{empty, once};
pub fn match_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> {
pub fn match_list<'a>(app: &'a App<'a>, filter: &str) -> ScrollbarTable<'a> {
let header = [
Text::from("Statement"),
Text::from("File"),
@ -38,7 +38,12 @@ pub fn match_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> {
ScrollbarTable::new(
once(all)
.chain(app.matches.iter().map(|result| log_row(result, app, "")))
.chain(
app.matches
.iter()
.filter(|result| result.matches(app, filter))
.map(|result| log_row(result, app, "")),
)
.chain(unmatched),
widths,
)

View file

@ -8,7 +8,7 @@ use crate::ui::raw_logs::raw_logs;
use crate::ui::single_log::single_log;
use crate::ui::single_match::grouped_lines;
use crate::ui::state::{
ErrorState, LogState, LogsState, MatchListState, MatchState, UiEvent, UiPage, UiState,
ErrorState, LogState, LogsState, MatchListState, MatchState, Mode, UiEvent, UiPage, UiState,
};
use ratatui::crossterm::event::{
DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers, MouseButton,
@ -84,22 +84,31 @@ fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEvent>
if event::poll(Duration::from_millis(50))? {
match event::read()? {
Event::Key(key) if key.kind == event::KeyEventKind::Press => {
return Ok(match key.code {
KeyCode::Char('c') if key.modifiers == KeyModifiers::CONTROL => {
return Ok(match (ui_state.mode(), key.code) {
(_, KeyCode::Char('c')) if key.modifiers == KeyModifiers::CONTROL => {
Some(UiEvent::Quit)
}
KeyCode::Char('q') => Some(UiEvent::Quit),
KeyCode::Esc => Some(UiEvent::Back),
KeyCode::Char('e') if page == UiPage::MatchList => Some(UiEvent::Errors),
KeyCode::Left if page != UiPage::MatchList => Some(UiEvent::Back),
KeyCode::Down => Some(UiEvent::Down(1, true)),
KeyCode::Up => Some(UiEvent::Up(1, true)),
KeyCode::PageDown => Some(UiEvent::Down(10, false)),
KeyCode::PageUp => Some(UiEvent::Up(10, false)),
KeyCode::End => Some(UiEvent::Down(usize::MAX, false)),
KeyCode::Home => Some(UiEvent::Up(usize::MAX, false)),
KeyCode::Enter | KeyCode::Right => Some(UiEvent::Select),
KeyCode::Char('c') => Some(UiEvent::Copy),
(Mode::Normal, KeyCode::Esc) => Some(UiEvent::Back),
(Mode::Normal, KeyCode::Char('q')) => Some(UiEvent::Quit),
(Mode::Normal, KeyCode::Char('e')) if page == UiPage::MatchList => {
Some(UiEvent::Errors)
}
(_, KeyCode::Left) if page != UiPage::MatchList => Some(UiEvent::Back),
(_, KeyCode::Down) => Some(UiEvent::Down(1, true)),
(_, KeyCode::Up) => Some(UiEvent::Up(1, true)),
(_, KeyCode::PageDown) => Some(UiEvent::Down(10, false)),
(_, KeyCode::PageUp) => Some(UiEvent::Up(10, false)),
(_, KeyCode::End) => Some(UiEvent::Down(usize::MAX, false)),
(_, KeyCode::Home) => Some(UiEvent::Up(usize::MAX, false)),
(_, KeyCode::Enter | KeyCode::Right) => Some(UiEvent::Select),
(Mode::Normal, KeyCode::Char('c')) => Some(UiEvent::Copy),
(Mode::Normal, KeyCode::F(4)) => Some(UiEvent::EnterFilterMode),
(Mode::FilterInput, KeyCode::Esc) => Some(UiEvent::ClearFilter),
(Mode::FilterInput, KeyCode::F(4)) => Some(UiEvent::Back),
(Mode::FilterInput, KeyCode::Backspace) => Some(UiEvent::Backspace),
(Mode::FilterInput, KeyCode::Char(c)) => Some(UiEvent::Text(c)),
_ => None,
});
}
@ -135,7 +144,6 @@ fn find_hit_row(row: u16, ui_state: &UiState) -> Option<usize> {
const UI_HEADER_SIZE: u16 = 5;
fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
let page = state.page();
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
@ -147,7 +155,11 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
match state {
UiState::Quit => {}
UiState::MatchList(MatchListState { table_state, .. }) => {
UiState::MatchList(MatchListState {
table_state,
filter,
..
}) => {
let selected = table_state.selected();
let histogram = if selected == 0 {
&app.all.histogram
@ -159,29 +171,37 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
};
frame.render_widget(UiHistogram::new(histogram), layout[0]);
frame.render_stateful_widget(match_list(app), layout[1], table_state);
frame.render_widget(footer(app, page), layout[2]);
frame.render_stateful_widget(match_list(app, filter), layout[1], table_state);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::Match(MatchState {
result,
table_state,
filter,
..
}) => {
let selected_group = &result.grouped[table_state.selected()];
frame.render_widget(UiHistogram::new(&selected_group.histogram), layout[0]);
frame.render_stateful_widget(grouped_lines(app, result), layout[1], table_state);
frame.render_widget(footer(app, page), layout[2]);
frame.render_stateful_widget(
grouped_lines(app, result, filter),
layout[1],
table_state,
);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::Logs(LogsState {
lines, table_state, ..
lines,
table_state,
filter,
..
}) => {
frame.render_stateful_widget(
raw_logs(app, lines),
raw_logs(app, lines, filter),
layout[0].union(layout[1]),
table_state,
);
frame.render_widget(footer(app, page), layout[2]);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::Log(LogState {
table_state,
@ -193,11 +213,11 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
layout[0].union(layout[1]),
table_state,
);
frame.render_widget(footer(app, page), layout[2]);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::Errors(ErrorState { table_state, .. }) => {
frame.render_stateful_widget(error_list(app), layout[0].union(layout[1]), table_state);
frame.render_widget(footer(app, page), layout[2]);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
}
}

View file

@ -6,7 +6,7 @@ use ratatui::layout::{Alignment, Constraint};
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize]) -> ScrollbarTable<'a> {
pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize], filter: &str) -> ScrollbarTable<'a> {
let lines = lines.iter().copied().map(|i| &app.lines[i]);
let header = [
Text::from("Level"),
@ -26,7 +26,11 @@ pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize]) -> ScrollbarTable<'a> {
Constraint::Percentage(100),
Constraint::Length(27),
];
ScrollbarTable::new(lines.map(log_row), widths).header(header)
ScrollbarTable::new(
lines.filter(|line| line.matches(filter)).map(log_row),
widths,
)
.header(header)
}
fn log_row<'a>(line: &'a LogLine<'a>) -> Row<'a> {

View file

@ -5,7 +5,11 @@ use ratatui::layout::Constraint;
use ratatui::text::Text;
use ratatui::widgets::{Cell, Row};
pub fn grouped_lines<'a>(app: &'a App<'a>, log_match: &'a LogMatch) -> ScrollbarTable<'a> {
pub fn grouped_lines<'a>(
app: &'a App<'a>,
log_match: &'a LogMatch,
filter: &str,
) -> ScrollbarTable<'a> {
let grouped = &log_match.grouped;
let header = [
Text::from("Level"),
@ -27,7 +31,14 @@ pub fn grouped_lines<'a>(app: &'a App<'a>, log_match: &'a LogMatch) -> Scrollbar
Constraint::Length(10),
Constraint::Min(10),
];
ScrollbarTable::new(grouped.iter().map(|group| group_row(app, group)), widths).header(header)
ScrollbarTable::new(
grouped
.iter()
.filter(|group| group.matches(app, filter))
.map(|group| group_row(app, group)),
widths,
)
.header(header)
}
fn group_row<'a>(app: &'a App, group: &'a GroupedLines) -> Row<'a> {

View file

@ -1,5 +1,6 @@
use crate::app::{App, LogMatch};
use crate::logline::{FullLogLine, LogLine};
use crate::ui::footer::FooterParams;
use crate::ui::table::ScrollbarTableState;
use crate::ui::UI_HEADER_SIZE;
use crate::{copy_osc, parse_line_full};
@ -17,10 +18,18 @@ pub enum UiState<'a> {
Quit,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Mode {
Normal,
FilterInput,
}
#[derive(Clone)]
pub struct MatchListState<'a> {
app: &'a App<'a>,
pub table_state: ScrollbarTableState,
pub filter: String,
mode: Mode,
}
impl<'a> MatchListState<'a> {
@ -32,7 +41,15 @@ impl<'a> MatchListState<'a> {
let result = if selected == 0 {
&app.all
} else if selected <= app.matches.len() {
&app.matches[selected - 1]
if self.filter.is_empty() {
&app.matches[selected - 1]
} else {
app.matches
.iter()
.filter(|log_match| log_match.matches(app, &self.filter))
.nth(selected - 1)
.expect("filtered select out of bounds")
}
} else {
&app.unmatched
};
@ -41,6 +58,8 @@ impl<'a> MatchListState<'a> {
result,
table_state,
previous: Box::new(self.into()),
filter: String::new(),
mode: Mode::Normal,
})
}
}
@ -56,6 +75,8 @@ pub struct MatchState<'a> {
pub result: &'a LogMatch,
pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>,
pub filter: String,
mode: Mode,
}
impl<'a> MatchState<'a> {
@ -63,16 +84,28 @@ impl<'a> MatchState<'a> {
self.table_state.selected()
}
fn enter(self, selected: usize) -> UiState<'a> {
fn enter(self, selected: usize, app: &'a App) -> UiState<'a> {
let mut table_state = TableState::default();
table_state.select(Some(0));
let lines = self.result.grouped[selected].lines.as_slice();
let selected_line = if self.filter.is_empty() {
&self.result.grouped[selected]
} else {
self.result
.grouped
.iter()
.filter(|grouped| grouped.matches(app, &self.filter))
.nth(selected)
.expect("filtered select out of bounds")
};
let lines = selected_line.lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len());
UiState::Logs(LogsState {
lines,
table_state,
previous: Box::new(self.into()),
filter: String::new(),
mode: Mode::Normal,
})
}
}
@ -88,6 +121,8 @@ pub struct LogsState<'a> {
pub lines: &'a [usize],
pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>,
pub filter: String,
mode: Mode,
}
impl<'a> LogsState<'a> {
@ -96,7 +131,17 @@ impl<'a> LogsState<'a> {
}
fn enter(self, selected: usize, app: &'a App<'a>) -> UiState<'a> {
let line = self.lines[selected];
let line = if self.filter.is_empty() {
self.lines[selected]
} else {
self.lines
.iter()
.map(|index| &app.lines[*index])
.filter(|line| line.matches(&self.filter))
.nth(selected)
.map(|line| line.index)
.expect("filtered select out of bounds")
};
let log = &app.lines[line];
let raw_line = app.get_line(log.index).unwrap();
let full_line = parse_line_full(raw_line).unwrap();
@ -155,6 +200,8 @@ impl<'a> UiState<'a> {
UiState::MatchList(MatchListState {
app,
table_state: ScrollbarTableState::new(app.match_lines()),
filter: String::new(),
mode: Mode::Normal,
})
}
@ -168,6 +215,42 @@ impl<'a> UiState<'a> {
}
}
pub fn mode(&self) -> Mode {
match self {
UiState::MatchList(state) => state.mode,
UiState::Match(state) => state.mode,
UiState::Logs(state) => state.mode,
_ => Mode::Normal,
}
}
pub fn set_mode(&mut self, mode: Mode) {
match self {
UiState::MatchList(state) => state.mode = mode,
UiState::Match(state) => state.mode = mode,
UiState::Logs(state) => state.mode = mode,
_ => {}
}
}
pub fn filter(&self) -> Option<&str> {
match self {
UiState::MatchList(state) => Some(&state.filter),
UiState::Match(state) => Some(&state.filter),
UiState::Logs(state) => Some(&state.filter),
_ => None,
}
}
pub fn filter_mut(&mut self) -> Option<&mut String> {
match self {
UiState::MatchList(state) => Some(&mut state.filter),
UiState::Match(state) => Some(&mut state.filter),
UiState::Logs(state) => Some(&mut state.filter),
_ => None,
}
}
fn table_state(&self) -> Option<&ScrollbarTableState> {
match self {
UiState::MatchList(state) => Some(&state.table_state),
@ -208,9 +291,13 @@ impl<'a> UiState<'a> {
pub fn index_for_row(&self, row: usize) -> usize {
match self {
UiState::MatchList(MatchListState { app, .. }) => {
UiState::MatchList(MatchListState { app, filter, .. }) => {
let mut total_height = 0;
let match_row_counts = app.matches.iter().map(|m| m.row_count());
let match_row_counts = app
.matches
.iter()
.filter(|m| m.matches(app, filter))
.map(|m| m.row_count());
for (index, row_count) in once(1)
.chain(match_row_counts)
.chain(once(1))
@ -248,7 +335,12 @@ impl<'a> UiState<'a> {
match (self, event) {
(UiState::Quit, _) => (true, UiState::Quit),
(_, UiEvent::Quit) => (true, UiState::Quit),
(UiState::MatchList(_), UiEvent::Back) => (true, UiState::Quit),
(
UiState::MatchList(MatchListState {
mode: Mode::Normal, ..
}),
UiEvent::Back,
) => (true, UiState::Quit),
(mut state, UiEvent::Down(step, rollover)) => {
if let Some(table_state) = state.table_state_mut() {
table_state.down(step, rollover);
@ -292,9 +384,9 @@ impl<'a> UiState<'a> {
}
(UiState::Match(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected))
(true, state.enter(selected, app))
}
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
(UiState::Match(state), UiEvent::Enter(selected)) => (true, state.enter(selected, app)),
(UiState::Logs(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected, app))
@ -324,6 +416,43 @@ impl<'a> UiState<'a> {
copy_osc(raw);
(false, UiState::Errors(state))
}
(mut ui, UiEvent::EnterFilterMode) if ui.mode() != Mode::FilterInput => {
ui.set_mode(Mode::FilterInput);
(true, ui)
}
(mut ui, UiEvent::Text(c)) if ui.mode() == Mode::FilterInput => {
if let Some(filter) = ui.filter_mut() {
filter.push(c);
}
(true, ui)
}
(mut ui, UiEvent::Backspace) if ui.mode() == Mode::FilterInput => {
if let Some(filter) = ui.filter_mut() {
filter.pop();
}
(true, ui)
}
(mut ui, UiEvent::ClearFilter) if ui.mode() != Mode::Normal => {
if let Some(filter) = ui.filter_mut() {
filter.clear();
}
ui.set_mode(Mode::Normal);
(true, ui)
}
(
mut ui @ UiState::MatchList(MatchListState {
mode: Mode::FilterInput,
..
}),
UiEvent::Back,
) => {
if let Some(filter) = ui.filter_mut() {
filter.clear();
}
ui.set_mode(Mode::Normal);
(true, ui)
}
(
UiState::Match(MatchState { previous, .. })
| UiState::Logs(LogsState { previous, .. })
@ -334,6 +463,13 @@ impl<'a> UiState<'a> {
(state, _) => (false, state),
}
}
pub fn footer_params(&self) -> FooterParams {
match self.mode() {
Mode::Normal => FooterParams::Normal(self.page()),
Mode::FilterInput => FooterParams::FilterInput(self.filter().unwrap_or_default()),
}
}
}
pub enum UiEvent {
@ -348,6 +484,10 @@ pub enum UiEvent {
SelectAt(usize),
Enter(usize),
Copy,
EnterFilterMode,
ClearFilter,
Text(char),
Backspace,
}
#[derive(PartialEq)]