| 15 |
15 |
|
|
| 16 |
16 |
|
use chrono::{DateTime, Utc};
|
| 17 |
17 |
|
use makeover_tui::Theme;
|
|
18 |
+ |
use makeover_tui::makeover_layout::{Column, Priority, Width};
|
|
19 |
+ |
use makeover_tui::table::{self, Cell as TableCell, Sizing, TableStyle};
|
| 18 |
20 |
|
use ops_status::{Method, Node};
|
| 19 |
21 |
|
use ratatui::Frame;
|
| 20 |
22 |
|
use ratatui::layout::{Constraint, Layout, Rect};
|
| 21 |
23 |
|
use ratatui::style::{Modifier, Style};
|
| 22 |
24 |
|
use ratatui::text::{Line, Span};
|
| 23 |
|
- |
use ratatui::widgets::{Block, Cell, Clear, List, ListItem, Paragraph, Row, Table, Tabs};
|
|
25 |
+ |
use ratatui::widgets::{Block, Clear, List, ListItem, Paragraph, TableState, Tabs};
|
| 24 |
26 |
|
|
| 25 |
27 |
|
use crate::model::{Model, Prompt, SourceState, Tab};
|
| 26 |
28 |
|
use crate::value;
|
| 128 |
130 |
|
// Rollup
|
| 129 |
131 |
|
// ---------------------------------------------------------------------------
|
| 130 |
132 |
|
|
|
133 |
+ |
/// The rollup's columns, left to right.
|
|
134 |
+ |
///
|
|
135 |
+ |
/// The mark's name is empty because the name is what the header row draws, and
|
|
136 |
+ |
/// this column's header always was blank: the glyph says what it is.
|
|
137 |
+ |
///
|
|
138 |
+ |
/// Nothing here is `Optional`. A rollup with the status or the source name
|
|
139 |
+ |
/// dropped is not a narrower rollup, it is a different screen, and the age is
|
|
140 |
+ |
/// what turns "FAIL" into "FAIL, and it has been that way for two days". Detail
|
|
141 |
+ |
/// absorbs what is left, which is what the old `Min(10)` was saying.
|
|
142 |
+ |
const ROLLUP_COLUMNS: [Column<'static>; 4] = [
|
|
143 |
+ |
Column {
|
|
144 |
+ |
name: "",
|
|
145 |
+ |
width: Width::Fixed,
|
|
146 |
+ |
priority: Priority::Essential,
|
|
147 |
+ |
sortable: false,
|
|
148 |
+ |
sorted: None,
|
|
149 |
+ |
},
|
|
150 |
+ |
Column {
|
|
151 |
+ |
name: "source",
|
|
152 |
+ |
width: Width::Content,
|
|
153 |
+ |
priority: Priority::Essential,
|
|
154 |
+ |
sortable: false,
|
|
155 |
+ |
sorted: None,
|
|
156 |
+ |
},
|
|
157 |
+ |
Column {
|
|
158 |
+ |
name: "age",
|
|
159 |
+ |
width: Width::Content,
|
|
160 |
+ |
priority: Priority::Secondary,
|
|
161 |
+ |
sortable: false,
|
|
162 |
+ |
sorted: None,
|
|
163 |
+ |
},
|
|
164 |
+ |
Column {
|
|
165 |
+ |
name: "detail",
|
|
166 |
+ |
width: Width::Fill,
|
|
167 |
+ |
priority: Priority::Essential,
|
|
168 |
+ |
sortable: false,
|
|
169 |
+ |
sorted: None,
|
|
170 |
+ |
},
|
|
171 |
+ |
];
|
|
172 |
+ |
|
|
173 |
+ |
/// The tracks the hand-written `Constraint`s carried, lifted rather than
|
|
174 |
+ |
/// re-chosen. The two `Width::Content` columns measure themselves from the
|
|
175 |
+ |
/// cells and use these only as a floor, so a run of short source names stops
|
|
176 |
+ |
/// spending fourteen columns to say `pom`.
|
|
177 |
+ |
const ROLLUP_SIZING: Sizing<'static> = Sizing {
|
|
178 |
+ |
lengths: &[("", 4), ("source", 14), ("age", 8), ("detail", 10)],
|
|
179 |
+ |
fallback: 8,
|
|
180 |
+ |
};
|
|
181 |
+ |
|
| 131 |
182 |
|
/// Every source at once, worst first.
|
| 132 |
183 |
|
///
|
| 133 |
184 |
|
/// Without this the viewer is N tabs you still have to visit one at a time,
|
| 134 |
185 |
|
/// which is the situation it replaces, with extra steps.
|
| 135 |
186 |
|
fn render_rollup(model: &Model, theme: &Theme, now: DateTime<Utc>, frame: &mut Frame, area: Rect) {
|
| 136 |
187 |
|
let order = model.rollup_order(now);
|
| 137 |
|
- |
let rows: Vec<Row> = order
|
|
188 |
+ |
let rows: Vec<Vec<TableCell>> = order
|
| 138 |
189 |
|
.iter()
|
| 139 |
|
- |
.enumerate()
|
| 140 |
|
- |
.map(|(row, &index)| {
|
|
190 |
+ |
.map(|&index| {
|
| 141 |
191 |
|
let source = &model.sources[index];
|
| 142 |
192 |
|
let status = source.status(now);
|
| 143 |
193 |
|
let age = match source.age(now) {
|
| 144 |
194 |
|
Some(age) => value::duration(age.num_seconds()),
|
| 145 |
195 |
|
None => "-".into(),
|
| 146 |
196 |
|
};
|
| 147 |
|
- |
let style = if row == model.rollup_selected {
|
| 148 |
|
- |
selected(theme)
|
| 149 |
|
- |
} else {
|
| 150 |
|
- |
Style::default().fg(theme.content_primary)
|
| 151 |
|
- |
};
|
| 152 |
|
- |
Row::new(vec![
|
| 153 |
|
- |
Cell::from(value::status_mark(status)).style(value::status_style(theme, status)),
|
| 154 |
|
- |
Cell::from(source.name.clone()),
|
| 155 |
|
- |
Cell::from(age).style(muted(theme)),
|
| 156 |
|
- |
Cell::from(source.summary(now)),
|
| 157 |
|
- |
])
|
| 158 |
|
- |
.style(style)
|
|
197 |
+ |
vec![
|
|
198 |
+ |
// The mark styles its own span rather than the cell: a status
|
|
199 |
+ |
// colour is this app's, not a part the table module knows, and
|
|
200 |
+ |
// a span's style sits on top of the cell's.
|
|
201 |
+ |
TableCell::new(
|
|
202 |
+ |
"",
|
|
203 |
+ |
Span::styled(
|
|
204 |
+ |
value::status_mark(status),
|
|
205 |
+ |
value::status_style(theme, status),
|
|
206 |
+ |
),
|
|
207 |
+ |
),
|
|
208 |
+ |
TableCell::new("source", source.name.clone()),
|
|
209 |
+ |
TableCell::new("age", Span::styled(age, muted(theme))),
|
|
210 |
+ |
TableCell::new("detail", source.summary(now)),
|
|
211 |
+ |
]
|
| 159 |
212 |
|
})
|
| 160 |
213 |
|
.collect();
|
| 161 |
214 |
|
|
| 162 |
|
- |
let table = Table::new(
|
| 163 |
|
- |
rows,
|
| 164 |
|
- |
[
|
| 165 |
|
- |
Constraint::Length(4),
|
| 166 |
|
- |
Constraint::Length(14),
|
| 167 |
|
- |
Constraint::Length(8),
|
| 168 |
|
- |
Constraint::Min(10),
|
| 169 |
|
- |
],
|
|
215 |
+ |
// The block first, because narrowing is measured against the width the
|
|
216 |
+ |
// table actually gets rather than the width of the area around it. Two
|
|
217 |
+ |
// columns of border is the difference between "detail fits" and "detail
|
|
218 |
+ |
// is cut", which is exactly the decision the cutoff is making.
|
|
219 |
+ |
let block = container(theme, " all sources ");
|
|
220 |
+ |
let inner = block.inner(area);
|
|
221 |
+ |
let table = table::table(
|
|
222 |
+ |
&ROLLUP_COLUMNS,
|
|
223 |
+ |
&rows,
|
|
224 |
+ |
&ROLLUP_SIZING,
|
|
225 |
+ |
&TableStyle::from_theme(theme),
|
|
226 |
+ |
inner.width,
|
| 170 |
227 |
|
)
|
| 171 |
|
- |
.header(Row::new(vec!["", "source", "age", "detail"]).style(muted(theme)))
|
| 172 |
|
- |
.block(container(theme, " all sources "));
|
|
228 |
+ |
.block(block);
|
| 173 |
229 |
|
|
| 174 |
|
- |
frame.render_widget(table, area);
|
|
230 |
+ |
// Selection through the widget's own highlight rather than a per-row style.
|
|
231 |
+ |
// `TableStyle::from_theme` carries it on the background alone, which is what
|
|
232 |
+ |
// leaves a FAIL row's danger colour on top of it -- the same reason the
|
|
233 |
+ |
// local `selected` helper gives a surface instead of reversing.
|
|
234 |
+ |
let mut state = TableState::default().with_selected(Some(model.rollup_selected));
|
|
235 |
+ |
frame.render_stateful_widget(table, area, &mut state);
|
| 175 |
236 |
|
}
|
| 176 |
237 |
|
|
| 177 |
238 |
|
// ---------------------------------------------------------------------------
|
| 628 |
689 |
|
assert!(text.contains("degr"), "stale-but-green is not ok:\n{text}");
|
| 629 |
690 |
|
}
|
| 630 |
691 |
|
|
|
692 |
+ |
#[test]
|
|
693 |
+ |
fn a_narrow_rollup_drops_the_age_before_it_drops_the_detail() {
|
|
694 |
+ |
// What the hand-written `Constraint`s could not do: at 80 columns every
|
|
695 |
+ |
// column is drawn, and at a width where they no longer all fit the
|
|
696 |
+ |
// priority decides which one goes rather than the order they were
|
|
697 |
+ |
// written in. Age is the only Secondary column, so it is the only one
|
|
698 |
+ |
// that can go.
|
|
699 |
+ |
let model = Model::new(vec![source(
|
|
700 |
+ |
"pom",
|
|
701 |
+ |
now() - TimeDelta::hours(4),
|
|
702 |
+ |
vec![node("backup", "backup", Status::Ok)],
|
|
703 |
+ |
)]);
|
|
704 |
+ |
|
|
705 |
+ |
let wide = joined(&draw(&model, now(), 80, 12));
|
|
706 |
+ |
assert!(wide.contains("age"), "the age column at 80 wide:\n{wide}");
|
|
707 |
+ |
|
|
708 |
+ |
let narrow = joined(&draw(&model, now(), 24, 12));
|
|
709 |
+ |
assert!(!narrow.contains("age"), "age must drop first:\n{narrow}");
|
|
710 |
+ |
assert!(narrow.contains("pom"), "the source stays:\n{narrow}");
|
|
711 |
+ |
assert!(narrow.contains("detail"), "the detail stays:\n{narrow}");
|
|
712 |
+ |
}
|
|
713 |
+ |
|
| 631 |
714 |
|
#[test]
|
| 632 |
715 |
|
fn a_source_tab_lists_its_nodes_with_children_indented() {
|
| 633 |
716 |
|
let mut parent = node("tier:b", "b (prod-1)", Status::Ok);
|