group by for distinct logs

This commit is contained in:
Robin Appelman 2025-08-16 15:16:38 +02:00
commit 3982c2e354
4 changed files with 80 additions and 43 deletions

View file

@ -165,6 +165,7 @@ impl<'logs> GroupingResult<'logs> for Groupings<'logs> {
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum GroupingOptions {
Url,
App,

View file

@ -57,8 +57,8 @@ fn help(page: UiPage) -> &'static str {
match page {
UiPage::GroupList => "«Q» Exit - «Enter» Select - «F» Filter - «E» Show parse errors",
UiPage::Group => "«Q» Exit - «Enter» Select - «F» Filter - «G» Group By - «Esc» Back",
UiPage::Logs => {
"«Q» Exit - «F» Filter - «Esc» Back - «C» Copy log line - «R» Show logs for request"
UiPage::DistinctLogs => {
"«Q» Exit - «F» Filter - «Esc» Back - «C» Copy log line - «G» Group By - «R» Show logs for request"
}
UiPage::Log => {
"«Q» Exit - «Esc» Back - «R» Toggle raw - «C» Copy log line - «R» Show logs for request"

View file

@ -1,6 +1,6 @@
use crate::app::App;
use crate::error::UiError;
use crate::grouping::{GroupingOptions, LogGrouping};
use crate::grouping::LogGrouping;
use crate::ui::by_identifier::logs_by_identifier;
use crate::ui::error_list::error_list;
use crate::ui::footer::footer;
@ -11,8 +11,8 @@ use crate::ui::list::SelectList;
use crate::ui::single_group::single_group;
use crate::ui::single_log::single_log;
use crate::ui::state::{
ErrorLinesState, ErrorState, GroupByMenuState, GroupListState, GroupState, LogState,
LogsByIdentifierState, UiState,
DistinctLogsState, ErrorLinesState, ErrorState, GroupByMenuState, GroupListState, GroupState,
LogState, UiState,
};
use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use ratatui::crossterm::terminal::{
@ -162,21 +162,23 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::GroupByMenu(GroupByMenuState {
options,
previous,
list_state,
..
}) => {
ui(frame, app, previous);
let area = center(
frame.area(),
Constraint::Percentage(20),
Constraint::Length(GroupingOptions::all().count() as u16 + 2), // top and bottom border + content
Constraint::Length(options.len() as u16 + 2), // top and bottom border + content
);
let popup = SelectList::new(GroupingOptions::all().map(|option| option.as_str()))
let popup = SelectList::new(options.iter().map(|option| option.as_str()))
.block(Block::bordered().title("Group By"));
frame.render_widget(Clear, area);
frame.render_stateful_widget(popup, area, list_state);
}
UiState::ByIdentifier(LogsByIdentifierState {
UiState::Distinct(DistinctLogsState {
lines,
table_state,
filter,

View file

@ -19,7 +19,7 @@ pub enum UiState<'a> {
GroupList(GroupListState<'a>),
Group(GroupState<'a>),
GroupByMenu(GroupByMenuState<'a>),
ByIdentifier(LogsByIdentifierState<'a>),
Distinct(DistinctLogsState<'a>),
Log(LogState<'a>),
Errors(ErrorLinesState<'a>),
Error(ErrorState<'a>),
@ -35,7 +35,7 @@ impl<'a> UiState<'a> {
previous: Some(_),
..
})
| UiState::ByIdentifier(LogsByIdentifierState { .. })
| UiState::Distinct(DistinctLogsState { .. })
| UiState::Log(LogState { .. })
| UiState::Errors(ErrorLinesState { .. })
)
@ -54,6 +54,7 @@ pub struct GroupListState<'a> {
pub table_state: ScrollbarTableState,
pub filter: Filter,
pub previous: Option<Box<UiState<'a>>>,
pub next: UiPage,
mode: Mode,
}
@ -75,14 +76,28 @@ impl<'a> GroupListState<'a> {
.clone()
};
let table_state = ScrollbarTableState::new(result.by_identifier().len() + 1);
UiState::Group(GroupState {
result,
table_state,
previous: Box::new(self.into()),
filter: Filter::default(),
mode: Mode::Normal,
})
let line_count = result.lines.lines.len();
match self.next {
UiPage::Group => {
let result_len = result.by_identifier().len();
UiState::Group(GroupState {
result,
table_state: ScrollbarTableState::new(result_len + 1),
previous: Box::new(self.into()),
filter: Filter::default(),
mode: Mode::Normal,
})
}
UiPage::DistinctLogs => UiState::Distinct(DistinctLogsState {
table_state: ScrollbarTableState::new(line_count + 1),
lines: result.lines.lines,
grouping: GroupedLogGrouping::Message,
previous: Box::new(self.into()),
filter: Filter::default(),
mode: Mode::Normal,
}),
_ => panic!("invalid next for grouped list"),
}
}
}
@ -93,8 +108,10 @@ impl PartialEq for GroupListState<'_> {
}
pub struct GroupByMenuState<'a> {
pub options: Vec<GroupingOptions>,
pub previous: Box<UiState<'a>>,
pub list_state: SizedListState,
pub next: UiPage,
}
impl PartialEq for GroupByMenuState<'_> {
@ -109,12 +126,13 @@ impl<'a> GroupByMenuState<'a> {
}
pub fn enter(self, selected: usize) -> UiState<'a> {
let group_option = GroupingOptions::all().nth(selected).unwrap();
let group_option = self.options[selected];
let lines = match self.previous.as_ref() {
UiState::Group(group) => &group.result.lines,
_ => panic!("Group by called from non-group"),
UiState::Group(group) => &group.result.lines.lines,
UiState::Distinct(group) => &group.lines,
_ => panic!("Group by called from invalid state"),
};
let items = group_option.group_by(lines.lines.clone());
let items = group_option.group_by(lines.clone());
let count = items.len();
UiState::GroupList(GroupListState {
@ -124,6 +142,7 @@ impl<'a> GroupByMenuState<'a> {
mode: Mode::Normal,
table_state: ScrollbarTableState::new(count),
previous: Some(self.previous),
next: self.next,
})
}
}
@ -143,8 +162,10 @@ impl<'a> GroupState<'a> {
fn group_by_menu(self) -> UiState<'a> {
UiState::GroupByMenu(GroupByMenuState {
options: GroupingOptions::all().collect(),
previous: Box::new(self.into()),
list_state: SizedListState::new(GroupingOptions::all().count()),
next: UiPage::Group,
})
}
@ -169,7 +190,7 @@ impl<'a> GroupState<'a> {
};
let lines = selected_line.lines.as_slice();
let table_state = ScrollbarTableState::new(lines.len());
UiState::ByIdentifier(LogsByIdentifierState {
UiState::Distinct(DistinctLogsState {
lines: lines.into(),
table_state,
previous: Box::new(self.into()),
@ -192,7 +213,7 @@ pub enum GroupedLogGrouping {
Request,
}
pub struct LogsByIdentifierState<'a> {
pub struct DistinctLogsState<'a> {
pub lines: Vec<&'a LogLine<'a>>,
pub table_state: ScrollbarTableState,
pub previous: Box<UiState<'a>>,
@ -201,7 +222,7 @@ pub struct LogsByIdentifierState<'a> {
pub grouping: GroupedLogGrouping,
}
impl<'a> LogsByIdentifierState<'a> {
impl<'a> DistinctLogsState<'a> {
fn selected(&self) -> usize {
self.table_state.selected()
}
@ -219,6 +240,17 @@ impl<'a> LogsByIdentifierState<'a> {
}
}
fn group_by_menu(self) -> UiState<'a> {
UiState::GroupByMenu(GroupByMenuState {
options: GroupingOptions::all()
.filter(|option| *option != GroupingOptions::App) // app is already unique
.collect(),
previous: Box::new(self.into()),
list_state: SizedListState::new(GroupingOptions::all().count()),
next: UiPage::DistinctLogs,
})
}
fn enter(self, selected: usize, app: &App<'a>) -> UiState<'a> {
let log = self.get_selected(selected);
let raw_line = app.get_source_line(log.line_number).unwrap();
@ -254,7 +286,7 @@ impl<'a> LogsByIdentifierState<'a> {
let lines: Vec<_> = app.lines_by_request(&log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len());
UiState::ByIdentifier(LogsByIdentifierState {
UiState::Distinct(DistinctLogsState {
lines,
mode: Mode::Normal,
filter: Filter::default(),
@ -265,7 +297,7 @@ impl<'a> LogsByIdentifierState<'a> {
}
}
impl PartialEq for LogsByIdentifierState<'_> {
impl PartialEq for DistinctLogsState<'_> {
fn eq(&self, other: &Self) -> bool {
self.lines == other.lines
}
@ -294,7 +326,7 @@ impl<'a> LogState<'a> {
let lines: Vec<_> = app.lines_by_request(&self.log.request_id).collect();
let table_state = ScrollbarTableState::new(lines.len());
UiState::ByIdentifier(LogsByIdentifierState {
UiState::Distinct(DistinctLogsState {
lines,
mode: Mode::Normal,
filter: Filter::default(),
@ -323,6 +355,7 @@ impl<'a> UiState<'a> {
filter: Filter::default(),
mode: Mode::Normal,
previous: None,
next: UiPage::Group,
})
}
@ -331,7 +364,7 @@ impl<'a> UiState<'a> {
UiState::Quit | UiState::GroupList(_) => UiPage::GroupList,
UiState::Group(_) => UiPage::Group,
UiState::GroupByMenu(_) => UiPage::Group, // todo
UiState::ByIdentifier(_) => UiPage::Logs,
UiState::Distinct(_) => UiPage::DistinctLogs,
UiState::Log(_) => UiPage::Log,
UiState::Errors(_) => UiPage::Errors,
UiState::Error(_) => UiPage::Error,
@ -342,7 +375,7 @@ impl<'a> UiState<'a> {
match self {
UiState::GroupList(state) => state.mode,
UiState::Group(state) => state.mode,
UiState::ByIdentifier(state) => state.mode,
UiState::Distinct(state) => state.mode,
_ => Mode::Normal,
}
}
@ -351,7 +384,7 @@ impl<'a> UiState<'a> {
match self {
UiState::GroupList(state) => state.mode = mode,
UiState::Group(state) => state.mode = mode,
UiState::ByIdentifier(state) => state.mode = mode,
UiState::Distinct(state) => state.mode = mode,
_ => {}
}
}
@ -360,7 +393,7 @@ impl<'a> UiState<'a> {
match self {
UiState::GroupList(state) => Some(&state.filter),
UiState::Group(state) => Some(&state.filter),
UiState::ByIdentifier(state) => Some(&state.filter),
UiState::Distinct(state) => Some(&state.filter),
_ => None,
}
}
@ -369,7 +402,7 @@ impl<'a> UiState<'a> {
match self {
UiState::GroupList(state) => Some(&mut state.filter),
UiState::Group(state) => Some(&mut state.filter),
UiState::ByIdentifier(state) => Some(&mut state.filter),
UiState::Distinct(state) => Some(&mut state.filter),
_ => None,
}
}
@ -378,7 +411,7 @@ impl<'a> UiState<'a> {
match self {
UiState::GroupList(state) => Some(&state.table_state),
UiState::Group(state) => Some(&state.table_state),
UiState::ByIdentifier(state) => Some(&state.table_state),
UiState::Distinct(state) => Some(&state.table_state),
UiState::Log(state) => Some(&state.table_state),
UiState::Errors(state) => Some(&state.table_state),
_ => None,
@ -389,7 +422,7 @@ impl<'a> UiState<'a> {
match self {
UiState::GroupList(state) => Some(&mut state.table_state),
UiState::Group(state) => Some(&mut state.table_state),
UiState::ByIdentifier(state) => Some(&mut state.table_state),
UiState::Distinct(state) => Some(&mut state.table_state),
UiState::Log(state) => Some(&mut state.table_state),
UiState::Errors(state) => Some(&mut state.table_state),
_ => None,
@ -453,7 +486,7 @@ impl<'a> UiState<'a> {
match self {
UiState::GroupList(_) => UI_HEADER_SIZE + 1,
UiState::Group(_) => UI_HEADER_SIZE + 1,
UiState::ByIdentifier(_) => UI_HEADER_SIZE + 1,
UiState::Distinct(_) => UI_HEADER_SIZE + 1,
UiState::GroupByMenu(_) => 0,
UiState::Log(_) => 0,
UiState::Errors(_) => 0,
@ -538,14 +571,14 @@ impl<'a> UiState<'a> {
let selected = state.selected();
(true, state.enter(selected))
}
(UiState::ByIdentifier(state), UiEvent::Select) => {
(UiState::Distinct(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected, app))
}
(UiState::ByIdentifier(state), UiEvent::Enter(selected)) => {
(UiState::Distinct(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected, app))
}
(UiState::ByIdentifier(state), UiEvent::Copy) => {
(UiState::Distinct(state), UiEvent::Copy) => {
let selected = state.selected();
let mut table_state = TableState::default();
table_state.select(Some(0));
@ -553,8 +586,9 @@ impl<'a> UiState<'a> {
let line = state.lines[selected];
let raw = app.get_source_line(line.line_number).unwrap_or_default();
copy_osc(raw);
(false, UiState::ByIdentifier(state))
(false, UiState::Distinct(state))
}
(UiState::Distinct(state), UiEvent::GroupBy) => (true, state.group_by_menu()),
(UiState::Log(state), UiEvent::Copy) => {
let raw = app
.get_source_line(state.log.line_number)
@ -562,7 +596,7 @@ impl<'a> UiState<'a> {
copy_osc(raw);
(false, UiState::Log(state))
}
(UiState::ByIdentifier(state), UiEvent::ByRequest) => {
(UiState::Distinct(state), UiEvent::ByRequest) => {
let selected = state.selected();
(true, state.by_request(selected, app))
}
@ -622,7 +656,7 @@ impl<'a> UiState<'a> {
previous: Some(previous),
..
})
| UiState::ByIdentifier(LogsByIdentifierState { previous, .. })
| UiState::Distinct(DistinctLogsState { previous, .. })
| UiState::Log(LogState { previous, .. })
| UiState::Errors(ErrorLinesState { previous, .. }),
UiEvent::Back,
@ -646,7 +680,7 @@ impl<'a> UiState<'a> {
pub enum UiPage {
GroupList,
Group,
Logs,
DistinctLogs,
Log,
Errors,
Error,