uptime-kuma-dashboard/src/ui/components/monitor_list.rs

209 lines
6 KiB
Rust

use std::cmp::min;
use crate::i18n::t;
use crate::ui::dashboard::{
MonitorStatus, MonitorViewState,
model::{DashboardViewState, GroupViewState},
};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Padding, Row, Table},
};
const STATUS_LINE_LENGTH: usize = 100;
const MAX_NAME_LENGTH: usize = 30;
pub fn render_monitor_list(frame: &mut Frame, area: Rect, state: &DashboardViewState) {
let group_areas = layout_groups(area, &state.groups);
for (i, (group, &group_area)) in state.groups.iter().zip(group_areas.iter()).enumerate() {
render_group(frame, group_area, group, i == 0);
}
}
fn layout_groups(area: Rect, groups: &[GroupViewState]) -> Vec<Rect> {
let total_lines: usize = groups.iter().map(|g| g.monitors.len() + 1).sum();
if total_lines == 0 {
return Vec::new();
}
let content_height = area.height.saturating_sub(2);
let line_height = content_height as usize / total_lines;
let mut current_y = area.y + 1;
let mut areas = Vec::with_capacity(groups.len());
for group in groups {
let group_lines = group.monitors.len() + 2;
let group_height = (group_lines + line_height).max(1);
areas.push(Rect {
x: area.x,
y: current_y,
width: area.width,
height: group_height as u16,
});
current_y += group_height as u16;
}
areas
}
fn render_group(frame: &mut Frame, area: Rect, group: &GroupViewState, is_first: bool) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(0)
.constraints([Constraint::Length(1), Constraint::Min(1)])
.split(area);
let group_title = Line::from(vec![
Span::styled(format!("{} ", group.name), title_style()),
Span::styled(
format!("({})", group.monitors.len()),
Style::default().fg(Color::Gray),
),
]);
let title_block = Block::default()
.borders(if is_first {
Borders::TOP | Borders::LEFT | Borders::RIGHT
} else {
Borders::ALL
})
.border_style(Style::default().fg(Color::Blue))
.title(group_title)
.title_alignment(Alignment::Left);
frame.render_widget(title_block, chunks[0]);
if !group.monitors.is_empty() {
render_monitor_table(frame, chunks[1], &group.monitors);
}
}
fn render_monitor_table(frame: &mut Frame, area: Rect, monitors: &Vec<MonitorViewState>) {
let max_items = area.height as usize;
let items_to_show = min(monitors.len(), max_items);
let header_cells = vec![
"".to_string(),
t("monitor"),
t("Response"),
t("uptime"),
t("history"),
];
let header = Row::new(header_cells).style(title_style()).height(1);
let rows: Vec<Row> = monitors
.iter()
.take(items_to_show)
.map(|monitor| create_monitor_item(monitor))
.collect();
let widths = vec![
Constraint::Length(3),
Constraint::Length(MAX_NAME_LENGTH as u16),
Constraint::Length(10),
Constraint::Length(10),
Constraint::Length(STATUS_LINE_LENGTH as u16),
];
let table = Table::new(rows, widths)
.header(header)
.block(
Block::default()
.borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM)
.border_style(Style::default().fg(Color::Blue))
.padding(Padding::horizontal(2)),
)
.column_spacing(1)
.style(Style::default());
frame.render_widget(table, area);
}
pub fn get_status_char(status: &MonitorStatus) -> char {
match status {
MonitorStatus::Up => '■',
MonitorStatus::Down => '■',
MonitorStatus::Unknown => '■',
}
}
pub fn get_status_color(status: &MonitorStatus) -> Color {
match status {
MonitorStatus::Up => Color::Green,
MonitorStatus::Down => Color::Red,
MonitorStatus::Unknown => Color::Yellow,
}
}
pub fn get_status_emoji(status: &MonitorStatus) -> &str {
match status {
MonitorStatus::Up => "",
MonitorStatus::Down => "",
MonitorStatus::Unknown => "",
}
}
fn create_monitor_item(monitor: &MonitorViewState) -> Row<'_> {
let status_icon = get_status_emoji(&monitor.status);
let status_color = get_status_color(&monitor.status);
let display_name: String = if monitor.name.len() > MAX_NAME_LENGTH {
format!("{:.width$}...", &monitor.name, width = MAX_NAME_LENGTH - 3)
} else {
monitor.name.clone()
};
let response_text = format!("{:>7}ms", monitor.response_time);
let uptime_text = format!("{:>7}%", monitor.uptime_24h);
let status_line_spans = create_status_line_spans(&monitor.status_history);
Row::new(vec![
get_formated_line(format!("{} ", status_icon), status_color, Modifier::empty()),
get_formated_line(display_name, Color::White, Modifier::empty()),
get_formated_line(response_text, Color::Cyan, Modifier::empty()),
get_formated_line(uptime_text, Color::Magenta, Modifier::empty()),
status_line_spans,
])
.style(Style::default())
.height(1)
}
fn get_formated_line(text: String, color: Color, modifier: Modifier) -> Line<'static> {
Line::from(vec![Span::styled(
text,
Style::default().fg(color).add_modifier(modifier),
)])
}
fn create_status_line_spans(status_history: &[MonitorStatus]) -> Line<'_> {
let recent_status: Vec<_> = status_history
.iter()
.rev()
.take(STATUS_LINE_LENGTH)
.collect();
let mut spans = Vec::with_capacity(recent_status.len());
for status in recent_status.iter().rev() {
let c = get_status_char(status);
let color = get_status_color(status);
spans.push(Span::styled(c.to_string(), Style::default().fg(color)));
}
Line::from(spans)
}
fn title_style() -> Style {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
}