envision/component/alert_panel/mod.rs
1//! A threshold-based alert panel for metric monitoring.
2//!
3//! [`AlertPanel`] displays a grid of metrics, each with configurable
4//! thresholds that determine alert states (OK, Warning, Critical, Unknown).
5//! Each metric card shows a state indicator, current value with units, and
6//! an optional sparkline history. The title bar summarizes aggregate counts.
7//!
8//! State is stored in [`AlertPanelState`], updated via [`AlertPanelMessage`],
9//! and produces [`AlertPanelOutput`].
10//!
11//!
12//! # Example
13//!
14//! ```rust
15//! use envision::component::{
16//! AlertPanel, AlertPanelState, AlertMetric, AlertThreshold, AlertState,
17//! Component,
18//! };
19//!
20//! let metrics = vec![
21//! AlertMetric::new("cpu", "CPU Usage", AlertThreshold::new(70.0, 90.0))
22//! .with_units("%")
23//! .with_value(45.0),
24//! AlertMetric::new("mem", "Memory", AlertThreshold::new(80.0, 95.0))
25//! .with_units("%")
26//! .with_value(82.0),
27//! ];
28//!
29//! let state = AlertPanelState::new()
30//! .with_metrics(metrics)
31//! .with_columns(2);
32//!
33//! assert_eq!(state.metrics().len(), 2);
34//! assert_eq!(state.ok_count(), 1);
35//! assert_eq!(state.warning_count(), 1);
36//! ```
37
38mod metric;
39mod render;
40
41pub use metric::{AlertMetric, AlertState, AlertThreshold};
42
43use std::marker::PhantomData;
44
45use super::{Component, EventContext, RenderContext};
46use crate::input::{Event, Key};
47
48/// Messages that can be sent to an AlertPanel.
49///
50/// # Example
51///
52/// ```rust
53/// use envision::component::{
54/// AlertPanel, AlertPanelState, AlertPanelMessage, AlertMetric, AlertThreshold,
55/// Component,
56/// };
57///
58/// let mut state = AlertPanelState::new().with_metrics(vec![
59/// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0)).with_value(50.0),
60/// ]);
61/// let output = state.update(AlertPanelMessage::UpdateMetric {
62/// id: "cpu".into(),
63/// value: 80.0,
64/// });
65/// assert!(output.is_some());
66/// ```
67#[derive(Clone, Debug, PartialEq)]
68#[cfg_attr(
69 feature = "serialization",
70 derive(serde::Serialize, serde::Deserialize)
71)]
72pub enum AlertPanelMessage {
73 /// Update a metric's value by id.
74 UpdateMetric {
75 /// The metric identifier.
76 id: String,
77 /// The new value.
78 value: f64,
79 },
80 /// Add a new metric.
81 AddMetric(AlertMetric),
82 /// Remove a metric by id.
83 RemoveMetric(String),
84 /// Replace all metrics.
85 SetMetrics(Vec<AlertMetric>),
86 /// Select the next metric.
87 SelectNext,
88 /// Select the previous metric.
89 SelectPrev,
90 /// Navigate up in the grid.
91 SelectUp,
92 /// Navigate down in the grid.
93 SelectDown,
94 /// Set the number of grid columns.
95 SetColumns(usize),
96 /// Confirm selection of the current metric.
97 Select,
98}
99
100/// Output messages from an AlertPanel.
101///
102/// # Example
103///
104/// ```rust
105/// use envision::component::{AlertPanelOutput, AlertState};
106///
107/// let output = AlertPanelOutput::StateChanged {
108/// id: "cpu".into(),
109/// old: AlertState::Ok,
110/// new_state: AlertState::Warning,
111/// };
112/// assert!(matches!(output, AlertPanelOutput::StateChanged { .. }));
113/// ```
114#[derive(Clone, Debug, PartialEq)]
115#[cfg_attr(
116 feature = "serialization",
117 derive(serde::Serialize, serde::Deserialize)
118)]
119pub enum AlertPanelOutput {
120 /// A metric changed alert state.
121 StateChanged {
122 /// The metric identifier.
123 id: String,
124 /// The previous alert state.
125 old: AlertState,
126 /// The new alert state.
127 new_state: AlertState,
128 },
129 /// A metric was selected (Enter pressed).
130 MetricSelected(String),
131}
132
133/// State for the AlertPanel component.
134///
135/// Contains the metrics, layout configuration, and navigation state.
136///
137/// # Example
138///
139/// ```rust
140/// use envision::component::{
141/// AlertPanelState, AlertMetric, AlertThreshold,
142/// };
143///
144/// let state = AlertPanelState::new()
145/// .with_metrics(vec![
146/// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0))
147/// .with_value(45.0),
148/// ])
149/// .with_columns(2)
150/// .with_title("Alerts");
151///
152/// assert_eq!(state.metrics().len(), 1);
153/// assert_eq!(state.ok_count(), 1);
154/// ```
155#[derive(Clone, Debug, PartialEq)]
156#[cfg_attr(
157 feature = "serialization",
158 derive(serde::Serialize, serde::Deserialize)
159)]
160pub struct AlertPanelState {
161 /// The alert metrics.
162 metrics: Vec<AlertMetric>,
163 /// Number of columns in the grid layout.
164 columns: usize,
165 /// Currently selected metric index.
166 selected: Option<usize>,
167 /// Optional title.
168 title: Option<String>,
169 /// Whether to show sparkline history.
170 show_sparklines: bool,
171 /// Whether to show threshold values.
172 show_thresholds: bool,
173}
174
175impl Default for AlertPanelState {
176 fn default() -> Self {
177 Self {
178 metrics: Vec::new(),
179 columns: 2,
180 selected: None,
181 title: None,
182 show_sparklines: true,
183 show_thresholds: false,
184 }
185 }
186}
187
188impl AlertPanelState {
189 /// Creates a new empty alert panel state.
190 ///
191 /// # Example
192 ///
193 /// ```rust
194 /// use envision::component::AlertPanelState;
195 ///
196 /// let state = AlertPanelState::new();
197 /// assert!(state.metrics().is_empty());
198 /// assert_eq!(state.columns(), 2);
199 /// ```
200 pub fn new() -> Self {
201 Self::default()
202 }
203
204 /// Sets the initial metrics (builder pattern).
205 ///
206 /// # Example
207 ///
208 /// ```rust
209 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
210 ///
211 /// let state = AlertPanelState::new().with_metrics(vec![
212 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0)),
213 /// ]);
214 /// assert_eq!(state.metrics().len(), 1);
215 /// ```
216 pub fn with_metrics(mut self, metrics: Vec<AlertMetric>) -> Self {
217 self.selected = if metrics.is_empty() { None } else { Some(0) };
218 self.metrics = metrics;
219 self
220 }
221
222 /// Sets the number of grid columns (builder pattern).
223 ///
224 /// # Example
225 ///
226 /// ```rust
227 /// use envision::component::AlertPanelState;
228 ///
229 /// let state = AlertPanelState::new().with_columns(3);
230 /// assert_eq!(state.columns(), 3);
231 /// ```
232 pub fn with_columns(mut self, columns: usize) -> Self {
233 self.columns = columns.max(1);
234 self
235 }
236
237 /// Sets the title (builder pattern).
238 ///
239 /// # Example
240 ///
241 /// ```rust
242 /// use envision::component::AlertPanelState;
243 ///
244 /// let state = AlertPanelState::new().with_title("System Alerts");
245 /// assert_eq!(state.title(), Some("System Alerts"));
246 /// ```
247 pub fn with_title(mut self, title: impl Into<String>) -> Self {
248 self.title = Some(title.into());
249 self
250 }
251
252 /// Sets whether to show sparklines (builder pattern).
253 ///
254 /// # Example
255 ///
256 /// ```rust
257 /// use envision::component::AlertPanelState;
258 ///
259 /// let state = AlertPanelState::new().with_show_sparklines(false);
260 /// assert!(!state.show_sparklines());
261 /// ```
262 pub fn with_show_sparklines(mut self, show: bool) -> Self {
263 self.show_sparklines = show;
264 self
265 }
266
267 /// Sets whether to show threshold values (builder pattern).
268 ///
269 /// # Example
270 ///
271 /// ```rust
272 /// use envision::component::AlertPanelState;
273 ///
274 /// let state = AlertPanelState::new().with_show_thresholds(true);
275 /// assert!(state.show_thresholds());
276 /// ```
277 pub fn with_show_thresholds(mut self, show: bool) -> Self {
278 self.show_thresholds = show;
279 self
280 }
281
282 // ---- Accessors ----
283
284 /// Returns the metrics.
285 ///
286 /// # Example
287 ///
288 /// ```rust
289 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
290 ///
291 /// let state = AlertPanelState::new().with_metrics(vec![
292 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0)),
293 /// ]);
294 /// assert_eq!(state.metrics().len(), 1);
295 /// ```
296 pub fn metrics(&self) -> &[AlertMetric] {
297 &self.metrics
298 }
299
300 /// Returns a mutable reference to the alert metrics.
301 ///
302 /// This is safe because metrics are simple data containers.
303 /// Selection state is index-based and unaffected by value mutation.
304 ///
305 /// # Example
306 ///
307 /// ```rust
308 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
309 ///
310 /// let mut state = AlertPanelState::new().with_metrics(vec![
311 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0))
312 /// .with_value(50.0),
313 /// ]);
314 /// state.metrics_mut()[0].update_value(85.0);
315 /// assert_eq!(state.metrics()[0].value(), 85.0);
316 /// ```
317 pub fn metrics_mut(&mut self) -> &mut Vec<AlertMetric> {
318 &mut self.metrics
319 }
320
321 /// Returns the number of grid columns.
322 ///
323 /// # Example
324 ///
325 /// ```rust
326 /// use envision::component::AlertPanelState;
327 ///
328 /// let state = AlertPanelState::new().with_columns(4);
329 /// assert_eq!(state.columns(), 4);
330 /// ```
331 pub fn columns(&self) -> usize {
332 self.columns
333 }
334
335 /// Returns the selected metric index.
336 ///
337 /// # Example
338 ///
339 /// ```rust
340 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
341 ///
342 /// let state = AlertPanelState::new().with_metrics(vec![
343 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0)),
344 /// ]);
345 /// assert_eq!(state.selected(), Some(0));
346 /// ```
347 pub fn selected(&self) -> Option<usize> {
348 self.selected
349 }
350
351 /// Returns the title.
352 ///
353 /// # Example
354 ///
355 /// ```rust
356 /// use envision::component::AlertPanelState;
357 ///
358 /// let state = AlertPanelState::new().with_title("Alerts");
359 /// assert_eq!(state.title(), Some("Alerts"));
360 /// ```
361 pub fn title(&self) -> Option<&str> {
362 self.title.as_deref()
363 }
364
365 /// Sets the title.
366 ///
367 /// # Example
368 ///
369 /// ```rust
370 /// use envision::component::AlertPanelState;
371 ///
372 /// let mut state = AlertPanelState::new();
373 /// state.set_title("System Alerts");
374 /// assert_eq!(state.title(), Some("System Alerts"));
375 /// ```
376 pub fn set_title(&mut self, title: impl Into<String>) {
377 self.title = Some(title.into());
378 }
379
380 /// Returns whether sparklines are shown.
381 ///
382 /// # Example
383 ///
384 /// ```rust
385 /// use envision::component::AlertPanelState;
386 ///
387 /// let state = AlertPanelState::new();
388 /// assert!(state.show_sparklines());
389 /// ```
390 pub fn show_sparklines(&self) -> bool {
391 self.show_sparklines
392 }
393
394 /// Returns whether threshold values are shown.
395 ///
396 /// # Example
397 ///
398 /// ```rust
399 /// use envision::component::AlertPanelState;
400 ///
401 /// let state = AlertPanelState::new();
402 /// assert!(!state.show_thresholds());
403 /// ```
404 pub fn show_thresholds(&self) -> bool {
405 self.show_thresholds
406 }
407
408 /// Sets whether sparklines are shown.
409 ///
410 /// # Example
411 ///
412 /// ```rust
413 /// use envision::component::AlertPanelState;
414 ///
415 /// let mut state = AlertPanelState::new();
416 /// state.set_show_sparklines(false);
417 /// assert!(!state.show_sparklines());
418 /// ```
419 pub fn set_show_sparklines(&mut self, show: bool) {
420 self.show_sparklines = show;
421 }
422
423 /// Sets whether threshold values are shown.
424 ///
425 /// # Example
426 ///
427 /// ```rust
428 /// use envision::component::AlertPanelState;
429 ///
430 /// let mut state = AlertPanelState::new();
431 /// state.set_show_thresholds(true);
432 /// assert!(state.show_thresholds());
433 /// ```
434 pub fn set_show_thresholds(&mut self, show: bool) {
435 self.show_thresholds = show;
436 }
437
438 /// Adds a metric to the panel.
439 ///
440 /// # Example
441 ///
442 /// ```rust
443 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
444 ///
445 /// let mut state = AlertPanelState::new();
446 /// state.add_metric(
447 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0))
448 /// );
449 /// assert_eq!(state.metrics().len(), 1);
450 /// assert_eq!(state.selected(), Some(0));
451 /// ```
452 pub fn add_metric(&mut self, metric: AlertMetric) {
453 self.metrics.push(metric);
454 if self.selected.is_none() {
455 self.selected = Some(0);
456 }
457 }
458
459 /// Updates a metric's value by id.
460 ///
461 /// Returns `Some((old_state, new_state))` if the alert state changed,
462 /// or `None` if the state did not change or the metric was not found.
463 ///
464 /// # Example
465 ///
466 /// ```rust
467 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold, AlertState};
468 ///
469 /// let mut state = AlertPanelState::new().with_metrics(vec![
470 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0))
471 /// .with_value(50.0),
472 /// ]);
473 /// let result = state.update_metric("cpu", 80.0);
474 /// assert_eq!(result, Some((AlertState::Ok, AlertState::Warning)));
475 /// ```
476 pub fn update_metric(&mut self, id: &str, value: f64) -> Option<(AlertState, AlertState)> {
477 if let Some(metric) = self.metrics.iter_mut().find(|m| m.id == id) {
478 let old_state = metric.state.clone();
479 metric.update_value(value);
480 let new_state = metric.state.clone();
481 if old_state != new_state {
482 Some((old_state, new_state))
483 } else {
484 None
485 }
486 } else {
487 None
488 }
489 }
490
491 /// Returns a reference to a metric by id.
492 ///
493 /// # Example
494 ///
495 /// ```rust
496 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
497 ///
498 /// let state = AlertPanelState::new().with_metrics(vec![
499 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0)),
500 /// ]);
501 /// assert!(state.metric_by_id("cpu").is_some());
502 /// assert!(state.metric_by_id("unknown").is_none());
503 /// ```
504 pub fn metric_by_id(&self, id: &str) -> Option<&AlertMetric> {
505 self.metrics.iter().find(|m| m.id == id)
506 }
507
508 /// Returns the count of metrics in OK state.
509 ///
510 /// # Example
511 ///
512 /// ```rust
513 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
514 ///
515 /// let state = AlertPanelState::new().with_metrics(vec![
516 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0))
517 /// .with_value(50.0),
518 /// AlertMetric::new("mem", "Memory", AlertThreshold::new(80.0, 95.0))
519 /// .with_value(30.0),
520 /// ]);
521 /// assert_eq!(state.ok_count(), 2);
522 /// ```
523 pub fn ok_count(&self) -> usize {
524 self.metrics
525 .iter()
526 .filter(|m| m.state == AlertState::Ok)
527 .count()
528 }
529
530 /// Returns the count of metrics in Warning state.
531 ///
532 /// # Example
533 ///
534 /// ```rust
535 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
536 ///
537 /// let state = AlertPanelState::new().with_metrics(vec![
538 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0))
539 /// .with_value(80.0),
540 /// ]);
541 /// assert_eq!(state.warning_count(), 1);
542 /// ```
543 pub fn warning_count(&self) -> usize {
544 self.metrics
545 .iter()
546 .filter(|m| m.state == AlertState::Warning)
547 .count()
548 }
549
550 /// Returns the count of metrics in Critical state.
551 ///
552 /// # Example
553 ///
554 /// ```rust
555 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
556 ///
557 /// let state = AlertPanelState::new().with_metrics(vec![
558 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0))
559 /// .with_value(95.0),
560 /// ]);
561 /// assert_eq!(state.critical_count(), 1);
562 /// ```
563 pub fn critical_count(&self) -> usize {
564 self.metrics
565 .iter()
566 .filter(|m| m.state == AlertState::Critical)
567 .count()
568 }
569
570 /// Returns the count of metrics in Unknown state.
571 ///
572 /// # Example
573 ///
574 /// ```rust
575 /// use envision::component::AlertPanelState;
576 ///
577 /// let state = AlertPanelState::new();
578 /// assert_eq!(state.unknown_count(), 0);
579 /// ```
580 pub fn unknown_count(&self) -> usize {
581 self.metrics
582 .iter()
583 .filter(|m| m.state == AlertState::Unknown)
584 .count()
585 }
586
587 /// Returns a reference to the currently selected metric.
588 ///
589 /// # Example
590 ///
591 /// ```rust
592 /// use envision::component::{AlertPanelState, AlertMetric, AlertThreshold};
593 ///
594 /// let state = AlertPanelState::new().with_metrics(vec![
595 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0)),
596 /// ]);
597 /// assert_eq!(state.selected_metric().unwrap().id(), "cpu");
598 /// ```
599 pub fn selected_metric(&self) -> Option<&AlertMetric> {
600 self.metrics.get(self.selected?)
601 }
602
603 /// Returns the number of rows in the grid.
604 pub fn rows(&self) -> usize {
605 if self.metrics.is_empty() {
606 0
607 } else {
608 self.metrics.len().div_ceil(self.columns)
609 }
610 }
611
612 /// Builds the title string with aggregate state counts.
613 pub(crate) fn title_with_counts(&self) -> String {
614 let base = self.title.as_deref().unwrap_or("Alerts");
615 let ok = self.ok_count();
616 let warn = self.warning_count();
617 let crit = self.critical_count();
618 let unknown = self.unknown_count();
619
620 let mut parts = Vec::new();
621 if ok > 0 {
622 parts.push(format!("{} OK", ok));
623 }
624 if warn > 0 {
625 parts.push(format!("{} WARN", warn));
626 }
627 if crit > 0 {
628 parts.push(format!("{} CRIT", crit));
629 }
630 if unknown > 0 {
631 parts.push(format!("{} UNKNOWN", unknown));
632 }
633
634 if parts.is_empty() {
635 base.to_string()
636 } else {
637 format!("{} ({})", base, parts.join(", "))
638 }
639 }
640
641 // ---- Instance methods ----
642
643 /// Updates the state with a message, returning any output.
644 ///
645 /// # Example
646 ///
647 /// ```rust
648 /// use envision::component::{
649 /// AlertPanelState, AlertPanelMessage, AlertPanelOutput,
650 /// AlertMetric, AlertThreshold,
651 /// };
652 ///
653 /// let mut state = AlertPanelState::new().with_metrics(vec![
654 /// AlertMetric::new("cpu", "CPU", AlertThreshold::new(70.0, 90.0)),
655 /// ]);
656 /// let output = state.update(AlertPanelMessage::Select);
657 /// assert_eq!(output, Some(AlertPanelOutput::MetricSelected("cpu".into())));
658 /// ```
659 pub fn update(&mut self, msg: AlertPanelMessage) -> Option<AlertPanelOutput> {
660 AlertPanel::update(self, msg)
661 }
662}
663
664/// A threshold-based alert panel component.
665///
666/// Displays metrics in a grid layout with visual state indicators,
667/// sparkline history, and keyboard navigation.
668///
669/// # Key Bindings
670///
671/// - `Left` / `h` -- Move selection left
672/// - `Right` / `l` -- Move selection right
673/// - `Up` / `k` -- Move selection up
674/// - `Down` / `j` -- Move selection down
675/// - `Enter` -- Confirm selection
676pub struct AlertPanel(PhantomData<()>);
677
678impl Component for AlertPanel {
679 type State = AlertPanelState;
680 type Message = AlertPanelMessage;
681 type Output = AlertPanelOutput;
682
683 fn init() -> Self::State {
684 AlertPanelState::default()
685 }
686
687 fn handle_event(
688 _state: &Self::State,
689 event: &Event,
690 ctx: &EventContext,
691 ) -> Option<Self::Message> {
692 if !ctx.focused || ctx.disabled {
693 return None;
694 }
695
696 let key = event.as_key()?;
697
698 match key.code {
699 Key::Left | Key::Char('h') => Some(AlertPanelMessage::SelectPrev),
700 Key::Right | Key::Char('l') => Some(AlertPanelMessage::SelectNext),
701 Key::Up | Key::Char('k') => Some(AlertPanelMessage::SelectUp),
702 Key::Down | Key::Char('j') => Some(AlertPanelMessage::SelectDown),
703 Key::Enter => Some(AlertPanelMessage::Select),
704 _ => None,
705 }
706 }
707
708 fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
709 match msg {
710 AlertPanelMessage::UpdateMetric { id, value } => {
711 if let Some(metric) = state.metrics.iter_mut().find(|m| m.id == id) {
712 let old_state = metric.state.clone();
713 metric.update_value(value);
714 let new_state = metric.state.clone();
715 if old_state != new_state {
716 return Some(AlertPanelOutput::StateChanged {
717 id,
718 old: old_state,
719 new_state,
720 });
721 }
722 }
723 None
724 }
725 AlertPanelMessage::AddMetric(metric) => {
726 state.add_metric(metric);
727 None
728 }
729 AlertPanelMessage::RemoveMetric(id) => {
730 state.metrics.retain(|m| m.id != id);
731 if state.metrics.is_empty() {
732 state.selected = None;
733 } else if let Some(sel) = state.selected {
734 if sel >= state.metrics.len() {
735 state.selected = Some(state.metrics.len() - 1);
736 }
737 }
738 None
739 }
740 AlertPanelMessage::SetMetrics(metrics) => {
741 state.selected = if metrics.is_empty() { None } else { Some(0) };
742 state.metrics = metrics;
743 None
744 }
745 AlertPanelMessage::SelectNext => {
746 if state.metrics.is_empty() {
747 return None;
748 }
749 let current = state.selected.unwrap_or(0);
750 let cols = state.columns;
751 let current_col = current % cols;
752 if current_col < cols - 1 && current + 1 < state.metrics.len() {
753 let new_index = current + 1;
754 state.selected = Some(new_index);
755 Some(AlertPanelOutput::MetricSelected(
756 state.metrics[new_index].id.clone(),
757 ))
758 } else {
759 None
760 }
761 }
762 AlertPanelMessage::SelectPrev => {
763 if state.metrics.is_empty() {
764 return None;
765 }
766 let current = state.selected.unwrap_or(0);
767 let current_col = current % state.columns;
768 if current_col > 0 {
769 let new_index = current - 1;
770 state.selected = Some(new_index);
771 Some(AlertPanelOutput::MetricSelected(
772 state.metrics[new_index].id.clone(),
773 ))
774 } else {
775 None
776 }
777 }
778 AlertPanelMessage::SelectUp => {
779 if state.metrics.is_empty() {
780 return None;
781 }
782 let current = state.selected.unwrap_or(0);
783 let cols = state.columns;
784 let current_row = current / cols;
785 if current_row > 0 {
786 let new_index = (current_row - 1) * cols + (current % cols);
787 if new_index < state.metrics.len() {
788 state.selected = Some(new_index);
789 return Some(AlertPanelOutput::MetricSelected(
790 state.metrics[new_index].id.clone(),
791 ));
792 }
793 }
794 None
795 }
796 AlertPanelMessage::SelectDown => {
797 if state.metrics.is_empty() {
798 return None;
799 }
800 let current = state.selected.unwrap_or(0);
801 let cols = state.columns;
802 let new_index = (current / cols + 1) * cols + (current % cols);
803 if new_index < state.metrics.len() {
804 state.selected = Some(new_index);
805 Some(AlertPanelOutput::MetricSelected(
806 state.metrics[new_index].id.clone(),
807 ))
808 } else {
809 None
810 }
811 }
812 AlertPanelMessage::SetColumns(columns) => {
813 state.columns = columns.max(1);
814 None
815 }
816 AlertPanelMessage::Select => state
817 .selected_metric()
818 .map(|metric| AlertPanelOutput::MetricSelected(metric.id.clone())),
819 }
820 }
821
822 fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
823 render::render_alert_panel(
824 state,
825 ctx.frame,
826 ctx.area,
827 ctx.theme,
828 ctx.focused,
829 ctx.disabled,
830 );
831 }
832}
833
834#[cfg(test)]
835mod snapshot_tests;
836#[cfg(test)]
837mod tests;