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())
}
}
#[must_use]
pub fn status_view(status: &str, palette: &Palette) -> (Color, String) {
let label = status.to_lowercase();
let color = match label.as_str() {
"on" | "started" | "active" | "running" | "ok" | "ready" | "created" | "deployed"
| "delivered" | "attached" | "available" | "success" | "finished" | "normal" => {
palette.success
}
"off" | "stopped" | "error" | "failure" | "failed" | "blocked" | "removed" | "damaged"
| "disabled" | "nopaid" | "startuperror" | "cancelled" | "expired" => palette.error,
_ => palette.warning
};
(color, label)
}
fn registry_used_percent(registry: &crate::tui::app::RegistrySummary) -> i64 {
if registry.disk_size <= 0 {
0
} else {
registry.disk_used * 100 / registry.disk_size
}
}
#[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_resources", n => p.resource_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.disk_used", pct => registry_used_percent(r))
),
Style::default().fg(palette.accent)
),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::Domains => app
.domains
.iter()
.map(|d| {
let (status_color, label) = status_view(&d.status, &palette);
let line = Line::from(vec![
Span::raw("\u{1F310} "),
Span::styled(&d.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(format!("[{label}]"), Style::default().fg(status_color)),
]);
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!("[{}]", f.policy),
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(v.subnet.clone(), Style::default().fg(palette.warning)),
]);
ListItem::new(line)
})
.collect(),
ResourceTab::DedicatedServers => app
.dedicated_servers
.iter()
.map(|d| {
let (status_color, label) = status_view(&d.status, &palette);
let line = Line::from(vec![
Span::raw("\u{1F5A5} "),
Span::styled(&d.name, Style::default().fg(palette.fg)),
Span::raw(" "),
Span::styled(format!("[{label}]"), Style::default().fg(status_color)),
]);
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(m.owner.clone(), Style::default().fg(palette.dim)),
]);
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(a.location.clone(), Style::default().fg(palette.warning)),
]);
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(
format!("[{}/{}]", a.tokens_used, a.tokens_total),
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 tab_name = app.active_tab.display_name();
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");
}
}