1use std::cmp::Ordering;
10
11use crate::render::{Cell, Filter, Row, SortSpec};
12
13pub fn cell_text(cell: &Cell) -> String {
15 match cell {
16 Cell::Text { value } => value.clone(),
17 Cell::Number { value } => value.to_string(),
18 Cell::Timestamp { millis } => millis.to_string(),
19 Cell::Status { label_key, .. } => label_key.clone(),
20 Cell::Redacted => String::new(),
21 }
22}
23
24pub fn cmp_cells(a: &Cell, b: &Cell) -> Ordering {
27 match (a, b) {
28 (Cell::Number { value: x }, Cell::Number { value: y }) => x.cmp(y),
29 (Cell::Timestamp { millis: x }, Cell::Timestamp { millis: y }) => x.cmp(y),
30 _ => cell_text(a).cmp(&cell_text(b)),
31 }
32}
33
34pub fn filter_rows(rows: Vec<Row>, filter: Option<&Filter>) -> Vec<Row> {
39 let Some(filter) = filter else {
40 return rows;
41 };
42 let needle = filter.expression.trim().to_ascii_lowercase();
43 if needle.is_empty() {
44 return rows;
45 }
46 rows.into_iter()
47 .filter(|row| {
48 row.cells
49 .iter()
50 .any(|c| cell_text(c).to_ascii_lowercase().contains(&needle))
51 })
52 .collect()
53}
54
55pub fn sort_rows(rows: &mut [Row], column_ids: &[String], sort: Option<&SortSpec>) {
60 let Some(sort) = sort else {
61 return;
62 };
63 let Some(idx) = column_ids.iter().position(|id| id == &sort.column) else {
64 return;
65 };
66 rows.sort_by(|a, b| {
67 let ord = match (a.cells.get(idx), b.cells.get(idx)) {
68 (Some(x), Some(y)) => cmp_cells(x, y),
69 (Some(_), None) => Ordering::Greater,
70 (None, Some(_)) => Ordering::Less,
71 (None, None) => Ordering::Equal,
72 };
73 if sort.descending { ord.reverse() } else { ord }
74 });
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::render::{RowId, StatusLevel};
81
82 fn text(v: &str) -> Cell {
83 Cell::Text { value: v.into() }
84 }
85 fn num(v: i64) -> Cell {
86 Cell::Number { value: v }
87 }
88 fn row(id: &str, cells: Vec<Cell>) -> Row {
89 Row {
90 id: RowId(id.into()),
91 cells,
92 }
93 }
94
95 #[test]
96 fn filter_matches_substring_across_cells() {
97 let rows = vec![
98 row("a", vec![text("zebra")]),
99 row("b", vec![text("apple"), text("ns-prod")]),
100 row("c", vec![text("banana")]),
101 ];
102 let f = Filter {
103 expression: "prod".into(),
104 };
105 let out = filter_rows(rows, Some(&f));
106 assert_eq!(out.len(), 1);
107 assert_eq!(out[0].id, RowId("b".into()));
108 }
109
110 #[test]
111 fn filter_none_keeps_all() {
112 let rows = vec![row("a", vec![text("x")]), row("b", vec![text("y")])];
113 assert_eq!(filter_rows(rows.clone(), None), rows);
114 }
115
116 #[test]
117 fn sort_by_numeric_column() {
118 let ids = vec!["name".into(), "count".into()];
119 let mut rows = vec![
120 row("a", vec![text("a"), num(10)]),
121 row("b", vec![text("b"), num(2)]),
122 row("c", vec![text("c"), num(9)]),
123 ];
124 sort_rows(
125 &mut rows,
126 &ids,
127 Some(&SortSpec {
128 column: "count".into(),
129 descending: false,
130 }),
131 );
132 assert_eq!(rows[0].id, RowId("b".into()));
133 assert_eq!(rows[1].id, RowId("c".into()));
134 assert_eq!(rows[2].id, RowId("a".into()));
135 }
136
137 #[test]
138 fn sort_descending_reverses() {
139 let ids = vec!["name".into()];
140 let mut rows = vec![row("a", vec![text("a")]), row("b", vec![text("b")])];
141 sort_rows(
142 &mut rows,
143 &ids,
144 Some(&SortSpec {
145 column: "name".into(),
146 descending: true,
147 }),
148 );
149 assert_eq!(rows[0].id, RowId("b".into()));
150 }
151
152 #[test]
153 fn sort_unknown_column_is_stable_noop() {
154 let ids = vec!["name".into()];
155 let mut rows = vec![row("a", vec![text("a")]), row("b", vec![text("b")])];
156 sort_rows(
157 &mut rows,
158 &ids,
159 Some(&SortSpec {
160 column: "nope".into(),
161 descending: false,
162 }),
163 );
164 assert_eq!(rows[0].id, RowId("a".into()));
165 }
166
167 #[test]
168 fn status_cell_has_label_text() {
169 let cell = Cell::Status {
170 level: StatusLevel::Warning,
171 label_key: "status.running".into(),
172 };
173 assert_eq!(cell_text(&cell), "status.running");
174 }
175}