feat(fxt): add Pods cockpit with service-grouped table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
pub mod data;
|
||||
pub mod overview;
|
||||
pub mod pods;
|
||||
pub mod risk;
|
||||
pub mod services;
|
||||
pub mod trading;
|
||||
|
||||
150
bin/fxt/src/tui/cockpits/pods.rs
Normal file
150
bin/fxt/src/tui/cockpits/pods.rs
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user