group by app and url

This commit is contained in:
Robin Appelman 2025-08-16 14:48:02 +02:00
commit a36df49377
9 changed files with 398 additions and 23 deletions

View file

@ -55,8 +55,8 @@ pub fn footer<'a>(app: &App, params: FooterParams<'a>) -> Table<'a> {
fn help(page: UiPage) -> &'static str {
match page {
UiPage::MatchList => "«Q» Exit - «Enter» Select - «F» Filter - «E» Show parse errors",
UiPage::Match => "«Q» Exit - «Enter» Select - «F» Filter - «Esc» Back",
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"
}
@ -70,7 +70,7 @@ fn help(page: UiPage) -> &'static str {
fn filter_help(page: UiPage) -> &'static str {
match page {
UiPage::MatchList => "«Esc» Clear",
UiPage::GroupList => "«Esc» Clear",
_ => "«Esc» Clear - «Left» Back",
}
}

View file

@ -22,6 +22,7 @@ pub enum UiEvent {
Text(char),
PopText(PopMode),
ByRequest,
GroupBy,
}
pub enum PopMode {
@ -40,10 +41,10 @@ pub fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEv
(Mode::Normal, KeyCode::Esc) => Some(UiEvent::Back),
(Mode::Normal, KeyCode::Char('q')) => Some(UiEvent::Quit),
(Mode::Normal, KeyCode::Char('e')) if page == UiPage::MatchList => {
(Mode::Normal, KeyCode::Char('e')) if page == UiPage::GroupList => {
Some(UiEvent::Errors)
}
(_, KeyCode::Left) if page != UiPage::MatchList => Some(UiEvent::Back),
(_, KeyCode::Left) if ui_state.has_previous() => Some(UiEvent::Back),
(_, KeyCode::Down) => Some(UiEvent::Down(1, true)),
(_, KeyCode::Up) => Some(UiEvent::Up(1, true)),
(_, KeyCode::PageDown) => Some(UiEvent::Down(10, false)),
@ -53,6 +54,7 @@ pub fn handle_events(page: UiPage, ui_state: &UiState) -> io::Result<Option<UiEv
(_, KeyCode::Enter | KeyCode::Right) => Some(UiEvent::Select),
(Mode::Normal, KeyCode::Char('c')) => Some(UiEvent::Copy),
(Mode::Normal, KeyCode::Char('r')) => Some(UiEvent::ByRequest),
(Mode::Normal, KeyCode::Char('g')) => Some(UiEvent::GroupBy),
(Mode::Normal, KeyCode::F(4) | KeyCode::Char('f')) => {
Some(UiEvent::EnterFilterMode)
}

101
src/ui/list.rs Normal file
View file

@ -0,0 +1,101 @@
use crate::ui::style::TABLE_SELECTED_STYLE;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::StatefulWidget;
use ratatui::widgets::{Block, HighlightSpacing, List, ListItem, ListState};
pub struct SelectList<'a> {
list: List<'a>,
}
impl<'a> SelectList<'a> {
pub fn new<T>(items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
SelectList {
list: List::new(items)
.highlight_style(TABLE_SELECTED_STYLE)
.highlight_spacing(HighlightSpacing::Always),
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(self, block: Block<'a>) -> Self {
SelectList {
list: self.list.block(block),
}
}
}
impl StatefulWidget for SelectList<'_> {
type State = SizedListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidget::render(self.list, area, buf, &mut state.list_state);
}
}
#[derive(PartialEq)]
pub struct SizedListState {
list_state: ListState,
count: usize,
}
impl SizedListState {
pub fn new(count: usize) -> Self {
SizedListState {
list_state: ListState::default().with_selected(Some(0)),
count,
}
}
pub fn selected(&self) -> usize {
self.list_state.selected().unwrap_or_default()
}
pub fn up(&mut self, step: usize, rollover: bool) -> usize {
let current = self.list_state.selected().unwrap_or(0);
let after = if step > current {
if rollover {
self.count - 1
} else {
0
}
} else {
current - step
};
self.list_state.select(Some(after));
after
}
pub fn down(&mut self, step: usize, rollover: bool) -> usize {
let current = self.list_state.selected().unwrap_or(0);
let after = if step >= self.count - current {
if rollover {
0
} else {
self.count - 1
}
} else {
current + step
};
self.list_state.select(Some(after));
after
}
pub fn scroll(&mut self, step: isize) {
let selected = self
.list_state
.selected()
.unwrap_or_default()
.saturating_add_signed(step)
.min(self.count - 1);
self.list_state.select(Some(selected));
}
pub fn select(&mut self, selected: usize) {
self.list_state.select(Some(selected));
}
}

View file

@ -1,25 +1,27 @@
use crate::app::App;
use crate::error::UiError;
use crate::grouping::LogGrouping;
use crate::grouping::{GroupingOptions, LogGrouping};
use crate::ui::by_identifier::logs_by_identifier;
use crate::ui::error_list::error_list;
use crate::ui::footer::footer;
use crate::ui::grouping_list::grouping_list;
use crate::ui::histogram::UiHistogram;
use crate::ui::input::handle_events;
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, GroupListState, GroupState, LogState, LogsByIdentifierState,
UiState,
ErrorLinesState, ErrorState, GroupByMenuState, GroupListState, GroupState, LogState,
LogsByIdentifierState, UiState,
};
use ratatui::crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use ratatui::crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::crossterm::ExecutableCommand;
use ratatui::layout::Flex;
use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use ratatui::widgets::{Block, Clear, Paragraph};
use ratatui::Terminal;
use serde_json::Value;
use std::io;
@ -32,6 +34,7 @@ mod footer;
mod grouping_list;
mod histogram;
mod input;
mod list;
mod single_group;
mod single_log;
mod state;
@ -158,6 +161,21 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
);
frame.render_widget(footer(app, state.footer_params()), layout[2]);
}
UiState::GroupByMenu(GroupByMenuState {
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
);
let popup = SelectList::new(GroupingOptions::all().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 {
lines,
table_state,
@ -198,3 +216,11 @@ fn ui(frame: &mut Frame, app: &App, state: &mut UiState) {
}
}
}
fn center(area: Rect, horizontal: Constraint, vertical: Constraint) -> Rect {
let [area] = Layout::horizontal([horizontal])
.flex(Flex::Center)
.areas(area);
let [area] = Layout::vertical([vertical]).flex(Flex::Center).areas(area);
area
}

View file

@ -1,10 +1,11 @@
use crate::app::{App, Filter, EMPTY_FILTER};
use crate::error::ParseError;
use crate::grouping::{GroupingUi, LogGrouping};
use crate::grouping::{GroupingOptions, GroupingUi, LogGrouping};
use crate::logfile::logline::{FullLogLine, LogLine};
use crate::matcher::MATCH_GROUPING_UI;
use crate::ui::footer::FooterParams;
use crate::ui::input::{PopMode, UiEvent};
use crate::ui::list::SizedListState;
use crate::ui::table::ScrollbarTableState;
use crate::ui::UI_HEADER_SIZE;
use crate::{copy_osc, parse_line_full};
@ -17,6 +18,7 @@ use std::sync::Arc;
pub enum UiState<'a> {
GroupList(GroupListState<'a>),
Group(GroupState<'a>),
GroupByMenu(GroupByMenuState<'a>),
ByIdentifier(LogsByIdentifierState<'a>),
Log(LogState<'a>),
Errors(ErrorLinesState<'a>),
@ -24,6 +26,22 @@ pub enum UiState<'a> {
Quit,
}
impl<'a> UiState<'a> {
pub fn has_previous(&self) -> bool {
matches!(
self,
UiState::Group(GroupState { .. })
| UiState::GroupList(GroupListState {
previous: Some(_),
..
})
| UiState::ByIdentifier(LogsByIdentifierState { .. })
| UiState::Log(LogState { .. })
| UiState::Errors(ErrorLinesState { .. })
)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Mode {
Normal,
@ -35,6 +53,7 @@ pub struct GroupListState<'a> {
pub ui: GroupingUi,
pub table_state: ScrollbarTableState,
pub filter: Filter,
pub previous: Option<Box<UiState<'a>>>,
mode: Mode,
}
@ -73,6 +92,42 @@ impl PartialEq for GroupListState<'_> {
}
}
pub struct GroupByMenuState<'a> {
pub previous: Box<UiState<'a>>,
pub list_state: SizedListState,
}
impl PartialEq for GroupByMenuState<'_> {
fn eq(&self, other: &Self) -> bool {
self.list_state == other.list_state
}
}
impl<'a> GroupByMenuState<'a> {
fn selected(&self) -> usize {
self.list_state.selected()
}
pub fn enter(self, selected: usize) -> UiState<'a> {
let group_option = GroupingOptions::all().nth(selected).unwrap();
let lines = match self.previous.as_ref() {
UiState::Group(group) => &group.result.lines,
_ => panic!("Group by called from non-group"),
};
let items = group_option.group_by(lines.lines.clone());
let count = items.len();
UiState::GroupList(GroupListState {
items,
filter: Filter::default(),
ui: group_option.ui(),
mode: Mode::Normal,
table_state: ScrollbarTableState::new(count),
previous: Some(self.previous),
})
}
}
pub struct GroupState<'a> {
pub result: LogGrouping<'a>,
pub table_state: ScrollbarTableState,
@ -86,6 +141,13 @@ impl<'a> GroupState<'a> {
self.table_state.selected()
}
fn group_by_menu(self) -> UiState<'a> {
UiState::GroupByMenu(GroupByMenuState {
previous: Box::new(self.into()),
list_state: SizedListState::new(GroupingOptions::all().count()),
})
}
fn enter(self, selected: usize) -> UiState<'a> {
let mut table_state = TableState::default();
table_state.select(Some(0));
@ -260,13 +322,15 @@ impl<'a> UiState<'a> {
table_state: ScrollbarTableState::new(lines),
filter: Filter::default(),
mode: Mode::Normal,
previous: None,
})
}
pub fn page(&self) -> UiPage {
match self {
UiState::Quit | UiState::GroupList(_) => UiPage::MatchList,
UiState::Group(_) => UiPage::Match,
UiState::Quit | UiState::GroupList(_) => UiPage::GroupList,
UiState::Group(_) => UiPage::Group,
UiState::GroupByMenu(_) => UiPage::Group, // todo
UiState::ByIdentifier(_) => UiPage::Logs,
UiState::Log(_) => UiPage::Log,
UiState::Errors(_) => UiPage::Errors,
@ -332,6 +396,13 @@ impl<'a> UiState<'a> {
}
}
fn list_state_mut(&mut self) -> Option<&mut SizedListState> {
match self {
UiState::GroupByMenu(state) => Some(&mut state.list_state),
_ => None,
}
}
pub fn scroll_offset(&self) -> usize {
if let Some(table_state) = self.table_state() {
table_state.offset()
@ -383,6 +454,7 @@ impl<'a> UiState<'a> {
UiState::GroupList(_) => UI_HEADER_SIZE + 1,
UiState::Group(_) => UI_HEADER_SIZE + 1,
UiState::ByIdentifier(_) => UI_HEADER_SIZE + 1,
UiState::GroupByMenu(_) => 0,
UiState::Log(_) => 0,
UiState::Errors(_) => 0,
UiState::Error(_) => 0,
@ -396,7 +468,9 @@ impl<'a> UiState<'a> {
(_, UiEvent::Quit) => (true, UiState::Quit),
(
UiState::GroupList(GroupListState {
mode: Mode::Normal, ..
previous: None,
mode: Mode::Normal,
..
}),
UiEvent::Back,
) => (true, UiState::Quit),
@ -404,24 +478,36 @@ impl<'a> UiState<'a> {
if let Some(table_state) = state.table_state_mut() {
table_state.down(step, rollover);
}
if let Some(list_state) = state.list_state_mut() {
list_state.down(step, rollover);
}
(true, state)
}
(mut state, UiEvent::Up(step, rollover)) => {
if let Some(table_state) = state.table_state_mut() {
table_state.up(step, rollover);
}
if let Some(list_state) = state.list_state_mut() {
list_state.up(step, rollover);
}
(true, state)
}
(mut state, UiEvent::Scroll(step)) => {
if let Some(table_state) = state.table_state_mut() {
table_state.scroll(step);
}
if let Some(list_state) = state.list_state_mut() {
list_state.scroll(step);
}
(true, state)
}
(mut state, UiEvent::SelectAt(selected)) => {
if let Some(table_state) = state.table_state_mut() {
table_state.select(selected);
}
if let Some(list_state) = state.list_state_mut() {
list_state.select(selected);
}
(true, state)
}
(UiState::GroupList(state), UiEvent::Select) => {
@ -444,6 +530,14 @@ impl<'a> UiState<'a> {
(true, state.enter(selected))
}
(UiState::Group(state), UiEvent::Enter(selected)) => (true, state.enter(selected)),
(UiState::Group(state), UiEvent::GroupBy) => (true, state.group_by_menu()),
(UiState::GroupByMenu(state), UiEvent::Enter(selected)) => {
(true, state.enter(selected))
}
(UiState::GroupByMenu(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected))
}
(UiState::ByIdentifier(state), UiEvent::Select) => {
let selected = state.selected();
(true, state.enter(selected, app))
@ -524,6 +618,10 @@ impl<'a> UiState<'a> {
(
UiState::Group(GroupState { previous, .. })
| UiState::GroupList(GroupListState {
previous: Some(previous),
..
})
| UiState::ByIdentifier(LogsByIdentifierState { previous, .. })
| UiState::Log(LogState { previous, .. })
| UiState::Errors(ErrorLinesState { previous, .. }),
@ -546,8 +644,8 @@ impl<'a> UiState<'a> {
#[derive(PartialEq)]
pub enum UiPage {
MatchList,
Match,
GroupList,
Group,
Logs,
Log,
Errors,