use ratatui::{
Frame,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, BorderType, Borders, List, ListItem, ListState}
};
use rust_i18n::t;
use crate::tui::{
app::{App, ResourceTab},
themes::Palette
};
#[must_use]
pub fn server_status_view(status: &str, palette: &Palette) -> (&'static str, Color, String) {
match status {
"On" => (
"\u{25B6}",
palette.success,
t!("resource_list.status_running").to_string()
),
"Off" => (
"\u{25CB}",
palette.error,
t!("resource_list.status_stopped").to_string()
),
"Removed" | "Blocked" => ("\u{25CB}", palette.error, status.to_lowercase()),
"TurningOn" => (
"\u{25D0}",
palette.warning,
t!("resource_list.status_starting").to_string()
),
"TurningOff" | "HardTurningOff" => (
"\u{25D0}",
palette.warning,
t!("resource_list.status_stopping").to_string()
),
"Rebooting" | "HardRebooting" => (
"\u{25D0}",
palette.warning,
t!("resource_list.status_rebooting").to_string()
),
"Installing" | "SoftwareInstall" | "Reinstalling" => (
"\u{25D0}",
palette.warning,
t!("resource_list.status_installing").to_string()
),
"Removing" => (
"\u{25CC}",
palette.error,
t!("resource_list.status_removing").to_string()
),
other => ("\u{25D0}", palette.warning, other.to_lowercase())
}
}
#[allow(clippy::too_many_lines)]
pub fn render(frame: &mut Frame, area: Rect, app: &App, border_color: Color) {
let palette = app.theme.palette();
let items: Vec<ListItem> = match app.active_tab {
ResourceTab::Servers => app
.servers
.iter()
.map(|s| {
let (icon, status_color, label) = server_status_view(&s.status, &palette);
let line = Line::from(vec![
Span::raw(format!("{icon} ")),
Span::styled(&s.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(format!("[{label}]"), Style::default().fg(status_color)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Databases => app
.databases
.iter()
.map(|d| {
let line = Line::from(vec![
Span::raw("\u{25CF} "),
Span::styled(&d.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!("[{}]", d.engine),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::S3 => app
.s3_storages
.iter()
.map(|s| {
let line = Line::from(vec![
Span::raw("\u{1F4E6} "),
Span::styled(&s.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(&s.region, Style::default().fg(palette.warning)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Kubernetes => app
.k8s_clusters
.iter()
.map(|c| {
let line = Line::from(vec![
Span::raw("\u{2638} "),
Span::styled(&c.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!("[v{}]", c.version),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Projects => app
.projects
.iter()
.map(|p| {
let line = Line::from(vec![
Span::raw("\u{1F4C1} "),
Span::styled(&p.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!(
"[{}]",
t!("resource_list.count_servers", n => p.server_count)
),
Style::default().fg(palette.dim)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Balancers => app
.balancers
.iter()
.map(|b| {
let line = Line::from(vec![
Span::raw("\u{2696} "),
Span::styled(&b.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(&b.ip, Style::default().fg(palette.warning)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Registry => app
.registries
.iter()
.map(|r| {
let line = Line::from(vec![
Span::raw("\u{1F433} "),
Span::styled(&r.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!(
"[{}]",
t!("resource_list.count_repos", n => r.repository_count)
),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Domains => app
.domains
.iter()
.map(|d| {
let line = Line::from(vec![
Span::raw("\u{1F310} "),
Span::styled(&d.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!("[{}]", d.status),
Style::default().fg(palette.success)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Firewall => app
.firewalls
.iter()
.map(|f| {
let line = Line::from(vec![
Span::raw("\u{1F6E1} "),
Span::styled(&f.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!("[{}]", t!("resource_list.count_rules", n => f.rule_count)),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::FloatingIps => app
.floating_ips
.iter()
.map(|f| {
let line = Line::from(vec![
Span::raw("\u{1F500} "),
Span::styled(&f.ip, Style::default().fg(palette.warning)),
Span::raw(" "),
Span::styled(&f.server_name, Style::default().fg(palette.fg)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Images => app
.images
.iter()
.map(|i| {
let line = Line::from(vec![
Span::raw("\u{1F5BC} "),
Span::styled(&i.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!("[{} MB]", i.size_mb),
Style::default().fg(palette.dim)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::NetworkDrives => app
.network_drives
.iter()
.map(|n| {
let line = Line::from(vec![
Span::raw("\u{1F4BE} "),
Span::styled(&n.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!("[{} GB]", n.size_gb),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Vpc => app
.vpcs
.iter()
.map(|v| {
let line = Line::from(vec![
Span::raw("\u{1F517} "),
Span::styled(&v.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!(
"[{}]",
t!("resource_list.count_subnets", n => v.subnet_count)
),
Style::default().fg(palette.warning)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::DedicatedServers => app
.dedicated_servers
.iter()
.map(|d| {
let line = Line::from(vec![
Span::raw("\u{1F5A5} "),
Span::styled(&d.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(&d.status, Style::default().fg(palette.success)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Mail => app
.mails
.iter()
.map(|m| {
let line = Line::from(vec![
Span::raw("\u{1F4E7} "),
Span::styled(&m.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!(
"[{}]",
t!("resource_list.count_mailboxes", n => m.mailbox_count)
),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Apps => app
.apps
.iter()
.map(|a| {
let line = Line::from(vec![
Span::raw("\u{1F680} "),
Span::styled(&a.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!(
"[{}]",
t!("resource_list.count_deploys", n => a.deploy_count)
),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::AiAgents => app
.ai_agents
.iter()
.map(|a| {
let line = Line::from(vec![
Span::raw("\u{1F916} "),
Span::styled(&a.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(&a.model, Style::default().fg(palette.warning)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::KnowledgeBases => app
.knowledge_bases
.iter()
.map(|k| {
let line = Line::from(vec![
Span::raw("\u{1F4DA} "),
Span::styled(&k.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(
format!(
"[{}]",
t!("resource_list.count_docs", n => k.document_count)
),
Style::default().fg(palette.dim)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::SshKeys => app
.ssh_keys
.iter()
.map(|k| {
let line = Line::from(vec![
Span::raw("\u{1F511} "),
Span::styled(k, Style::default().fg(palette.fg)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Finances => app
.finances
.iter()
.map(|f| {
let line = Line::from(vec![
Span::raw("\u{1F4B0} "),
Span::styled(f, Style::default().fg(palette.fg)),
]);
ListItem::new(line)
})
.collect()
};
let indices = app.filtered_indices();
let items: Vec<ListItem> = indices
.iter()
.filter_map(|&i| items.get(i).cloned())
.collect();
let fallback = t!("resource_list.title_fallback").to_string();
let tab_name = ResourceTab::names()
.get(app.active_tab.index())
.copied()
.unwrap_or(fallback.as_str());
let title = if app.filter_active() {
let cursor = if app.filter_editing { "\u{2588}" } else { "" };
format!(" {tab_name} ({}) /{}{cursor} ", items.len(), app.filter)
} else {
format!(" {tab_name} ({}) ", items.len())
};
let list = List::new(items)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(border_color))
.title(Line::from(Span::styled(
title,
Style::default()
.fg(palette.title)
.add_modifier(Modifier::BOLD)
)))
)
.highlight_style(
Style::default()
.fg(palette.bg)
.bg(palette.accent)
.add_modifier(Modifier::BOLD)
)
.highlight_symbol("\u{2503} ");
let mut state = ListState::default();
state.select(Some(app.selected));
frame.render_stateful_widget(list, area, &mut state);
}
pub struct ResourceListWidget {
enabled: bool
}
impl ResourceListWidget {
#[must_use]
pub const fn new(enabled: bool) -> Self {
Self {
enabled
}
}
}
impl crate::tui::widgets::Widget for ResourceListWidget {
fn id(&self) -> &'static str {
"resource_list"
}
fn name(&self) -> &'static str {
"Resource List"
}
fn enabled(&self) -> bool {
self.enabled
}
fn toggle(&mut self) {
self.enabled = !self.enabled;
}
fn render(&self, frame: &mut Frame, area: Rect, app: &App) {
let border_color = if app.focus == crate::tui::app::Focus::ResourceList {
app.theme.palette().accent
} else {
app.theme.palette().border
};
render(frame, area, app, border_color);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::themes::Theme;
#[test]
fn server_status_on_is_running() {
let palette = Theme::GruvboxDark.palette();
let (icon, color, label) = server_status_view("On", &palette);
assert_eq!(icon, "\u{25B6}");
assert_eq!(color, palette.success);
assert_eq!(label, "running");
}
#[test]
fn server_status_off_is_stopped() {
let palette = Theme::GruvboxDark.palette();
let (icon, color, label) = server_status_view("Off", &palette);
assert_eq!(icon, "\u{25CB}");
assert_eq!(color, palette.error);
assert_eq!(label, "stopped");
}
#[test]
fn server_status_transitional_is_warning() {
let palette = Theme::GruvboxDark.palette();
let (_, color, label) = server_status_view("Rebooting", &palette);
assert_eq!(color, palette.warning);
assert_eq!(label, "rebooting");
}
#[test]
fn server_status_unknown_falls_back_to_lowercased() {
let palette = Theme::GruvboxDark.palette();
let (_, color, label) = server_status_view("SomethingNew", &palette);
assert_eq!(color, palette.warning);
assert_eq!(label, "somethingnew");
}
}