1use crate::common::render_vertical_scrollbar;
2use ratatui::prelude::*;
3use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType, Paragraph};
4
5pub trait MonitoringState {
7 fn is_metrics_loading(&self) -> bool;
8 fn set_metrics_loading(&mut self, loading: bool);
9 fn monitoring_scroll(&self) -> usize;
10 fn set_monitoring_scroll(&mut self, scroll: usize);
11 fn clear_metrics(&mut self);
12}
13
14pub struct MetricChart<'a> {
15 pub title: &'a str,
16 pub data: &'a [(i64, f64)],
17 pub y_axis_label: &'a str,
18 pub x_axis_label: Option<String>,
19}
20
21pub struct MultiDatasetChart<'a> {
22 pub title: &'a str,
23 pub datasets: Vec<(&'a str, &'a [(i64, f64)])>,
24 pub y_axis_label: &'a str,
25 pub y_axis_step: u32,
26 pub x_axis_label: Option<String>,
27}
28
29pub struct DualAxisChart<'a> {
30 pub title: &'a str,
31 pub left_dataset: (&'a str, &'a [(i64, f64)]),
32 pub right_dataset: (&'a str, &'a [(i64, f64)]),
33 pub left_y_label: &'a str,
34 pub right_y_label: &'a str,
35 pub x_axis_label: Option<String>,
36}
37
38pub fn render_monitoring_tab(
39 frame: &mut Frame,
40 area: Rect,
41 single_charts: &[MetricChart],
42 multi_charts: &[MultiDatasetChart],
43 dual_charts: &[DualAxisChart],
44 trailing_single_charts: &[MetricChart],
45 scroll_position: usize,
46) {
47 let available_height = area.height as usize;
48 let total_charts =
49 single_charts.len() + multi_charts.len() + dual_charts.len() + trailing_single_charts.len();
50
51 let mut y_offset = 0;
52 let mut chart_idx = 0;
53
54 for chart in single_charts.iter() {
55 if chart_idx < scroll_position {
56 chart_idx += 1;
57 continue;
58 }
59 if y_offset + 20 > available_height {
60 break;
61 }
62 let chart_height = 20.min((available_height - y_offset) as u16);
63 let chart_rect = Rect {
64 x: area.x,
65 y: area.y + y_offset as u16,
66 width: area.width.saturating_sub(1),
67 height: chart_height,
68 };
69 render_chart(frame, chart, chart_rect);
70 y_offset += 20;
71 chart_idx += 1;
72 }
73
74 for chart in multi_charts.iter() {
75 if chart_idx < scroll_position {
76 chart_idx += 1;
77 continue;
78 }
79 if y_offset + 20 > available_height {
80 break;
81 }
82 let chart_height = 20.min((available_height - y_offset) as u16);
83 let chart_rect = Rect {
84 x: area.x,
85 y: area.y + y_offset as u16,
86 width: area.width.saturating_sub(1),
87 height: chart_height,
88 };
89 render_multi_dataset_chart(frame, chart, chart_rect);
90 y_offset += 20;
91 chart_idx += 1;
92 }
93
94 for chart in dual_charts.iter() {
95 if chart_idx < scroll_position {
96 chart_idx += 1;
97 continue;
98 }
99 if y_offset + 20 > available_height {
100 break;
101 }
102 let chart_height = 20.min((available_height - y_offset) as u16);
103 let chart_rect = Rect {
104 x: area.x,
105 y: area.y + y_offset as u16,
106 width: area.width.saturating_sub(1),
107 height: chart_height,
108 };
109 render_dual_axis_chart(frame, chart, chart_rect);
110 y_offset += 20;
111 chart_idx += 1;
112 }
113
114 for chart in trailing_single_charts.iter() {
115 if chart_idx < scroll_position {
116 chart_idx += 1;
117 continue;
118 }
119 if y_offset + 20 > available_height {
120 break;
121 }
122 let chart_height = 20.min((available_height - y_offset) as u16);
123 let chart_rect = Rect {
124 x: area.x,
125 y: area.y + y_offset as u16,
126 width: area.width.saturating_sub(1),
127 height: chart_height,
128 };
129 render_chart(frame, chart, chart_rect);
130 y_offset += 20;
131 chart_idx += 1;
132 }
133
134 let total_height = total_charts * 20;
135 let scroll_offset = scroll_position * 20;
136 render_vertical_scrollbar(frame, area, total_height, scroll_offset);
137}
138
139fn render_chart(frame: &mut Frame, chart: &MetricChart, area: Rect) {
140 let block = Block::default()
141 .title(format!(" {} ", chart.title))
142 .borders(Borders::ALL)
143 .border_type(BorderType::Rounded)
144 .border_style(Style::default().fg(Color::Gray));
145
146 if chart.data.is_empty() {
147 let inner = block.inner(area);
148 frame.render_widget(block, area);
149 let paragraph = Paragraph::new("--");
150 frame.render_widget(paragraph, inner);
151 return;
152 }
153
154 let data: Vec<(f64, f64)> = chart
155 .data
156 .iter()
157 .map(|(timestamp, value)| (*timestamp as f64, *value))
158 .collect();
159
160 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
161 let max_x = data
162 .iter()
163 .map(|(x, _)| *x)
164 .fold(f64::NEG_INFINITY, f64::max);
165 let max_y = data
166 .iter()
167 .map(|(_, y)| *y)
168 .fold(0.0_f64, f64::max)
169 .max(1.0);
170
171 let dataset = Dataset::default()
172 .name(chart.title)
173 .marker(symbols::Marker::Braille)
174 .graph_type(GraphType::Line)
175 .style(Style::default().fg(Color::Cyan))
176 .data(&data);
177
178 let x_labels: Vec<Span> = {
179 let mut labels = Vec::new();
180 let step = 1800;
181 let mut current = (min_x as i64 / step) * step;
182 while current <= max_x as i64 {
183 let time = chrono::DateTime::from_timestamp(current, 0)
184 .unwrap_or_default()
185 .format("%H:%M")
186 .to_string();
187 labels.push(Span::raw(time));
188 current += step;
189 }
190 labels
191 };
192
193 let mut x_axis = Axis::default()
194 .style(Style::default().fg(Color::Gray))
195 .bounds([min_x, max_x])
196 .labels(x_labels);
197
198 if let Some(label) = &chart.x_axis_label {
199 x_axis = x_axis.title(label.as_str());
200 }
201
202 let y_labels: Vec<Span> = {
203 let mut labels = Vec::new();
204 let mut current = 0.0;
205 let max = max_y * 1.1;
206 let step = if max <= 10.0 {
207 0.5
208 } else {
209 (max / 10.0).ceil()
210 };
211 while current <= max {
212 labels.push(Span::raw(format!("{:.1}", current)));
213 current += step;
214 }
215 labels
216 };
217
218 let y_axis = Axis::default()
219 .title(chart.y_axis_label)
220 .style(Style::default().fg(Color::Gray))
221 .bounds([0.0, max_y * 1.1])
222 .labels(y_labels);
223
224 let chart_widget = Chart::new(vec![dataset])
225 .block(block)
226 .x_axis(x_axis)
227 .y_axis(y_axis);
228
229 frame.render_widget(chart_widget, area);
230}
231
232fn render_multi_dataset_chart(frame: &mut Frame, chart: &MultiDatasetChart, area: Rect) {
233 let block = Block::default()
234 .title(format!(" {} ", chart.title))
235 .borders(Borders::ALL)
236 .border_type(BorderType::Rounded)
237 .border_style(Style::default().fg(Color::Gray));
238
239 let all_empty = chart.datasets.iter().all(|(_, data)| data.is_empty());
240 if all_empty {
241 let inner = block.inner(area);
242 frame.render_widget(block, area);
243 let paragraph = Paragraph::new("--");
244 frame.render_widget(paragraph, inner);
245 return;
246 }
247
248 let colors = [Color::Cyan, Color::Yellow, Color::Magenta];
249 let mut converted_data: Vec<Vec<(f64, f64)>> = Vec::new();
250 let mut min_x = f64::INFINITY;
251 let mut max_x = f64::NEG_INFINITY;
252 let mut max_y = 0.0_f64;
253
254 for (_, data) in chart.datasets.iter() {
255 if data.is_empty() {
256 converted_data.push(Vec::new());
257 continue;
258 }
259 let converted: Vec<(f64, f64)> = data
260 .iter()
261 .map(|(timestamp, value)| (*timestamp as f64, *value))
262 .collect();
263
264 min_x = min_x.min(
265 converted
266 .iter()
267 .map(|(x, _)| *x)
268 .fold(f64::INFINITY, f64::min),
269 );
270 max_x = max_x.max(
271 converted
272 .iter()
273 .map(|(x, _)| *x)
274 .fold(f64::NEG_INFINITY, f64::max),
275 );
276 max_y = max_y.max(converted.iter().map(|(_, y)| *y).fold(0.0_f64, f64::max));
277
278 converted_data.push(converted);
279 }
280
281 let mut datasets_vec = Vec::new();
282 for (idx, ((name, _), data)) in chart.datasets.iter().zip(converted_data.iter()).enumerate() {
283 if data.is_empty() {
284 continue;
285 }
286 let dataset = Dataset::default()
287 .name(*name)
288 .marker(symbols::Marker::Braille)
289 .graph_type(GraphType::Line)
290 .style(Style::default().fg(colors[idx % colors.len()]))
291 .data(data);
292
293 datasets_vec.push(dataset);
294 }
295
296 max_y = max_y.max(1.0);
297
298 let x_labels: Vec<Span> = {
299 let mut labels = Vec::new();
300 let step = 1800;
301 let mut current = (min_x as i64 / step) * step;
302 while current <= max_x as i64 {
303 let time = chrono::DateTime::from_timestamp(current, 0)
304 .unwrap_or_default()
305 .format("%H:%M")
306 .to_string();
307 labels.push(Span::raw(time));
308 current += step;
309 }
310 labels
311 };
312
313 let mut x_axis = Axis::default()
314 .style(Style::default().fg(Color::Gray))
315 .bounds([min_x, max_x])
316 .labels(x_labels);
317
318 if let Some(label) = &chart.x_axis_label {
319 x_axis = x_axis.title(label.as_str());
320 }
321
322 let y_labels: Vec<Span> = {
323 let mut labels = Vec::new();
324 let mut current = 0.0;
325 let max = max_y * 1.1;
326 let step = chart.y_axis_step as f64;
327 while current <= max {
328 let label = if step >= 1000.0 {
329 format!("{}K", (current / 1000.0) as u32)
330 } else {
331 format!("{:.0}", current)
332 };
333 labels.push(Span::raw(label));
334 current += step;
335 }
336 labels
337 };
338
339 let y_axis = Axis::default()
340 .title(chart.y_axis_label)
341 .style(Style::default().fg(Color::Gray))
342 .bounds([0.0, max_y * 1.1])
343 .labels(y_labels);
344
345 let chart_widget = Chart::new(datasets_vec)
346 .block(block)
347 .x_axis(x_axis)
348 .y_axis(y_axis);
349
350 frame.render_widget(chart_widget, area);
351}
352
353fn render_dual_axis_chart(frame: &mut Frame, chart: &DualAxisChart, area: Rect) {
354 let block = Block::default()
355 .title(format!(" {} ", chart.title))
356 .borders(Borders::ALL)
357 .border_type(BorderType::Rounded)
358 .border_style(Style::default().fg(Color::Gray));
359
360 let (left_name, left_data) = chart.left_dataset;
361 let (right_name, right_data) = chart.right_dataset;
362
363 if left_data.is_empty() && right_data.is_empty() {
364 let inner = block.inner(area);
365 frame.render_widget(block, area);
366 let paragraph = Paragraph::new("--");
367 frame.render_widget(paragraph, inner);
368 return;
369 }
370
371 let left_converted: Vec<(f64, f64)> = left_data
372 .iter()
373 .map(|(timestamp, value)| (*timestamp as f64, *value))
374 .collect();
375
376 let right_converted: Vec<(f64, f64)> = right_data
377 .iter()
378 .map(|(timestamp, value)| (*timestamp as f64, *value))
379 .collect();
380
381 let mut min_x = f64::INFINITY;
382 let mut max_x = f64::NEG_INFINITY;
383 let mut max_left_y = 0.0_f64;
384 let max_right_y = 100.0;
385
386 if !left_converted.is_empty() {
387 min_x = min_x.min(
388 left_converted
389 .iter()
390 .map(|(x, _)| *x)
391 .fold(f64::INFINITY, f64::min),
392 );
393 max_x = max_x.max(
394 left_converted
395 .iter()
396 .map(|(x, _)| *x)
397 .fold(f64::NEG_INFINITY, f64::max),
398 );
399 max_left_y = left_converted
400 .iter()
401 .map(|(_, y)| *y)
402 .fold(0.0_f64, f64::max);
403 }
404
405 if !right_converted.is_empty() {
406 min_x = min_x.min(
407 right_converted
408 .iter()
409 .map(|(x, _)| *x)
410 .fold(f64::INFINITY, f64::min),
411 );
412 max_x = max_x.max(
413 right_converted
414 .iter()
415 .map(|(x, _)| *x)
416 .fold(f64::NEG_INFINITY, f64::max),
417 );
418 }
419
420 max_left_y = max_left_y.max(1.0);
421
422 let normalized_right: Vec<(f64, f64)> = right_converted
423 .iter()
424 .map(|(x, y)| (*x, y * max_left_y / max_right_y))
425 .collect();
426
427 let left_dataset = Dataset::default()
428 .name(left_name)
429 .marker(symbols::Marker::Braille)
430 .graph_type(GraphType::Line)
431 .style(Style::default().fg(Color::Red))
432 .data(&left_converted);
433
434 let right_dataset = Dataset::default()
435 .name(right_name)
436 .marker(symbols::Marker::Braille)
437 .graph_type(GraphType::Line)
438 .style(Style::default().fg(Color::Green))
439 .data(&normalized_right);
440
441 let x_labels: Vec<Span> = {
442 let mut labels = Vec::new();
443 let step = 1800;
444 let mut current = (min_x as i64 / step) * step;
445 while current <= max_x as i64 {
446 let time = chrono::DateTime::from_timestamp(current, 0)
447 .unwrap_or_default()
448 .format("%H:%M")
449 .to_string();
450 labels.push(Span::raw(time));
451 current += step;
452 }
453 labels
454 };
455
456 let mut x_axis = Axis::default()
457 .style(Style::default().fg(Color::Gray))
458 .bounds([min_x, max_x])
459 .labels(x_labels);
460
461 if let Some(label) = &chart.x_axis_label {
462 x_axis = x_axis.title(label.as_str());
463 }
464
465 let y_labels: Vec<Span> = {
466 let mut labels = Vec::new();
467 let mut current = 0.0;
468 let max = max_left_y * 1.1;
469 let step = if max <= 10.0 {
470 0.5
471 } else {
472 (max / 10.0).ceil()
473 };
474 while current <= max {
475 labels.push(Span::raw(format!("{:.0}", current)));
476 current += step;
477 }
478 labels
479 };
480
481 let y_axis = Axis::default()
482 .title(chart.left_y_label)
483 .style(Style::default().fg(Color::Gray))
484 .bounds([0.0, max_left_y * 1.1])
485 .labels(y_labels);
486
487 let chart_widget = Chart::new(vec![left_dataset, right_dataset])
488 .block(block)
489 .x_axis(x_axis)
490 .y_axis(y_axis);
491
492 frame.render_widget(chart_widget, area);
493}
494
495#[cfg(test)]
496mod tests {
497 use super::*;
498
499 #[test]
500 fn test_metric_chart_creation() {
501 let data = vec![(1700000000, 5.0), (1700000060, 10.0)];
502 let chart = MetricChart {
503 title: "Test Metric",
504 data: &data,
505 y_axis_label: "Count",
506 x_axis_label: None,
507 };
508 assert_eq!(chart.title, "Test Metric");
509 assert_eq!(chart.data.len(), 2);
510 assert_eq!(chart.y_axis_label, "Count");
511 assert_eq!(chart.x_axis_label, None);
512 }
513
514 #[test]
515 fn test_empty_chart_data() {
516 let data: Vec<(i64, f64)> = vec![];
517 let chart = MetricChart {
518 title: "Empty Chart",
519 data: &data,
520 y_axis_label: "Value",
521 x_axis_label: None,
522 };
523 assert!(chart.data.is_empty());
524 }
525
526 #[test]
527 fn test_metric_chart_with_x_axis_label() {
528 let data = vec![(1700000000, 5.0), (1700000060, 10.0)];
529 let chart = MetricChart {
530 title: "Invocations",
531 data: &data,
532 y_axis_label: "Count",
533 x_axis_label: Some("Invocations [sum: 15]".to_string()),
534 };
535 assert_eq!(
536 chart.x_axis_label,
537 Some("Invocations [sum: 15]".to_string())
538 );
539 }
540
541 #[test]
542 fn test_multi_dataset_chart_creation() {
543 let min_data = vec![(1700000000, 100.0), (1700000060, 150.0)];
544 let avg_data = vec![(1700000000, 200.0), (1700000060, 250.0)];
545 let max_data = vec![(1700000000, 300.0), (1700000060, 350.0)];
546
547 let chart = MultiDatasetChart {
548 title: "Duration",
549 datasets: vec![
550 ("Minimum", &min_data),
551 ("Average", &avg_data),
552 ("Maximum", &max_data),
553 ],
554 y_axis_label: "Milliseconds",
555 y_axis_step: 1000,
556 x_axis_label: Some("Minimum [100], Average [200], Maximum [300]".to_string()),
557 };
558
559 assert_eq!(chart.title, "Duration");
560 assert_eq!(chart.datasets.len(), 3);
561 assert_eq!(chart.y_axis_label, "Milliseconds");
562 assert_eq!(chart.y_axis_step, 1000);
563 }
564
565 #[test]
566 fn test_multi_dataset_chart_empty() {
567 let empty: Vec<(i64, f64)> = vec![];
568 let chart = MultiDatasetChart {
569 title: "Empty Duration",
570 datasets: vec![
571 ("Minimum", &empty),
572 ("Average", &empty),
573 ("Maximum", &empty),
574 ],
575 y_axis_label: "Milliseconds",
576 y_axis_step: 1000,
577 x_axis_label: None,
578 };
579
580 assert!(chart.datasets.iter().all(|(_, data)| data.is_empty()));
581 }
582
583 #[test]
584 fn test_duration_label_format() {
585 let min = 100.0;
586 let avg = 200.5;
587 let max = 350.0;
588 let label = format!(
589 "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
590 min, avg, max
591 );
592 assert_eq!(label, "Minimum [100], Average [200], Maximum [350]");
593 }
594
595 #[test]
596 fn test_y_axis_label_formatting_1k() {
597 let value = 1000.0;
598 let label = format!("{}K", (value / 1000.0) as u32);
599 assert_eq!(label, "1K");
600 }
601
602 #[test]
603 fn test_y_axis_label_formatting_5k() {
604 let value = 5000.0;
605 let label = format!("{}K", (value / 1000.0) as u32);
606 assert_eq!(label, "5K");
607 }
608
609 #[test]
610 fn test_y_axis_step_1000() {
611 let step = 1000;
612 assert_eq!(step, 1000);
613 let values = [0, 1000, 2000, 3000, 4000, 5000];
614 for (i, val) in values.iter().enumerate() {
615 assert_eq!(*val, i * step);
616 }
617 }
618
619 #[test]
620 fn test_duration_min_calculation() {
621 let data = [(1700000000, 100.0), (1700000060, 50.0), (1700000120, 75.0)];
622 let min: f64 = data
623 .iter()
624 .map(|(_, v)| v)
625 .fold(f64::INFINITY, |a, &b| a.min(b));
626 assert_eq!(min, 50.0);
627 }
628
629 #[test]
630 fn test_duration_avg_calculation() {
631 let data = [
632 (1700000000, 100.0),
633 (1700000060, 200.0),
634 (1700000120, 300.0),
635 ];
636 let avg: f64 = data.iter().map(|(_, v)| v).sum::<f64>() / data.len() as f64;
637 assert_eq!(avg, 200.0);
638 }
639
640 #[test]
641 fn test_duration_max_calculation() {
642 let data = [
643 (1700000000, 100.0),
644 (1700000060, 350.0),
645 (1700000120, 200.0),
646 ];
647 let max: f64 = data
648 .iter()
649 .map(|(_, v)| v)
650 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
651 assert_eq!(max, 350.0);
652 }
653
654 #[test]
655 fn test_duration_empty_data_min() {
656 let data: Vec<(i64, f64)> = vec![];
657 let min: f64 = data
658 .iter()
659 .map(|(_, v)| v)
660 .fold(f64::INFINITY, |a, &b| a.min(b));
661 assert!(min.is_infinite() && min.is_sign_positive());
662 }
663
664 #[test]
665 fn test_duration_empty_data_avg() {
666 let data: Vec<(i64, f64)> = vec![];
667 let avg: f64 = if !data.is_empty() {
668 data.iter().map(|(_, v)| v).sum::<f64>() / data.len() as f64
669 } else {
670 0.0
671 };
672 assert_eq!(avg, 0.0);
673 }
674
675 #[test]
676 fn test_dual_axis_chart_creation() {
677 let errors = vec![(1700000000, 5.0), (1700000060, 10.0)];
678 let success_rate = vec![(1700000000, 95.0), (1700000060, 90.0)];
679
680 let chart = DualAxisChart {
681 title: "Error count and success rate",
682 left_dataset: ("Errors", &errors),
683 right_dataset: ("Success rate", &success_rate),
684 left_y_label: "Count",
685 right_y_label: "%",
686 x_axis_label: Some("Errors [max: 10] and Success rate [min: 90%]".to_string()),
687 };
688
689 assert_eq!(chart.title, "Error count and success rate");
690 assert_eq!(chart.left_y_label, "Count");
691 assert_eq!(chart.right_y_label, "%");
692 }
693
694 #[test]
695 fn test_dual_axis_normalization() {
696 let max_left_y = 10.0;
697 let max_right_y = 100.0;
698 let right_value = 95.0;
699 let normalized = right_value * max_left_y / max_right_y;
700 assert_eq!(normalized, 9.5);
701 }
702}