Skip to content

Commit

Permalink
Merge pull request #23 from zaghaghi/hz/history-popup
Browse files Browse the repository at this point in the history
feat: ✨ added history popup to show list of previous requests
  • Loading branch information
zaghaghi committed Apr 14, 2024
2 parents 0abdd5d + dd1865c commit 5298dae
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 19 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,18 @@ Then, add `openapi-tui` to your `configuration.nix`
| `[` | Move to previous tab |
| `f` | Toggle fullscreen pane|
| `g` | Go in nested items in lists|
| `q` | Quit|
| `/` | Filter apis|
| `:` | Run commands|
| `Backspace`, `b` | Get out of nested items in lists|

# Commands
| Command | Description |
|:--------|:------------|
| `q` | Quit |
| `request` | Go to request page|
| `history` | Request history|

# Implemented Features
- [X] Viewer
- [X] OpenAPI v3.1
Expand All @@ -170,14 +179,15 @@ Then, add `openapi-tui` to your `configuration.nix`
- [X] Plain Response Viewer (Status + Headers + Body)

# Next Release
- [ ] History viewer
- [ ] Refactor footer, add flash footer messages
- [X] History viewer
- [X] Refactor footer, add flash footer messages
- [ ] Import request body file
- [ ] Save response body and header

# Backlog
- [ ] Schema Types (openapi-31)
- [ ] Display Key Mappings in Popup
- [ ] Cache Schema Styles
- [ ] Read Spec from STDIN
- [ ] Command Line
- [ ] Support array query strings
- [ ] Suppert extra headers
4 changes: 3 additions & 1 deletion src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ pub enum Action {
FocusFooter(Command, Args),
FooterResult(Command, Args),
Noop,
NewCall,
NewCall(Option<String>),
HangUp(Option<String>),
Dial,
History,
CloseHistory,
}
75 changes: 63 additions & 12 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::HashMap;
use color_eyre::eyre::Result;
use crossterm::event::KeyEvent;
use ratatui::{
layout::{Constraint, Direction, Layout},
layout::{Constraint, Layout},
prelude::Rect,
};
use serde::{Deserialize, Serialize};
Expand All @@ -13,7 +13,7 @@ use crate::{
action::Action,
config::Config,
pages::{home::Home, phone::Phone, Page},
panes::{footer::FooterPane, header::HeaderPane, Pane},
panes::{footer::FooterPane, header::HeaderPane, history::HistoryPane, Pane},
request::Request,
response::Response,
state::{InputMode, OperationItemType, State},
Expand All @@ -33,6 +33,7 @@ pub struct App {
pub active_page: usize,
pub footer: FooterPane,
pub header: HeaderPane,
pub popup: Option<Box<dyn Pane>>,
pub should_quit: bool,
pub should_suspend: bool,
pub mode: Mode,
Expand All @@ -53,6 +54,7 @@ impl App {
active_page: 0,
footer: FooterPane::new(),
header: HeaderPane::new(),
popup: None,
should_quit: false,
should_suspend: false,
config,
Expand Down Expand Up @@ -88,9 +90,9 @@ impl App {
loop {
if let Some(e) = tui.next().await {
let mut stop_event_propagation = self
.pages
.get_mut(self.active_page)
.and_then(|page| page.handle_events(e.clone(), &mut self.state).ok())
.popup
.as_mut()
.and_then(|pane| pane.handle_events(e.clone(), &mut self.state).ok())
.map(|response| {
match response {
Some(tui::EventResponse::Continue(action)) => {
Expand All @@ -105,6 +107,25 @@ impl App {
}
})
.unwrap_or(false);
stop_event_propagation = stop_event_propagation
|| self
.pages
.get_mut(self.active_page)
.and_then(|page| page.handle_events(e.clone(), &mut self.state).ok())
.map(|response| {
match response {
Some(tui::EventResponse::Continue(action)) => {
action_tx.send(action).ok();
false
},
Some(tui::EventResponse::Stop(action)) => {
action_tx.send(action).ok();
true
},
_ => false,
}
})
.unwrap_or(false);

stop_event_propagation = stop_event_propagation
|| self
Expand Down Expand Up @@ -178,8 +199,8 @@ impl App {
})
})?;
},
Action::NewCall => {
if let Some(operation_item) = self.state.active_operation() {
Action::NewCall(ref operation_id) => {
if let Some(operation_item) = self.state.get_operation(operation_id.clone()) {
if let OperationItemType::Path = operation_item.r#type {
if let Some(page) = operation_item
.operation
Expand All @@ -199,6 +220,7 @@ impl App {
}
}
}
action_tx.send(Action::CloseHistory).unwrap();
},
Action::HangUp(ref operation_id) => {
if self.pages.len() > 1 {
Expand All @@ -210,13 +232,37 @@ impl App {
}
}
},
Action::History => {
let operation_ids = self
.state
.openapi_operations
.iter()
.filter(|operation_item| {
let op_id = operation_item.operation.operation_id.clone();
self.history.keys().any(|operation_id| op_id.eq(&Some(operation_id.clone())))
})
.collect::<Vec<_>>();
let history_popup = HistoryPane::new(operation_ids);
self.popup = Some(Box::new(history_popup));
},
Action::CloseHistory => {
if self.popup.is_some() {
self.popup = None;
}
},
_ => {},
}
if let Some(page) = self.pages.get_mut(self.active_page) {

if let Some(popup) = &mut self.popup {
if let Some(action) = popup.update(action.clone(), &mut self.state)? {
action_tx.send(action)?
};
} else if let Some(page) = self.pages.get_mut(self.active_page) {
if let Some(action) = page.update(action.clone(), &mut self.state)? {
action_tx.send(action)?
};
}

if let Some(action) = self.header.update(action.clone(), &mut self.state)? {
action_tx.send(action)?
};
Expand Down Expand Up @@ -252,17 +298,22 @@ impl App {
}

fn draw(&mut self, frame: &mut tui::Frame<'_>) -> Result<()> {
let vertical_layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Max(1), Constraint::Fill(1), Constraint::Max(1)])
.split(frame.size());
let vertical_layout =
Layout::vertical(vec![Constraint::Max(1), Constraint::Fill(1), Constraint::Max(1)]).split(frame.size());

self.header.draw(frame, vertical_layout[0], &self.state)?;

if let Some(page) = self.pages.get_mut(self.active_page) {
page.draw(frame, vertical_layout[1], &self.state)?;
};

if let Some(popup) = &mut self.popup {
let popup_vertical_layout =
Layout::vertical(vec![Constraint::Fill(1), popup.height_constraint(), Constraint::Fill(1)]).split(frame.size());
let popup_layout = Layout::horizontal(vec![Constraint::Fill(1), Constraint::Fill(1), Constraint::Fill(1)])
.split(popup_vertical_layout[1]);
popup.draw(frame, popup_layout[1], &self.state)?;
}
self.footer.draw(frame, vertical_layout[2], &self.state)?;
Ok(())
}
Expand Down
13 changes: 10 additions & 3 deletions src/pages/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,11 @@ impl Page for Home {
}
if args.eq("q") {
actions.push(Some(Action::Quit));
} else if args.eq("request") {
actions.push(Some(Action::NewCall));
} else if args.eq("request") || args.eq("r") {
actions
.push(Some(Action::NewCall(state.active_operation().and_then(|op| op.operation.operation_id.clone()))));
} else if args.eq("history") {
actions.push(Some(Action::History));
} else {
actions.push(Some(Action::TimedStatusLine("unknown command".into(), 1)));
}
Expand Down Expand Up @@ -158,7 +161,11 @@ impl Page for Home {
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => EventResponse::Stop(Action::Up),
KeyCode::Char('g') | KeyCode::Char('G') => EventResponse::Stop(Action::Go),
KeyCode::Backspace | KeyCode::Char('b') | KeyCode::Char('B') => EventResponse::Stop(Action::Back),
KeyCode::Enter => EventResponse::Stop(Action::NewCall),
KeyCode::Enter => {
EventResponse::Stop(Action::NewCall(
state.active_operation().and_then(|op| op.operation.operation_id.clone()),
))
},
KeyCode::Char('f') | KeyCode::Char('F') => EventResponse::Stop(Action::ToggleFullScreen),
KeyCode::Char(c) if ('1'..='9').contains(&c) => {
EventResponse::Stop(Action::Tab(c.to_digit(10).unwrap_or(0) - 1))
Expand Down
139 changes: 139 additions & 0 deletions src/panes/history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
use std::ops::Not;

use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use ratatui::{
prelude::*,
widgets::{block::*, *},
};

use crate::{
action::Action,
panes::Pane,
state::{InputMode, OperationItem, State},
tui::{EventResponse, Frame},
};

#[derive(Default)]
struct OperationHistoryItem {
operation_id: String,
method: String,
path: String,
}

#[derive(Default)]
pub struct HistoryPane {
history: Vec<OperationHistoryItem>,
history_item_index: Option<usize>,
}

impl HistoryPane {
pub fn new(operation_ids: Vec<&OperationItem>) -> Self {
let history = operation_ids
.iter()
.filter_map(|opertation_item| {
opertation_item.operation.operation_id.as_ref().map(|operation_id| {
OperationHistoryItem {
operation_id: operation_id.clone(),
method: opertation_item.method.clone(),
path: opertation_item.path.clone(),
}
})
})
.collect::<Vec<OperationHistoryItem>>();
let history_item_index = history.is_empty().not().then_some(0);
Self { history, history_item_index }
}

fn method_color(method: &str) -> Color {
match method {
"GET" => Color::LightCyan,
"POST" => Color::LightBlue,
"PUT" => Color::LightYellow,
"DELETE" => Color::LightRed,
_ => Color::Gray,
}
}
}

impl Pane for HistoryPane {
fn height_constraint(&self) -> Constraint {
Constraint::Fill(3)
}

fn handle_key_events(
&mut self,
key: crossterm::event::KeyEvent,
state: &mut State,
) -> Result<Option<crate::tui::EventResponse<crate::action::Action>>> {
match state.input_mode {
InputMode::Normal => {
let response = match key.code {
KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => EventResponse::Stop(Action::Down),
KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => EventResponse::Stop(Action::Up),
KeyCode::Esc => EventResponse::Stop(Action::CloseHistory),
KeyCode::Enter => {
if let Some(item_index) = self.history_item_index {
EventResponse::Stop(Action::NewCall(self.history.get(item_index).map(|item| item.operation_id.clone())))
} else {
return Ok(Some(EventResponse::Stop(Action::Noop)));
}
},
_ => {
return Ok(Some(EventResponse::Stop(Action::Noop)));
},
};
Ok(Some(response))
},
InputMode::Insert => Ok(Some(EventResponse::Stop(Action::Noop))),
InputMode::Command => Ok(Some(EventResponse::Stop(Action::Noop))),
}
}

fn update(&mut self, action: Action, _state: &mut State) -> Result<Option<Action>> {
match action {
Action::Down => {
let history_len = self.history.len();
if history_len > 0 {
self.history_item_index = self.history_item_index.map(|item_idx| item_idx.saturating_add(1) % history_len);
} else {
self.history_item_index = None;
}
return Ok(Some(Action::Update));
},
Action::Up => {
let history_len = self.history.len();
if history_len > 0 {
self.history_item_index = self
.history_item_index
.map(|item_idx| item_idx.saturating_add(history_len.saturating_sub(1)) % history_len);
} else {
self.history_item_index = None;
}
return Ok(Some(Action::Update));
},
_ => {},
}
Ok(None)
}

fn draw(&mut self, frame: &mut Frame<'_>, area: Rect, _state: &State) -> Result<()> {
frame.render_widget(Clear, area);
let items = self.history.iter().map(|item| {
Line::from(vec![
Span::styled(format!(" {:7}", item.method), Self::method_color(item.method.as_str())),
Span::from(item.path.clone()),
])
});
let list = List::new(items)
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(symbols::scrollbar::HORIZONTAL.end)
.highlight_spacing(HighlightSpacing::Always)
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
let mut list_state = ListState::default().with_selected(self.history_item_index);

frame.render_stateful_widget(list, area, &mut list_state);
frame.render_widget(Block::default().borders(Borders::ALL).title("Request History").style(Style::default()), area);
Ok(())
}
}
1 change: 1 addition & 0 deletions src/panes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod apis;
pub mod body_editor;
pub mod footer;
pub mod header;
pub mod history;
pub mod parameter_editor;
pub mod request;
pub mod response;
Expand Down
4 changes: 4 additions & 0 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ impl State {
}
}

pub fn get_operation(&self, operation_id: Option<String>) -> Option<&OperationItem> {
self.openapi_operations.iter().find(|operation_item| operation_item.operation.operation_id.eq(&operation_id))
}

pub fn active_operation(&self) -> Option<&OperationItem> {
if let Some(active_tag) = &self.active_tag_name {
self
Expand Down

0 comments on commit 5298dae

Please sign in to comment.