Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL plugin #53

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 30 additions & 34 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::Arc;

use crossterm::event::KeyEvent;
use ratatui::widgets::ScrollbarState;
use serde::{Deserialize, Serialize};
use tokio::sync::mpsc;
use tokio::sync::mpsc::UnboundedSender;
use tokio::sync::Mutex;

use crate::components::component::Component;
use crate::components::handlers::HandlerMetadata;
use crate::components::home::{Home, WebSockerInternalState};
use crate::components::home::Home;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really minor and you definitely don't have to do it, but a style guideline I follow is to prevent what they call "module-name prefixes", which essentially means repeating the module name within your type names, like components::home::Home or components::component::Component or components::handlers::HandlerMetadata. The Go community recommends against this as well. It's generally more concise and readable to use alternatives like components::Home, components::Component, or components::handlers::Metadata.

To do this, I'll often do public exports of specific types within my lib.rs or mod.rs files, like this. With things like pub use hooks::Hook; and pub use tags::Tag;, it allows me to use more concise imports like inject::Hook, and inject::Tag everywhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you can collapse a lot of these imports together into one statement, like this. The end result after adopting these two style guidelines would look something like this:

use crate::{
    components::{Component, Home, handlers::Metadata},
    services::websocket::{Client, Trace},
    tui::{Event, Tui},
    wss::client,
};

use crate::services::websocket::{Client, Trace};
use crate::tui::{Event, Tui};
use crate::wss::client;
Expand Down Expand Up @@ -78,6 +76,12 @@ pub struct UIState {
pub horizontal_scroll_state: ScrollbarState,
}

