use crate::app::{App, SortColumn, SortOrder};
use crate::models::{GitlabMrState, MergeabilityStatus, MrStatus};
use crate::ui::inspector::create_chip_span;
use ratatui::{
layout::{Constraint, Rect},
style::{Color, Modifier, Style, Stylize},
text::{Line, Span},
widgets::{Cell, Row, Table},
};
pub const BADGE_WIDTH: usize = 18;
pub fn badge_label(text: &str) -> String {
format!("{:^width$}", text, width = BADGE_WIDTH)
}
fn state_badge(
state: &GitlabMrState,
mergeability: &MergeabilityStatus,
tick: u64,
) -> Cell<'static> {
if *state != GitlabMrState::Opened {
let (text, fg, bg) = match state {
GitlabMrState::Merged => ("MERGED", Color::Black, Color::Magenta),
GitlabMrState::Closed => ("CLOSED", Color::Black, Color::Red),
GitlabMrState::Opened => unreachable!(),
};
return Cell::from(Line::from(Span::styled(
badge_label(text),
Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD),
)));
}
if tick.is_multiple_of(2) {
return Cell::from(Line::from(Span::styled(
badge_label("OPEN"),
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD),
)));
}
let (text, fg, bg) = match mergeability {
MergeabilityStatus::Mergeable => ("MERGEABLE", Color::Black, Color::LightGreen),
MergeabilityStatus::Conflict => ("CONFLICT", Color::White, Color::Red),
MergeabilityStatus::NeedsRebase => ("REBASE", Color::Black, Color::Yellow),
MergeabilityStatus::NotOpen => ("CLOSED", Color::Black, Color::Red),
MergeabilityStatus::Draft => ("DRAFT", Color::White, Color::Rgb(80, 80, 80)),
MergeabilityStatus::DiscussionsNotResolved => {
("DISCUSSIONS", Color::Black, Color::LightMagenta)
}
MergeabilityStatus::CiMustPass => ("CI MUST PASS", Color::Black, Color::LightYellow),
MergeabilityStatus::CiStillRunning => ("CI STILL RUNNING", Color::Black, Color::Yellow),
MergeabilityStatus::NotApproved => ("NOT APPROVED", Color::Black, Color::LightRed),
MergeabilityStatus::RequestedChanges => ("REQUESTED CHANGES", Color::White, Color::Red),
MergeabilityStatus::Unknown => ("OPEN", Color::Black, Color::Green),
};
Cell::from(Line::from(Span::styled(
badge_label(text),
Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD),
)))
}
pub fn render_table(app: &App, area: Rect) -> Table<'static> {
let _ = area; let cols = &app.config.visible_columns;
let mut header_cells = vec![
Cell::from("MR ID"),
Cell::from("Title / API Status"),
Cell::from("Status").bold(),
];
if cols.activity {
header_cells.push(Cell::from("Activity").bold());
}
if cols.target_branch {
header_cells.push(Cell::from("Target").bold());
}
if cols.labels {
header_cells.push(Cell::from("Labels").bold());
}
if cols.milestone {
header_cells.push(Cell::from("Milestone").bold());
}
if cols.notes {
header_cells.push(Cell::from("Notes").bold());
}
for b in &app.branches {
header_cells.push(Cell::from(b.clone()).bold());
}
let header = Row::new(header_cells).bottom_margin(1).underlined();
let rows: Vec<Row> = app
.visible_mrs()
.map(|mr| {
let filtered_labels: Vec<&String> = mr
.labels
.iter()
.filter(|l| app.config.is_table_label(l))
.collect();
let label_cell = if filtered_labels.is_empty() {
Cell::from("-").dark_gray()
} else {
let mut spans = Vec::new();
for label in filtered_labels {
spans.push(create_chip_span(label, &app.config));
spans.push(ratatui::text::Span::raw(" "));
}
Cell::from(Line::from(spans))
};
let highlight = mr.recently_updated && app.update_highlight_ticks > 0;
fn maybe_highlight(cell: Cell<'static>, highlight: bool) -> Cell<'static> {
if highlight {
cell.bg(Color::Rgb(0, 90, 40))
} else {
cell
}
}
let title_cell = if mr.flagged {
let title_color = match mr.status {
MrStatus::Error => Color::Red,
_ => Color::White,
};
Cell::from(Line::from(vec![
Span::styled(
"★ ",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::styled(mr.title.clone(), Style::default().fg(title_color)),
]))
} else {
Cell::from(mr.title.clone()).fg(match mr.status {
MrStatus::Error => Color::Red,
_ => Color::White,
})
};
let mut cells = vec![
maybe_highlight(Cell::from(format!("!{}", mr.id)), highlight),
maybe_highlight(title_cell, highlight),
maybe_highlight(
state_badge(&mr.state, &mr.mergeability, app.time_left),
highlight,
),
];
if cols.activity {
let (icon, color) = app.config.activity_badge(mr.updated_at.as_deref());
cells.push(maybe_highlight(Cell::from(icon).fg(color), highlight));
}
if cols.target_branch {
cells.push(maybe_highlight(
Cell::from(mr.target_branch.clone()).fg(Color::LightBlue),
highlight,
));
}
if cols.labels {
cells.push(maybe_highlight(label_cell, highlight));
}
if cols.milestone {
cells.push(maybe_highlight(
Cell::from(mr.milestone.clone()).fg(Color::Cyan),
highlight,
));
}
if cols.notes {
let (notes_text, notes_color) = if mr.user_notes_count == 0 {
("✔ 0".to_string(), Color::DarkGray)
} else {
(format!("💬 {}", mr.user_notes_count), Color::Yellow)
};
cells.push(maybe_highlight(
Cell::from(notes_text).fg(notes_color),
highlight,
));
}
for b in &app.branches {
let cell = match &mr.status {
MrStatus::Loading => Cell::from("⏳ LOADING...").yellow(),
MrStatus::Error => Cell::from("❌ FAILED").red(),
MrStatus::MergedIn(set) => {
if set.contains(b) {
Cell::from("🟢 PRESENT").green()
} else {
Cell::from("🔴 ABSENT").red()
}
}
};
cells.push(maybe_highlight(cell, highlight));
}
Row::new(cells)
})
.collect();
let mut constraints = vec![
Constraint::Length(8), Constraint::Fill(3), Constraint::Length(BADGE_WIDTH as u16 + 2), ];
if cols.activity {
constraints.push(Constraint::Length(12)); }
if cols.target_branch {
constraints.push(Constraint::Fill(2)); }
if cols.labels {
constraints.push(Constraint::Fill(2)); }
if cols.milestone {
constraints.push(Constraint::Fill(2)); }
if cols.notes {
constraints.push(Constraint::Length(10)); }
for _ in &app.branches {
constraints.push(Constraint::Fill(1));
}
let mins = app.time_left / 60;
let secs = app.time_left % 60;
let sort_label = match app.sort_column {
SortColumn::UpdatedAt => "Updated ↕",
SortColumn::Id => "ID ↕",
SortColumn::Milestone => "Milestone ↕",
SortColumn::Title => "Title ↕",
};
let order_label = match app.sort_order {
SortOrder::Ascending => "↑",
SortOrder::Descending => "↓",
};
let filter_label = app.filter_mode.label();
let title_text = format!(
" GitLab MR Tracker ({}) │ 🔄 Next refresh: {:02}:{:02} │ Sort: {} {} │ Filter: {} ",
app.base_url, mins, secs, sort_label, order_label, filter_label
);
Table::new(rows, constraints)
.header(header)
.row_highlight_style(
Style::default()
.bg(Color::DarkGray)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("> ")
.block(
ratatui::widgets::Block::default()
.borders(ratatui::widgets::Borders::ALL)
.title(title_text),
)
}