use super::*;
pub(crate) fn draw_apps_table(f: &mut Frame, area: Rect, app: &mut App) {
let theme = app.theme.clone();
let header = Row::new(
[
"NAME",
"ENVS",
"RED",
"UPDATING",
"VERSIONS",
"UPDATED",
"LATEST",
"DESCRIPTION",
]
.map(|h| {
Cell::from(h).style(
Style::default()
.fg(theme.title)
.add_modifier(Modifier::BOLD),
)
}),
)
.height(1);
let now = chrono::Utc::now();
let rows: Vec<Row> = app
.applications
.iter()
.enumerate()
.map(|(i, a)| {
let age = |d: Option<chrono::DateTime<chrono::Utc>>| -> String {
d.map(|t| humanize_age(now.signed_duration_since(t)))
.unwrap_or_else(|| "—".into())
};
let latest_cell = match (a.latest_version_label.as_deref(), a.latest_version_created) {
(Some(label), Some(created)) => Cell::from(Line::from(vec![
Span::styled(
label.to_string(),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" {}", humanize_age(now.signed_duration_since(created))),
Style::default().fg(age_color(Some(created), now, &theme)),
),
])),
(Some(label), None) => Cell::from(Span::styled(
label.to_string(),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
)),
_ => Cell::from(Span::styled("—", Style::default().fg(theme.muted))),
};
let rollup = crate::app::app_rollup(&app.environments, &a.name, &app.worker_dlq_depths);
let red_style = if rollup.red_count > 0 {
Style::default()
.fg(theme.health_red)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.muted)
};
let updating_style = if rollup.updating_count > 0 {
Style::default()
.fg(theme.status_updating)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(theme.muted)
};
let total_alerting = rollup.red_count + rollup.worker_dlq_alerts;
let pinned = app.pinned_apps.contains(&a.name);
let selected = app.apps_selected.contains(&a.name);
let prefix = if pinned {
Span::styled(
glyph(theme.icons, "★ ", "* "),
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
)
} else if selected {
Span::styled(
glyph(theme.icons, "▶ ", "> "),
Style::default()
.fg(theme.title_alt)
.add_modifier(Modifier::BOLD),
)
} else {
Span::raw(" ")
};
let name_cell = Cell::from(Line::from(vec![
prefix,
Span::styled(
a.name.clone(),
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
]));
let r = Row::new(vec![
name_cell,
Cell::from(rollup.env_count.to_string())
.style(Style::default().fg(theme.text).add_modifier(Modifier::BOLD)),
Cell::from(total_alerting.to_string()).style(red_style),
Cell::from(rollup.updating_count.to_string()).style(updating_style),
Cell::from(a.version_count.to_string())
.style(Style::default().fg(theme.app_palette[0])),
Cell::from(age(a.date_updated)).style(Style::default().fg(age_color(
a.date_updated,
now,
&theme,
))),
latest_cell,
Cell::from(a.description.clone()).style(Style::default().fg(theme.text)),
]);
if selected {
r.style(Style::default().bg(theme.row_selected_bg))
} else if i % 2 == 0 {
r.style(Style::default().bg(theme.row_alt_bg))
} else {
r
}
})
.collect();
let title = format!("Applications {}", app.applications.len());
let widths = [
Constraint::Percentage(20),
Constraint::Length(5), Constraint::Length(4), Constraint::Length(9), Constraint::Length(8), Constraint::Length(8), Constraint::Percentage(22), Constraint::Percentage(28), ];
let popup_open = matches!(
app.mode,
Mode::Help | Mode::Picker | Mode::Command | Mode::Action | Mode::Filter
);
let table = Table::new(rows, widths)
.header(header)
.row_highlight_style(
Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD),
)
.highlight_symbol(cursor_marker(&theme))
.block(
titled_block(&theme, &title, !popup_open, theme.title).padding(Padding::horizontal(1)),
);
f.render_stateful_widget(table, area, &mut app.app_table_state);
}
pub(crate) fn visible_columns(
multi_regions: &[String],
cost_enabled: bool,
hidden_cols: &std::collections::BTreeSet<String>,
compact: bool,
) -> Vec<(&'static str, SortKey)> {
let mut full = vec![
("NAME", SortKey::Name),
("APPLICATION", SortKey::App),
("TIER", SortKey::App),
("STATUS", SortKey::Status),
("HEALTH", SortKey::Health),
("INST", SortKey::Health),
("TREND", SortKey::Health),
("PLATFORM", SortKey::Version),
("VERSION", SortKey::Version),
("CNAME", SortKey::Name),
("AGE", SortKey::Age),
];
if !multi_regions.is_empty() {
full.insert(1, ("REGION", SortKey::App));
}
if cost_enabled {
let age_idx = full
.iter()
.position(|(l, _)| *l == "AGE")
.unwrap_or(full.len());
full.insert(age_idx, ("COST", SortKey::Name));
}
if compact {
full.retain(|(label, _)| !matches!(*label, "TREND" | "PLATFORM"));
}
let columns: Vec<(&'static str, SortKey)> = full
.into_iter()
.filter(|(label, _)| {
if *label == "NAME" {
return true;
}
!hidden_cols.contains(*label)
})
.collect();
columns
}
pub(crate) struct CellCtx<'a> {
pub theme: &'a Theme,
pub e: &'a Environment,
pub redact: bool,
pub dlq_depth: i64,
pub instance_counts: Option<crate::aws::EnvInstanceCounts>,
pub history: Option<&'a std::collections::VecDeque<String>>,
pub newly_red: bool,
pub stale_platform: Option<&'a String>,
pub cost: Option<f64>,
pub name_cell: Cell<'a>,
pub age: String,
pub color: Color,
pub now: chrono::DateTime<chrono::Utc>,
}
pub(crate) fn env_cell<'a>(label: &str, ctx: &CellCtx<'a>) -> Cell<'a> {
let CellCtx {
theme,
e,
name_cell,
age,
color,
now,
..
} = ctx;
let (color, now) = (*color, *now);
match label {
"NAME" => name_cell.clone(),
"APPLICATION" => Cell::from(Span::raw(e.application.as_str()))
.style(Style::default().fg(color).add_modifier(Modifier::BOLD)),
"TIER" => tier_cell(&e.tier, theme),
"STATUS" => {
let dlq = if e.tier.eq_ignore_ascii_case("Worker") {
ctx.dlq_depth
} else {
0
};
let alert = status_alert(&e.health, dlq);
if dlq > 0 {
Cell::from(Line::from(vec![
status_pill_for(&e.status, theme, alert),
Span::styled(
format!(" {}{dlq}", warn_glyph(theme.icons).trim_end()),
Style::default()
.fg(theme.health_red)
.add_modifier(Modifier::BOLD),
),
]))
} else {
Cell::from(status_pill_for(&e.status, theme, alert))
}
}
"HEALTH" => Cell::from(health_dot(&e.health, theme)),
"INST" => {
let counts = ctx.instance_counts;
let (text, color) = format_instance_counts(counts, theme);
Cell::from(Span::styled(
text,
Style::default().fg(color).add_modifier(Modifier::BOLD),
))
}
"TREND" => Cell::from(sparkline_for(ctx.history, theme, ctx.newly_red)),
"PLATFORM" => {
let style = platform_style(&e.platform);
let colour = style
.as_ref()
.and_then(|s| theme.app_palette.get(s.palette_idx).copied())
.unwrap_or(theme.muted);
let icon = if theme.icons == IconStyle::Powerline {
style.as_ref().map(|s| s.icon)
} else {
None
};
let stale = ctx.stale_platform;
let name_colour = if stale.is_some() {
theme.health_yellow
} else {
colour
};
let mut spans = Vec::new();
if let Some(g) = icon {
spans.push(Span::styled(format!("{g} "), Style::default().fg(colour)));
}
spans.push(Span::styled(
e.platform.as_str(),
Style::default().fg(name_colour),
));
if stale.is_some() {
spans.push(Span::styled(
format!(" {}", stale_glyph(theme.icons)),
Style::default().fg(theme.health_yellow),
));
}
Cell::from(Line::from(spans))
}
"VERSION" => Cell::from(Span::raw(e.version_label.as_str()))
.style(Style::default().fg(theme.app_palette[0])),
"CNAME" => Cell::from(redact(&e.cname, ctx.redact)).style(Style::default().fg(theme.muted)),
"AGE" => {
Cell::from(age.clone()).style(Style::default().fg(age_color(e.updated, now, theme)))
}
"REGION" => Cell::from(Span::raw(e.region.as_deref().unwrap_or_default()))
.style(Style::default().fg(theme.accent)),
"COST" => {
match ctx.cost {
Some(cost) => {
let text = format!("${cost:.0}");
let fg = if cost >= 500.0 {
theme.health_red
} else if cost >= 50.0 {
theme.text
} else {
theme.health_green
};
Cell::from(text).style(Style::default().fg(fg).add_modifier(Modifier::BOLD))
}
None => Cell::from(Span::styled("—", Style::default().fg(theme.muted))),
}
}
_ => Cell::from(""),
}
}
pub(crate) fn draw_table(f: &mut Frame, area: Rect, app: &mut App) {
app.table_area = area;
let theme = app.theme.clone();
let compact = app.view.mode == ViewMode::Compact;
let spacious = app.view.mode == ViewMode::Spacious;
let row_height: u16 = if spacious { 2 } else { 1 };
let block_padding: u16 = if spacious { 2 } else { 1 };
let indexes = app.filtered_indexes();
let columns = visible_columns(
&app.multi_regions,
app.cost_enabled,
&app.view.hidden_cols,
compact,
);
let sort_marker = if app.view.sort_desc() {
glyph(app.theme.icons, " ▼", " v")
} else {
glyph(app.theme.icons, " ▲", " ^")
};
let trend_window =
crate::app::humanize_short_age(app.refresh_interval * crate::app::HISTORY_CAP as u32);
let header_cells: Vec<Cell> = columns
.iter()
.map(|(label, key)| {
let display: std::borrow::Cow<'_, str> = if *label == "HEALTH" {
"●".into()
} else if *label == "TREND" {
format!("TREND ({trend_window})").into()
} else {
(*label).into()
};
let mut text = display.into_owned();
let primary_match = matches!(
(key, app.view.sort_key()),
(SortKey::Name, SortKey::Name)
| (SortKey::App, SortKey::App)
| (SortKey::Status, SortKey::Status)
| (SortKey::Health, SortKey::Health)
| (SortKey::Age, SortKey::Age)
| (SortKey::Version, SortKey::Version)
);
let show_marker = primary_match && !matches!(*label, "TREND" | "CNAME" | "TIER");
if show_marker {
text.push_str(sort_marker);
}
Cell::from(text).style(
Style::default()
.fg(theme.title)
.add_modifier(Modifier::BOLD),
)
})
.collect();
let header = Row::new(header_cells).height(1);
let app_colors = app.view.app_colors();
let hover = if app.mode == Mode::Normal {
app.hover_row
} else {
None
};
let display = app.display_rows();
let now = chrono::Utc::now();
let mut env_idx: usize = 0;
let rows: Vec<Row> = display
.iter()
.enumerate()
.map(|(row_idx, row)| match row {
DisplayRow::Env(i) => {
let env_position = env_idx;
env_idx += 1;
let e = &app.environments[*i];
let color = app_colors
.get(&e.application)
.copied()
.unwrap_or(theme.text);
let age = e
.updated
.map(|u| humanize_age(now.signed_duration_since(u)))
.unwrap_or_else(|| "—".into());
let display_name = app
.aliases
.get(&e.name)
.cloned()
.unwrap_or_else(|| e.name.clone());
let star = if app.pinned.contains(&e.name) {
glyph(app.theme.icons, "★ ", "* ")
} else {
""
};
let checked = if app.multi_selected.contains(&e.name) {
glyph(app.theme.icons, "✓ ", "x ")
} else {
""
};
let added_marker = if app.newly_added.contains(&e.name) {
"+ "
} else {
""
};
let alert = if app.newly_red.contains(&e.name) {
glyph(app.theme.icons, "▲ ", "! ")
} else {
""
};
let (drift_glyph, drift_color) = match e.updated {
Some(u) => {
let dur = now.signed_duration_since(u);
if dur < chrono::Duration::hours(24) && dur > chrono::Duration::zero() {
(glyph(app.theme.icons, "◆ ", "# "), theme.title_alt)
} else if dur > chrono::Duration::days(30) {
(glyph(app.theme.icons, "◇ ", "o "), theme.muted)
} else {
("", theme.text)
}
}
None => ("", theme.text),
};
let name_cell = Cell::from(Line::from(vec![
Span::styled(
checked.to_string(),
Style::default()
.fg(theme.title_alt)
.add_modifier(Modifier::BOLD),
),
Span::styled(
star.to_string(),
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(
alert.to_string(),
Style::default()
.fg(theme.health_red)
.add_modifier(Modifier::BOLD),
),
Span::styled(
added_marker.to_string(),
Style::default()
.fg(theme.health_green)
.add_modifier(Modifier::BOLD),
),
Span::styled(
drift_glyph.to_string(),
Style::default()
.fg(drift_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(
if app.tf_managed_envs.contains(&e.name) {
glyph(app.theme.icons, "ⓣ ", "t ")
} else {
""
},
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
),
Span::styled(
display_name,
Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
),
]));
let cell_ctx = CellCtx {
theme: &theme,
e,
name_cell,
age,
color,
now,
redact: app.view.redact,
dlq_depth: app.worker_dlq_depths.get(&e.name).copied().unwrap_or(0),
instance_counts: app.env_instance_counts.get(&e.name).copied(),
history: app.history.get(&e.name),
newly_red: app.newly_red.contains(&e.name),
stale_platform: app.view.stale_platforms().get(&e.name),
cost: app.costs.get(&e.name).copied(),
};
let cells: Vec<Cell> = columns
.iter()
.map(|(label, _)| env_cell(label, &cell_ctx))
.collect();
let is_hover = hover == Some(row_idx);
let dlq_red = e.tier.eq_ignore_ascii_case("Worker")
&& app.worker_dlq_depths.get(&e.name).copied().unwrap_or(0) > 0;
let bg = if dlq_red
|| e.health.eq_ignore_ascii_case("Red")
|| e.health.eq_ignore_ascii_case("Severe")
{
Some(theme.row_red_bg)
} else if e.health.eq_ignore_ascii_case("Yellow") {
Some(theme.row_yellow_bg)
} else if is_hover {
Some(theme.row_hover_bg)
} else if env_position.is_multiple_of(2) {
Some(theme.row_alt_bg)
} else {
None
};
let style = match bg {
Some(c) => Style::default().bg(c),
None => Style::default(),
};
Row::new(cells).style(style).height(row_height)
}
DisplayRow::Separator => {
let (next_app_name, next_color) = display
.iter()
.skip(row_idx + 1)
.find_map(|r| match r {
DisplayRow::Env(i) => {
let env = &app.environments[*i];
Some((
env.application.clone(),
app_colors
.get(&env.application)
.copied()
.unwrap_or(theme.muted),
))
}
_ => None,
})
.unwrap_or_else(|| (String::new(), theme.muted));
let group_envs: Vec<&Environment> = display
.iter()
.skip(row_idx + 1)
.map_while(|r| match r {
DisplayRow::Env(i) => Some(&app.environments[*i]),
DisplayRow::Separator => None,
})
.collect();
let summary = summarize_group(&group_envs);
let dashes = "─".repeat(DIVIDER_FILL_WIDTH);
let count = columns.len();
if theme.icons == IconStyle::Powerline && !next_app_name.is_empty() {
let summary_text = summary.clone();
let cells: Vec<Cell> = columns
.iter()
.enumerate()
.map(|(i, (label, _))| {
if i == 0 && *label == "NAME" {
Cell::from(Line::from(vec![
Span::styled("\u{e0b2}", Style::default().fg(next_color)),
Span::styled(
format!(" {next_app_name} "),
Style::default()
.fg(theme.contrast_text(next_color))
.bg(next_color)
.add_modifier(Modifier::BOLD),
),
Span::styled("\u{e0b0}", Style::default().fg(next_color)),
]))
} else if i == 1 {
Cell::from(Span::styled(
format!(" {summary_text} "),
Style::default().fg(theme.muted),
))
} else {
Cell::from(Span::styled(
dashes.clone(),
Style::default().fg(next_color),
))
}
})
.collect();
Row::new(cells)
} else if !next_app_name.is_empty() {
let glyph = separator_glyph(theme.icons);
let summary_text = summary.clone();
let cells: Vec<Cell> = columns
.iter()
.enumerate()
.map(|(i, (label, _))| {
if i == 0 && *label == "NAME" {
Cell::from(Line::from(vec![
Span::styled(
"── ".to_string(),
Style::default().fg(theme.muted),
),
Span::styled(
format!("{glyph} "),
Style::default()
.fg(next_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(
next_app_name.clone(),
Style::default()
.fg(next_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" ──".to_string(),
Style::default().fg(theme.muted),
),
]))
} else if i == 1 {
Cell::from(Span::styled(
format!(" {summary_text} "),
Style::default().fg(theme.muted),
))
} else {
Cell::from(Span::styled(
dashes.clone(),
Style::default().fg(next_color),
))
}
})
.collect();
Row::new(cells)
} else {
let cells = (0..count).map(|_| {
Cell::from(Span::styled(
dashes.clone(),
Style::default().fg(next_color),
))
});
Row::new(cells)
}
}
})
.collect();
let title = format!("Environments {}/{}", indexes.len(), app.environments.len());
let widths: Vec<Constraint> = columns
.iter()
.map(|(label, _)| match *label {
"NAME" => Constraint::Percentage(14),
"APPLICATION" => Constraint::Percentage(12),
"TIER" => Constraint::Length(11),
"STATUS" => Constraint::Length(10),
"HEALTH" => Constraint::Length(3),
"INST" => Constraint::Length(7),
"TREND" => Constraint::Length(12),
"PLATFORM" => Constraint::Percentage(15),
"VERSION" => Constraint::Percentage(10),
"CNAME" => Constraint::Percentage(14),
"AGE" => Constraint::Length(6),
"COST" => Constraint::Length(8),
_ => Constraint::Length(6),
})
.collect();
let popup_open = matches!(
app.mode,
Mode::Help | Mode::Picker | Mode::Command | Mode::Action | Mode::Filter
);
let block = titled_block(&theme, &title, !popup_open, theme.title)
.padding(Padding::horizontal(block_padding));
let table = Table::new(rows, widths)
.header(header)
.row_highlight_style(
Style::default().add_modifier(Modifier::REVERSED | Modifier::BOLD),
)
.highlight_symbol(cursor_marker(&theme))
.block(block);
let hover_preview: Option<(Rect, String)> = hover.and_then(|idx| match display.get(idx)? {
DisplayRow::Env(i) => {
let e = &app.environments[*i];
let alias_part = match app.aliases.get(&e.name) {
Some(a) => format!(" alias \"{a}\""),
None => String::new(),
};
let preview = format!(
" ⓘ {}{} · {} · {} / {} · {} · {}",
e.name,
alias_part,
e.application,
e.status,
e.health,
e.platform,
redact(&e.cname, app.view.redact),
);
let row = Rect {
x: area.x,
y: area.y + area.height.saturating_sub(1),
width: area.width,
height: 1,
};
Some((row, preview))
}
_ => None,
});
let env_count_total = app.environments.len();
let env_count_visible = indexes.len();
f.render_stateful_widget(table, area, &mut app.table_state);
if env_count_visible == 0 {
let heading: String;
let hint: String;
if env_count_total == 0 {
heading = "no envs in this account / region".to_string();
hint = "try a different region (r) or profile (p), or check the AWS console (b)"
.to_string();
} else if app.view.filter().is_empty() {
heading = "no envs match the active view".to_string();
hint = "type `:views` to switch back to default, or `:filters` to drop a saved one"
.to_string();
} else {
heading = format!("no envs match `{}`", app.view.filter().text());
hint = "press / to edit, or Esc in filter mode to clear".to_string();
}
let block_height: u16 = 4;
let inner = Rect {
x: area.x + 2,
y: area
.y
.saturating_add(area.height.saturating_sub(block_height) / 2),
width: area.width.saturating_sub(4),
height: block_height.min(area.height),
};
let lines = vec![
Line::from(Span::styled(
heading,
Style::default()
.fg(theme.title_alt)
.add_modifier(Modifier::BOLD),
))
.alignment(Alignment::Center),
Line::from(Span::raw("")),
Line::from(Span::styled(hint, Style::default().fg(theme.muted)))
.alignment(Alignment::Center),
];
f.render_widget(Paragraph::new(lines), inner);
}
if let Some((row, preview)) = hover_preview {
let para = Paragraph::new(Span::styled(
preview,
Style::default()
.bg(theme.row_hover_bg)
.fg(theme.text)
.add_modifier(Modifier::DIM),
));
f.render_widget(Clear, row);
f.render_widget(para, row);
}
}
pub(crate) fn tier_cell(tier: &str, theme: &Theme) -> Cell<'static> {
let label_width = "Worker".chars().count();
let (web_icon, worker_icon) = tier_icons(theme.icons);
match tier {
"Worker" => Cell::from(Line::from(vec![
pill(
&format!("{worker_icon} {:<label_width$}", "Worker"),
theme.contrast_text(theme.accent),
theme.accent,
),
Span::raw(" "),
])),
"Web" => Cell::from(Line::from(vec![
pill(
&format!("{web_icon} {:<label_width$}", "Web"),
theme.contrast_text(theme.title),
theme.title,
),
Span::raw(" "),
])),
other => Cell::from(Span::styled(
other.to_string(),
Style::default().fg(theme.muted),
)),
}
}
pub(crate) struct PlatformStyle {
icon: &'static str,
palette_idx: usize,
}
pub(crate) fn platform_style(family: &str) -> Option<PlatformStyle> {
let lc = family.to_ascii_lowercase();
let (icon, palette_idx) = if lc.contains("node") {
("\u{e718}", 2) } else if lc.contains("java") || lc.contains("tomcat") || lc.contains("corretto") {
("\u{e738}", 3) } else if lc.contains("python") {
("\u{e73c}", 0) } else if lc.contains("ruby") {
("\u{e791}", 5) } else if lc.contains("php") {
("\u{e73d}", 6) } else if lc.contains(".net") || lc.contains("iis") {
("\u{e77f}", 1) } else if lc.contains("docker") {
("\u{e7b0}", 7) } else if lc.contains("go ") || lc.ends_with(" go") || lc == "go" {
("\u{e626}", 9) } else {
return None;
};
Some(PlatformStyle { icon, palette_idx })
}
pub(crate) fn tier_icons(icons: IconStyle) -> (&'static str, &'static str) {
match icons {
IconStyle::Ascii => ("W", "K"),
_ => ("⊕", "⚒"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusAlert {
None,
Yellow,
Red,
}
pub fn format_instance_counts(
counts: Option<crate::aws::EnvInstanceCounts>,
theme: &Theme,
) -> (String, Color) {
let Some(c) = counts else {
return ("—".into(), theme.muted);
};
let text = format!("{}/{}", c.healthy, c.total);
let color = if c.total == 0 {
theme.muted
} else if c.healthy == c.total {
theme.health_green
} else if c.healthy == 0 {
theme.health_red
} else {
theme.health_yellow
};
(text, color)
}
pub fn status_alert(health: &str, dlq: i64) -> StatusAlert {
if health.eq_ignore_ascii_case("Red") || health.eq_ignore_ascii_case("Severe") {
StatusAlert::Red
} else if health.eq_ignore_ascii_case("Yellow")
|| health.eq_ignore_ascii_case("Warning")
|| health.eq_ignore_ascii_case("Degraded")
|| dlq > 0
{
StatusAlert::Yellow
} else {
StatusAlert::None
}
}
pub(crate) fn status_pill(status: &str, theme: &Theme) -> Span<'static> {
status_pill_for(status, theme, StatusAlert::None)
}
pub(crate) fn status_pill_for(status: &str, theme: &Theme, alert: StatusAlert) -> Span<'static> {
if status.eq_ignore_ascii_case("ready") {
match alert {
StatusAlert::Red => Span::styled(
" Ready ",
Style::default()
.fg(theme.health_red)
.add_modifier(Modifier::BOLD),
),
StatusAlert::Yellow => Span::styled(
" Ready ",
Style::default()
.fg(theme.health_yellow)
.add_modifier(Modifier::BOLD),
),
StatusAlert::None => pill(
"Ready",
theme.contrast_text(theme.status_ready),
theme.status_ready,
),
}
} else if ieq_any(status, &["updating", "launching"]) {
Span::styled(
format!(" {status} "),
Style::default()
.fg(theme.contrast_text(theme.status_updating))
.bg(theme.status_updating)
.add_modifier(Modifier::BOLD | Modifier::SLOW_BLINK),
)
} else if ieq_any(status, &["terminating", "terminated"]) {
pill(
status,
theme.contrast_text(theme.status_terminating),
theme.status_terminating,
)
} else {
Span::styled(status.to_string(), Style::default().fg(theme.text))
}
}
pub(crate) fn summarize_group(envs: &[&Environment]) -> String {
if envs.is_empty() {
return String::new();
}
let total = envs.len();
let mut web = 0usize;
let mut worker = 0usize;
let mut red = 0usize;
let mut yellow = 0usize;
for e in envs {
match e.tier.as_str() {
"Web" => web += 1,
"Worker" => worker += 1,
_ => {}
}
match e.health.to_lowercase().as_str() {
"red" | "severe" | "degraded" => red += 1,
"yellow" | "warning" => yellow += 1,
_ => {}
}
}
let env_word = if total == 1 { "env" } else { "envs" };
let mut parts: Vec<String> = vec![format!("{total} {env_word}")];
if web > 0 && worker > 0 {
parts.push(format!("{web} Web"));
parts.push(format!("{worker} Worker"));
}
if red > 0 {
parts.push(format!("{red} red"));
}
if yellow > 0 {
parts.push(format!("{yellow} yellow"));
}
parts.join(" · ")
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn cols(regions: &[&str], cost: bool, hidden: &[&str], compact: bool) -> Vec<&'static str> {
let regions: Vec<String> = regions.iter().map(|s| s.to_string()).collect();
let hidden: BTreeSet<String> = hidden.iter().map(|s| s.to_string()).collect();
visible_columns(®ions, cost, &hidden, compact)
.into_iter()
.map(|(l, _)| l)
.collect()
}
#[test]
fn the_column_set_follows_its_four_inputs() {
let base = cols(&[], false, &[], false);
assert_eq!(base.first(), Some(&"NAME"));
assert!(!base.contains(&"REGION"), "REGION is fan-out only");
assert!(!base.contains(&"COST"), "COST is opt-in");
let fanned = cols(&["us-east-1", "eu-west-2"], false, &[], false);
assert_eq!(fanned[1], "REGION");
let costed = cols(&[], true, &[], false);
let ci = costed
.iter()
.position(|c| *c == "COST")
.expect("COST shown");
assert_eq!(costed[ci + 1], "AGE");
let compact = cols(&[], false, &[], true);
assert!(!compact.contains(&"TREND"));
assert!(!compact.contains(&"PLATFORM"));
assert!(compact.contains(&"STATUS"), "only those two");
let hidden = cols(&[], false, &["CNAME", "VERSION"], false);
assert!(!hidden.contains(&"CNAME") && !hidden.contains(&"VERSION"));
let hidden = cols(&[], false, &["NAME"], false);
assert_eq!(hidden.first(), Some(&"NAME"));
let no_trend = cols(&[], false, &["TREND"], false);
assert!(no_trend.contains(&"HEALTH") && !no_trend.contains(&"TREND"));
let no_health = cols(&[], false, &["HEALTH"], false);
assert!(no_health.contains(&"TREND") && !no_health.contains(&"HEALTH"));
let all: Vec<&str> = base.to_vec();
let everything = cols(&[], false, &all, false);
assert_eq!(everything, vec!["NAME"]);
}
}