feat(fxt): add Pods cockpit with service-grouped table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-03-04 13:04:53 +01:00
parent 3c8fe238dd
commit 2571c2bbb3
2 changed files with 151 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
pub mod data;
pub mod overview;
pub mod pods;
pub mod risk;
pub mod services;
pub mod trading;

View File

@@ -0,0 +1,150 @@
#![allow(clippy::indexing_slicing)] // ratatui Layout::split indices match constraints
//! Pods cockpit -- K8s pod table grouped by service.
use ratatui::Frame;
use ratatui::layout::{Constraint, Rect};
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Paragraph, Row, Table};
use crate::tui::cockpit::Cockpit;
use crate::tui::state::AppState;
use crate::tui::theme;
pub struct PodsCockpit;
impl Cockpit for PodsCockpit {
fn name(&self) -> &str {
"Pods"
}
fn render(&self, frame: &mut Frame, area: Rect, state: &AppState) {
if state.pods.is_empty() {
let msg = Paragraph::new(Line::from(Span::styled(
"Waiting for pod data...",
theme::muted_style(),
)))
.block(theme::block(" Pods "));
frame.render_widget(msg, area);
return;
}
render_pod_table(frame, area, state);
}
}
/// Shorten a K8s pod name to its trailing random suffix.
///
/// `api-gateway-7686d84bdd-zl4mn` becomes `zl4mn`.
/// Falls back to the full name if there is no `-` separator.
fn shorten_pod_name(name: &str) -> &str {
name.rsplit('-').next().unwrap_or(name)
}
fn render_pod_table(frame: &mut Frame, area: Rect, state: &AppState) {
let header = Row::new(vec![
"Service", "Ready", "Status", "Version", "Restarts", "Age", "Node",
])
.style(theme::header_style());
let mut rows: Vec<Row<'_>> = Vec::new();
for group in &state.pods {
let ready_str = format!("{}/{}", group.ready_count, group.total_count);
let (status_text, status_style) = if group.ready_count == group.total_count
&& group.total_count > 0
{
("Running", Style::default().fg(theme::SUCCESS))
} else if group.ready_count > 0 {
("Degraded", Style::default().fg(theme::WARNING))
} else {
("Down", Style::default().fg(theme::ERROR))
};
let total_restarts: i64 = group.pods.iter().map(|p| i64::from(p.restarts)).sum();
let restart_style = if total_restarts > 0 {
Style::default().fg(theme::WARNING)
} else {
theme::muted_style()
};
// First pod provides age + node for the group header row.
let (age, node) = group
.pods
.first()
.map(|p| (p.age.as_str(), p.node.as_str()))
.unwrap_or(("-", "-"));
let version = group
.pods
.first()
.map(|p| p.version.as_str())
.unwrap_or("-");
// Group header row -- bold service name.
rows.push(Row::new(vec![
Span::styled(
group.service.clone(),
Style::default()
.fg(theme::TEXT)
.add_modifier(Modifier::BOLD),
),
Span::styled(ready_str, Style::default().fg(theme::TEXT)),
Span::styled(status_text.to_owned(), status_style),
Span::styled(version.to_owned(), theme::muted_style()),
Span::styled(total_restarts.to_string(), restart_style),
Span::styled(age.to_owned(), theme::muted_style()),
Span::styled(node.to_owned(), theme::muted_style()),
]));
// Sub-rows for groups with more than one pod.
if group.pods.len() > 1 {
for pod in &group.pods {
let pod_status_style = if pod.ready {
Style::default().fg(theme::SUCCESS)
} else {
Style::default().fg(theme::ERROR)
};
let pod_restart_style = if pod.restarts > 0 {
Style::default().fg(theme::WARNING)
} else {
theme::muted_style()
};
let suffix = shorten_pod_name(&pod.name);
rows.push(Row::new(vec![
Span::styled(format!(" {suffix}"), theme::muted_style()),
Span::styled(
if pod.ready { "1/1" } else { "0/1" }.to_owned(),
Style::default().fg(theme::TEXT),
),
Span::styled(pod.status.clone(), pod_status_style),
Span::styled(pod.version.clone(), theme::muted_style()),
Span::styled(pod.restarts.to_string(), pod_restart_style),
Span::styled(pod.age.clone(), theme::muted_style()),
Span::styled(pod.node.clone(), theme::muted_style()),
]));
}
}
}
let table = Table::new(
rows,
[
Constraint::Length(22),
Constraint::Length(7),
Constraint::Length(10),
Constraint::Length(12),
Constraint::Length(10),
Constraint::Length(8),
Constraint::Min(12),
],
)
.header(header)
.block(theme::block(" Pods "));
frame.render_widget(table, area);
}