#[derive(Default, Clone, PartialEq, Eq, Debug, Hash)]
pub struct WssClient {
pub path: String,
pub address: String,
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum Action {
#[serde(skip)]
Expand Down Expand Up @@ -121,25 +125,27 @@ pub enum Action {
#[serde(skip)]
SetGeneralStatus(String),
#[serde(skip)]
SetWebsocketStatus(WebSockerInternalState),
#[serde(skip)]
MarkTraceAsTimedOut(String),
#[serde(skip)]
ClearStatusMessage,
#[serde(skip)]
AddTrace(Trace),
#[serde(skip)]
AddClient(WssClient),
#[serde(skip)]
RemoveClient(WssClient),
AddTraceError,
}

#[derive(Default)]
pub struct Services {
websocket_client: Arc<Mutex<Client>>,
websocket_client: Client,
}

#[derive(Default)]
pub struct App {
pub action_tx: Option<UnboundedSender<Action>>,
pub components: Vec<Arc<Mutex<dyn Component>>>,
pub components: Vec<Box<dyn Component>>,
pub services: Services,
pub is_first_render: bool,
pub logs: Vec<String>,
Expand All @@ -152,12 +158,12 @@ impl App {
pub fn new() -> Result<App, Box<dyn Error>> {
let config = crate::config::Config::new()?;

let home = Arc::new(Mutex::new(Home::new()?));
let home = Home::new()?;

let websocket_client = Arc::new(Mutex::new(Client::new()));
let websocket_client = Client::new();

let app = App {
components: vec![home],
components: vec![Box::new(home)],
services: Services { websocket_client },
key_map: config.mapping.0,
..Self::default()
Expand All @@ -182,39 +188,34 @@ impl App {

self.services
.websocket_client
.lock()
.await
.register_action_handler(action_tx.clone())?;

self.services.websocket_client.lock().await.start();
self.services.websocket_client.start();

let mut t = Tui::new();

t.enter()?;

for component in self.components.iter() {
component
.lock()
.await
.register_action_handler(action_tx.clone())?;
for component in self.components.iter_mut() {
component.register_action_handler(action_tx.clone())?;
}

self.services.websocket_client.lock().await.init();
self.services.websocket_client.init();

let action_to_clone = self.action_tx.as_ref().unwrap().clone();

tokio::spawn(async move {
client(Some(action_to_clone)).await;
let _ = client(Some(action_to_clone)).await;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can also do client(Some(action_to_clone)).await?; which would allow the error to surface from the result

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check that out!

});

loop {
let event = t.next().await;

if let Some(Event::Render) = event {
for component in self.components.iter() {
let c = component.lock().await;
t.terminal.draw(|frame| {
let r = c.render(frame);
let r = component.render(frame);

if let Err(e) = r {
action_tx
.send(Action::Error(format!("Failed to draw: {:?}", e)))
Expand All @@ -225,8 +226,8 @@ impl App {
};

if let Some(Event::OnMount) = event {
for component in self.components.iter() {
if let Some(action) = component.lock().await.on_mount()? {
for component in self.components.iter_mut() {
if let Some(action) = component.on_mount()? {
action_tx.send(action.clone())?;
}
}
Expand All @@ -245,8 +246,8 @@ impl App {
}
};

for component in self.components.iter() {
if let Some(action) = component.lock().await.handle_events(event)? {
for component in self.components.iter_mut() {
if let Some(action) = component.handle_events(event)? {
action_tx.send(action.clone())?;
}
}
Expand All @@ -256,18 +257,13 @@ impl App {
self.should_quit = true;
}

for component in self.components.iter() {
if let Some(action) = component.lock().await.update(action.clone())? {
for component in self.components.iter_mut() {
if let Some(action) = component.update(action.clone())? {
action_tx.send(action.clone())?;
}
}

self.services
.websocket_client
.clone()
.lock()
.await
.update(action.clone());
self.services.websocket_client.update(action.clone());
}

if self.should_quit {
Expand Down
4 changes: 4 additions & 0 deletions src/components/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,25 @@ pub trait Component {
) -> Result<(), Box<dyn Error>> {
Ok(())
}

fn handle_events(&mut self, event: Option<Event>) -> Result<Option<Action>, Box<dyn Error>> {
let r = match event {
Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
_ => None,
};
Ok(r)
}

#[allow(unused_variables)]
fn handle_key_events(&mut self, key: KeyEvent) -> Result<Option<Action>, Box<dyn Error>> {
Ok(None)
}

#[allow(unused_variables)]
fn update(&mut self, action: Action) -> Result<Option<Action>, Box<dyn Error>> {
Ok(None)
}

fn render(&self, f: &mut Frame<'_>) -> Result<(), Box<dyn Error>>;

fn on_mount(&mut self) -> Result<Option<Action>, Box<dyn Error>>;
Expand Down
24 changes: 22 additions & 2 deletions src/components/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1090,8 +1090,28 @@ pub fn handle_delete_item(app: &mut Home) {
let _ = &app.items.remove(current_trace);
}

pub fn handle_general_status(app: &mut Home, s: String) {
app.status_message = Some(s);
pub fn handle_general_status(
app: &mut Home,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable name is app but the actual struct is Home. That was confusing. 😅 I wanted to see what abort_handlers looked like, so I pulled up app.rs and couldn't find it.

message: String,
sender: Option<UnboundedSender<Action>>,
) {
app.status_message = Some(message);

app.abort_handlers.iter().for_each(|handler| {
handler.abort();
});
Comment on lines +1100 to +1102
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a minute to wrap my head around this, so I'll summarize here to make sure I understand. When you're showing a status message, you want to set a 5-second clear action. If the status message is changed during that time, you abort the prior clear and reset the timer to 5 seconds anew so that the new message isn't cleared out too soon?


app.abort_handlers.clear();

if let Some(sender) = sender {
let thread_handler = tokio::spawn(async move {
sleep(Duration::from_millis(5000)).await;

sender.send(Action::ClearStatusMessage)
});
Comment on lines +1107 to +1111
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is a tokio::spawn independent task because there's no natural point to await the results to trigger polling and allow the future to progress?


app.abort_handlers.push(thread_handler.abort_handle());
}
}

pub fn handle_select(app: &mut Home) {
Expand Down
55 changes: 34 additions & 21 deletions src/components/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,30 +12,21 @@ use tokio::sync::mpsc::UnboundedSender;
use tokio::task::AbortHandle;

use crate::{
app::{Action, ActiveBlock, Mode, RequestDetailsPane, ResponseDetailsPane, UIState},
app::{Action, ActiveBlock, Mode, RequestDetailsPane, ResponseDetailsPane, UIState, WssClient},
components::component::Component,
components::handlers,
plugins::graphql::{GraphQLPlugin, Plugin},
render,
services::websocket::{State, Trace},
tui::{Event, Frame},
utils::{Ordering, TraceSort},
utils::TraceSort,
};

#[derive(Default, PartialEq, Eq, Debug, Clone)]
pub enum WebSockerInternalState {
Connected(usize),
Open,
#[default]
Closed,
}

#[derive(Clone, PartialEq, Debug, Eq, Default)]
pub enum FilterSource {
#[default]
All,
Applied(HashSet<String>), // Source(String),
// Method(http::method::Method),
// Status(String),
Applied(HashSet<String>),
}

#[derive(Default)]
Expand Down Expand Up @@ -78,14 +69,15 @@ pub struct Home {
pub ws_status: String,
pub wss_connected: bool,
pub wss_connection_count: usize,
pub wss_state: WebSockerInternalState,
pub filter_index: usize,
pub sort_index: usize,
metadata: Option<handlers::HandlerMetadata>,
filter_source: FilterSource,
pub method_filters: HashMap<http::method::Method, MethodFilter>,
pub status_filters: HashMap<String, StatusFilter>,
pub order: TraceSort,
pub plugins: Vec<Box<dyn Plugin>>,
pub wss: HashSet<WssClient>,
}

impl Home {
Expand All @@ -101,6 +93,10 @@ impl Home {

let statuses = vec!["1xx", "2xx", "3xx", "4xx", "5xx"];

let gql_plugin = GraphQLPlugin::new();

home.plugins.push(Box::new(gql_plugin));

statuses.iter().for_each(|status| {
home.status_filters.insert(
status.clone().to_string(),
Expand Down Expand Up @@ -219,7 +215,9 @@ impl Component for Home {
Action::Help => handlers::handle_help(self),
Action::ToggleDebug => handlers::handle_debug(self),
Action::Select => handlers::handle_select(self),
Action::HandleFilter(l) => handlers::handle_general_status(self, l.to_string()),
Action::HandleFilter(l) => {
handlers::handle_general_status(self, l.to_string(), self.action_tx.clone())
}
Action::OpenFilter => {
let current_block = self.active_block;

Expand Down Expand Up @@ -249,8 +247,9 @@ impl Component for Home {
Action::FocusOnTraces => handlers::handle_esc(self),
Action::StopWebSocketServer => self.wss_connected = false,
Action::StartWebSocketServer => self.wss_connected = true,
Action::SetGeneralStatus(s) => handlers::handle_general_status(self, s),
Action::SetWebsocketStatus(s) => self.wss_state = s,
Action::SetGeneralStatus(message) => {
handlers::handle_general_status(self, message, self.action_tx.clone())
}
Action::NavigateUp(Some(key)) => handlers::handle_up(self, key, metadata),
Action::NavigateDown(Some(key)) => handlers::handle_down(self, key, metadata),
Action::NavigateLeft(Some(key)) => handlers::handle_left(self, key, metadata),
Expand All @@ -267,6 +266,16 @@ impl Component for Home {
handlers::handle_adjust_scroll_bar(self, metadata);
}
Action::MarkTraceAsTimedOut(id) => self.mark_trace_as_timed_out(id),
Action::AddClient(client) => {
self.wss.replace(client);

()
}
Action::RemoveClient(client) => {
self.wss.remove(&client);

()
}
_ => {}
}

Expand All @@ -287,7 +296,8 @@ impl Component for Home {
ActiveBlock::Filter(crate::app::FilterScreen::FilterMain) => {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.margin(3)
.vertical_margin(10)
.horizontal_margin(20)
.constraints([Constraint::Percentage(100)].as_ref())
.split(frame.size());

Expand All @@ -296,7 +306,8 @@ impl Component for Home {
ActiveBlock::Filter(crate::app::FilterScreen::FilterSource) => {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.margin(3)
.vertical_margin(10)
.horizontal_margin(20)
.constraints([Constraint::Percentage(100)].as_ref())
.split(frame.size());

Expand All @@ -305,7 +316,8 @@ impl Component for Home {
ActiveBlock::Filter(crate::app::FilterScreen::FilterStatus) => {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.margin(3)
.vertical_margin(10)
.horizontal_margin(20)
.constraints([Constraint::Percentage(100)].as_ref())
.split(frame.size());

Expand All @@ -314,7 +326,8 @@ impl Component for Home {
ActiveBlock::Filter(crate::app::FilterScreen::FilterMethod) => {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.margin(3)
.vertical_margin(10)
.horizontal_margin(20)
.constraints([Constraint::Percentage(100)].as_ref())
.split(frame.size());

Expand Down
Loading