Skip to content

Commit eed3004

Browse files
committed
chore(shell): migrate to using Error
1 parent 29e7f56 commit eed3004

File tree

8 files changed

+144
-181
lines changed

8 files changed

+144
-181
lines changed

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,3 @@ the design decisions section for an explanation of what's happening there.
270270
- Is there a way to do FPS on a per-session basis with prometheus? Naively the
271271
way to do it would be to have a per-session label value, but that would be
272272
crazy for cardinality.
273-
- Update shell to use the same error widget + handling that tabs are.

src/resources/pod.rs

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use std::{borrow::Borrow, cmp::Ordering, error::Error, fmt::Display, sync::Arc};
22

33
use chrono::{TimeDelta, Utc};
4-
use itertools::Itertools;
54
use k8s_openapi::{
65
api::core::v1::{
76
ContainerState, ContainerStateTerminated, ContainerStateWaiting, ContainerStatus, Pod,
@@ -28,28 +27,27 @@ use crate::widget::{
2827
// TODO: There's probably a better debug implementation than this.
2928
#[derive(Clone, Debug)]
3029
pub struct StatusError {
31-
inner: v1::Status,
30+
pub message: String,
3231
}
3332

33+
// Because this is a golang error that's being returned, there's really no good
34+
// way to convert this into something that is moderately usable. The rest of the
35+
// `Status` struct is empty of anything useful. The decision is to be naive here
36+
// and let other display handlers figure out if they would like to deal with the
37+
// message.
3438
impl StatusError {
3539
pub fn new(inner: v1::Status) -> Self {
36-
Self { inner }
40+
Self {
41+
message: inner.message.unwrap_or("unknown status".to_string()),
42+
}
3743
}
3844
}
3945

4046
impl Error for StatusError {}
4147

4248
impl Display for StatusError {
4349
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44-
let lines = self
45-
.inner
46-
.message
47-
.as_ref()
48-
.map_or(format!("{:#?}", self.inner), |msg| {
49-
msg.split(':').map(str::trim).join("\n")
50-
});
51-
52-
write!(f, "{lines}")
50+
write!(f, "{}", self.message)
5351
}
5452
}
5553

src/widget.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod apex;
22
pub mod debug;
3+
pub mod error;
34
pub mod input;
45
pub mod loading;
56
pub mod log;

src/widget/error.rs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use eyre::{Report, Result};
2+
use ratatui::{
3+
layout::Rect,
4+
prelude::*,
5+
style::Style,
6+
widgets::{Block, Borders, Paragraph},
7+
Frame,
8+
};
9+
10+
use super::Widget;
11+
use crate::events::{Broadcast, Event, Keypress};
12+
13+
pub struct Error {
14+
inner: Report,
15+
16+
position: (u16, u16),
17+
}
18+
19+
impl Error {
20+
pub fn new(inner: Report) -> Self {
21+
Self {
22+
inner,
23+
position: (0, 0),
24+
}
25+
}
26+
}
27+
28+
impl Widget for Error {
29+
fn dispatch(&mut self, event: &Event) -> Result<Broadcast> {
30+
match event.key() {
31+
Some(Keypress::CursorLeft) => {
32+
self.position.1 = self.position.1.saturating_sub(1);
33+
}
34+
Some(Keypress::CursorRight) => {
35+
self.position.1 = self.position.1.saturating_add(1);
36+
}
37+
Some(Keypress::CursorUp) => {
38+
self.position.0 = self.position.0.saturating_sub(1);
39+
}
40+
Some(Keypress::CursorDown) => {
41+
self.position.0 = self.position.0.saturating_add(1);
42+
}
43+
_ => return Ok(Broadcast::Exited),
44+
}
45+
46+
Ok(Broadcast::Consumed)
47+
}
48+
49+
#[allow(clippy::cast_possible_truncation)]
50+
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
51+
let block = Block::default()
52+
.borders(Borders::ALL)
53+
.border_style(Style::default().fg(Color::Red));
54+
55+
let pg = Paragraph::new(format!("Error:{:?}", self.inner))
56+
.block(block)
57+
.scroll(self.position);
58+
59+
let width = pg.line_width() as u16 + 2;
60+
61+
let [_, area, _] = Layout::horizontal([
62+
Constraint::Fill(1),
63+
Constraint::Max(width),
64+
Constraint::Fill(1),
65+
])
66+
.areas(area);
67+
68+
let height = pg.line_count(area.width) as u16 + 2;
69+
70+
let [_, vert, _] = Layout::vertical([
71+
Constraint::Max(10),
72+
Constraint::Max(height),
73+
Constraint::Max(10),
74+
])
75+
.areas(area);
76+
77+
frame.render_widget(pg, vert);
78+
79+
Ok(())
80+
}
81+
}

src/widget/log.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use std::sync::Arc;
22

33
use color_eyre::{Section, SectionExt};
4-
use eyre::{eyre, Report, Result, WrapErr};
4+
use eyre::{eyre, Report, Result};
55
use futures::{future::BoxFuture, AsyncBufReadExt, FutureExt, TryStreamExt};
6-
use itertools::Itertools;
76
use k8s_openapi::api::core::v1::Pod;
87
use kube::{api::LogParams, Api, ResourceExt};
98
use ratatui::{layout::Rect, text::Line, widgets::Paragraph, Frame};
@@ -133,7 +132,7 @@ impl Widget for Log {
133132
let task = &mut self.task;
134133

135134
match futures::executor::block_on(async move { task.await? }) {
136-
Ok(_) => return Err(eyre!("Log task finished unexpectedly")),
135+
Ok(()) => return Err(eyre!("Log task finished unexpectedly")),
137136
Err(err) => {
138137
let Some(kube::Error::Api(resp)) = err.downcast_ref::<kube::Error>() else {
139138
return Err(err);

src/widget/pod/shell.rs

+33-89
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
use std::{pin::Pin, sync::Arc, vec};
22

33
use chrono::{DateTime, Utc};
4+
use color_eyre::{Section, SectionExt};
45
use derive_builder::Builder;
56
use eyre::{eyre, Result};
67
use futures::StreamExt;
8+
use itertools::Itertools;
79
use k8s_openapi::api::core::v1::Pod;
810
use kube::{
911
api::{Api, AttachParams},
1012
ResourceExt,
1113
};
1214
use lazy_static::lazy_static;
1315
use prometheus::{histogram_opts, register_histogram, Histogram};
14-
use ratatui::{
15-
layout::Rect,
16-
prelude::*,
17-
style::{palette::tailwind, Modifier, Style},
18-
widgets,
19-
widgets::{Block, Borders, Row},
20-
};
16+
use ratatui::{layout::Rect, prelude::*};
2117
use tokio::{
2218
io::{AsyncWrite, AsyncWriteExt},
2319
sync::mpsc::UnboundedReceiver,
@@ -81,14 +77,17 @@ impl Widget for Shell {
8177
}
8278

8379
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
84-
self.table.draw(frame, area, &self.pod)
80+
if let Err(err) = self.table.draw(frame, area, &self.pod) {
81+
tracing::info!("failed to draw table: {}", err);
82+
}
83+
84+
Ok(())
8585
}
8686
}
8787

8888
enum CommandState {
8989
Input(Text),
9090
Attached,
91-
Error(String),
9291
}
9392

9493
static COMMAND: &str = "/bin/bash";
@@ -163,23 +162,6 @@ impl Command {
163162
}
164163
}
165164

166-
#[allow(clippy::unnecessary_wraps)]
167-
fn dispatch_error(&mut self, event: &Event) -> Result<Broadcast> {
168-
if !matches!(self.state, CommandState::Error(_)) {
169-
return Ok(Broadcast::Ignored);
170-
}
171-
172-
match event.key() {
173-
// TODO: should handle scrolling inside the error message.
174-
Some(_) => {
175-
self.state = CommandState::Input(Command::input(&self.container));
176-
177-
Ok(Broadcast::Consumed)
178-
}
179-
_ => Ok(Broadcast::Ignored),
180-
}
181-
}
182-
183165
fn draw_input(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
184166
let CommandState::Input(ref mut txt) = self.state else {
185167
return Ok(());
@@ -201,77 +183,40 @@ impl Command {
201183

202184
txt.draw(frame, area)
203185
}
204-
205-
// TODO: this should be a separate widget of its own.
206-
#[allow(clippy::cast_possible_truncation, clippy::unnecessary_wraps)]
207-
fn draw_error(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
208-
let CommandState::Error(ref err) = self.state else {
209-
return Ok(());
210-
};
211-
212-
let block = Block::default()
213-
.title("Error")
214-
.borders(Borders::ALL)
215-
.border_style(Style::default().fg(Color::Red))
216-
.title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD));
217-
218-
// TODO: get this into a variable so that it can be styled.
219-
let rows: Vec<Row> = err
220-
.split('\n')
221-
.enumerate()
222-
.map(|(i, line)| {
223-
Row::new(vec![
224-
Span::from(format!("{i}: "))
225-
.style(style::Style::default().fg(tailwind::RED.c300)),
226-
Span::from(line),
227-
])
228-
})
229-
.collect();
230-
231-
let height = rows.len() as u16 + 2;
232-
233-
let content =
234-
widgets::Table::new(rows, vec![Constraint::Max(3), Constraint::Fill(0)]).block(block);
235-
236-
let [_, area, _] = Layout::horizontal([
237-
Constraint::Max(10),
238-
Constraint::Fill(0),
239-
Constraint::Max(10),
240-
])
241-
.areas(area);
242-
243-
let [_, mut vert, _] = Layout::vertical([
244-
Constraint::Max(10),
245-
Constraint::Fill(0),
246-
Constraint::Max(10),
247-
])
248-
.areas(area);
249-
250-
if vert.height > height {
251-
vert.height = height;
252-
}
253-
254-
frame.render_widget(content, vert);
255-
256-
Ok(())
257-
}
258186
}
259187

260188
impl Widget for Command {
261189
fn dispatch(&mut self, event: &Event) -> Result<Broadcast> {
262190
propagate!(self.dispatch_input(event));
263-
propagate!(self.dispatch_error(event));
264191

265192
match event {
266-
Event::Finished(result) => {
267-
let Err(err) = result else {
268-
return Ok(Broadcast::Exited);
193+
Event::Finished(result) => result.as_ref().map(|()| Broadcast::Exited).map_err(|err| {
194+
let Some(err) = err.downcast_ref::<StatusError>() else {
195+
return eyre!(err.to_string());
269196
};
270197

271-
self.state = CommandState::Error(err.to_string());
272-
273-
Ok(Broadcast::Consumed)
274-
}
198+
let separated = err.message.splitn(8, ':');
199+
200+
eyre!(
201+
"{}",
202+
separated.clone().last().unwrap_or("unknown error").trim()
203+
)
204+
.section(
205+
separated
206+
.with_position()
207+
.map(|(i, line)| {
208+
let l = line.trim().to_string();
209+
210+
match i {
211+
itertools::Position::Middle => format!("├─ {l}"),
212+
itertools::Position::Last => format!("└─ {l}"),
213+
_ => l,
214+
}
215+
})
216+
.join("\n")
217+
.header("Raw:"),
218+
)
219+
}),
275220
_ => Ok(Broadcast::Ignored),
276221
}
277222
}
@@ -280,7 +225,6 @@ impl Widget for Command {
280225
match self.state {
281226
CommandState::Input(_) => self.draw_input(frame, area)?,
282227
CommandState::Attached => {}
283-
CommandState::Error(_) => self.draw_error(frame, area)?,
284228
}
285229

286230
Ok(())

src/widget/table.rs

+13-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use ratatui::{
1212
};
1313

1414
use super::{input::Text, propagate, TableRow, Widget};
15-
use crate::events::{Broadcast, Event, Keypress};
15+
use crate::{
16+
events::{Broadcast, Event, Keypress},
17+
widget::error::Error,
18+
};
1619

1720
lazy_static! {
1821
static ref TABLE_FILTER: IntCounter = register_int_counter!(
@@ -260,12 +263,20 @@ impl Table {
260263
filter.dispatch(event)
261264
}
262265

266+
#[allow(clippy::unnecessary_wraps)]
263267
fn dispatch_detail(&mut self, event: &Event) -> Result<Broadcast> {
264268
let State::Detail(ref mut widget) = self.state.borrow_mut() else {
265269
return Ok(Broadcast::Ignored);
266270
};
267271

268-
widget.dispatch(event)
272+
match widget.dispatch(event) {
273+
Ok(result) => Ok(result),
274+
Err(err) => {
275+
self.state.detail(Box::new(Error::new(err)));
276+
277+
Ok(Broadcast::Consumed)
278+
}
279+
}
269280
}
270281

271282
fn handle_route(&mut self, route: &[String]) -> Result<()> {

0 commit comments

Comments
 (0)