Skip to main content

envision/component/metrics_dashboard/
widget.rs

1//! Metric widget types for the dashboard.
2//!
3//! Contains [`MetricKind`] and [`MetricWidget`], used by the
4//! [`MetricsDashboard`](super::MetricsDashboard) component.
5
6/// The kind of metric a widget displays.
7#[derive(Clone, Debug, PartialEq)]
8#[cfg_attr(
9    feature = "serialization",
10    derive(serde::Serialize, serde::Deserialize)
11)]
12pub enum MetricKind {
13    /// A numeric counter value.
14    Counter {
15        /// The current value.
16        value: i64,
17    },
18    /// A gauge value with a known range.
19    Gauge {
20        /// The current value.
21        value: u64,
22        /// The maximum value.
23        max: u64,
24    },
25    /// A status indicator (up/down).
26    Status {
27        /// Whether the status is "up" (healthy).
28        up: bool,
29    },
30    /// A text-based metric.
31    Text {
32        /// The display text.
33        text: String,
34    },
35}
36
37/// A single metric widget in the dashboard.
38#[derive(Clone, Debug, PartialEq)]
39#[cfg_attr(
40    feature = "serialization",
41    derive(serde::Serialize, serde::Deserialize)
42)]
43pub struct MetricWidget {
44    /// The display label.
45    pub(super) label: String,
46    /// The metric kind and value.
47    pub(super) kind: MetricKind,
48    /// Sparkline history (recent values for trend display).
49    pub(super) history: Vec<u64>,
50    /// Maximum history length.
51    pub(super) max_history: usize,
52}
53
54impl MetricWidget {
55    /// Creates a counter widget.
56    ///
57    /// # Example
58    ///
59    /// ```rust
60    /// use envision::component::MetricWidget;
61    ///
62    /// let widget = MetricWidget::counter("Requests", 42);
63    /// assert_eq!(widget.label(), "Requests");
64    /// assert_eq!(widget.display_value(), "42");
65    /// ```
66    pub fn counter(label: impl Into<String>, value: i64) -> Self {
67        Self {
68            label: label.into(),
69            kind: MetricKind::Counter { value },
70            history: Vec::new(),
71            max_history: 20,
72        }
73    }
74
75    /// Creates a gauge widget with a maximum value.
76    ///
77    /// # Example
78    ///
79    /// ```rust
80    /// use envision::component::MetricWidget;
81    ///
82    /// let widget = MetricWidget::gauge("CPU %", 75, 100);
83    /// assert_eq!(widget.display_value(), "75/100");
84    /// ```
85    pub fn gauge(label: impl Into<String>, value: u64, max: u64) -> Self {
86        Self {
87            label: label.into(),
88            kind: MetricKind::Gauge { value, max },
89            history: Vec::new(),
90            max_history: 20,
91        }
92    }
93
94    /// Creates a status indicator widget.
95    ///
96    /// # Example
97    ///
98    /// ```rust
99    /// use envision::component::MetricWidget;
100    ///
101    /// let widget = MetricWidget::status("API", true);
102    /// assert_eq!(widget.label(), "API");
103    /// assert_eq!(widget.display_value(), "UP");
104    /// ```
105    pub fn status(label: impl Into<String>, up: bool) -> Self {
106        Self {
107            label: label.into(),
108            kind: MetricKind::Status { up },
109            history: Vec::new(),
110            max_history: 0,
111        }
112    }
113
114    /// Creates a text metric widget.
115    ///
116    /// # Example
117    ///
118    /// ```rust
119    /// use envision::component::MetricWidget;
120    ///
121    /// let widget = MetricWidget::text("Version", "1.2.3");
122    /// assert_eq!(widget.label(), "Version");
123    /// assert_eq!(widget.display_value(), "1.2.3");
124    /// ```
125    pub fn text(label: impl Into<String>, text: impl Into<String>) -> Self {
126        Self {
127            label: label.into(),
128            kind: MetricKind::Text { text: text.into() },
129            history: Vec::new(),
130            max_history: 0,
131        }
132    }
133
134    /// Sets the maximum history length for sparkline display (builder pattern).
135    ///
136    /// # Example
137    ///
138    /// ```rust
139    /// use envision::component::MetricWidget;
140    ///
141    /// let widget = MetricWidget::counter("Ops", 0).with_max_history(50);
142    /// assert_eq!(widget.history().len(), 0); // no values yet
143    /// ```
144    pub fn with_max_history(mut self, max: usize) -> Self {
145        self.max_history = max;
146        self
147    }
148
149    /// Returns the label.
150    ///
151    /// # Example
152    ///
153    /// ```rust
154    /// use envision::component::MetricWidget;
155    ///
156    /// let widget = MetricWidget::counter("Requests", 0);
157    /// assert_eq!(widget.label(), "Requests");
158    /// ```
159    pub fn label(&self) -> &str {
160        &self.label
161    }
162
163    /// Returns the metric kind.
164    ///
165    /// # Example
166    ///
167    /// ```rust
168    /// use envision::component::{MetricWidget, MetricKind};
169    ///
170    /// let widget = MetricWidget::status("DB", false);
171    /// assert!(matches!(widget.kind(), MetricKind::Status { up: false }));
172    /// ```
173    pub fn kind(&self) -> &MetricKind {
174        &self.kind
175    }
176
177    /// Returns the sparkline history.
178    ///
179    /// # Example
180    ///
181    /// ```rust
182    /// use envision::component::MetricWidget;
183    ///
184    /// let widget = MetricWidget::counter("Ops", 0);
185    /// assert!(widget.history().is_empty());
186    /// ```
187    pub fn history(&self) -> &[u64] {
188        &self.history
189    }
190
191    /// Returns the display value as a string.
192    ///
193    /// # Example
194    ///
195    /// ```rust
196    /// use envision::component::MetricWidget;
197    ///
198    /// assert_eq!(MetricWidget::counter("A", 42).display_value(), "42");
199    /// assert_eq!(MetricWidget::gauge("B", 75, 100).display_value(), "75/100");
200    /// assert_eq!(MetricWidget::status("C", true).display_value(), "UP");
201    /// assert_eq!(MetricWidget::status("D", false).display_value(), "DOWN");
202    /// assert_eq!(MetricWidget::text("E", "ok").display_value(), "ok");
203    /// ```
204    pub fn display_value(&self) -> String {
205        match &self.kind {
206            MetricKind::Counter { value } => value.to_string(),
207            MetricKind::Gauge { value, max } => format!("{}/{}", value, max),
208            MetricKind::Status { up } => {
209                if *up {
210                    "UP".to_string()
211                } else {
212                    "DOWN".to_string()
213                }
214            }
215            MetricKind::Text { text } => text.clone(),
216        }
217    }
218
219    /// Sets the counter value.
220    ///
221    /// # Example
222    ///
223    /// ```rust
224    /// use envision::component::MetricWidget;
225    ///
226    /// let mut widget = MetricWidget::counter("Requests", 0);
227    /// widget.set_counter_value(100);
228    /// assert_eq!(widget.display_value(), "100");
229    /// ```
230    pub fn set_counter_value(&mut self, value: i64) {
231        if let MetricKind::Counter { value: ref mut v } = self.kind {
232            *v = value;
233            if self.max_history > 0 {
234                self.history.push(value.unsigned_abs());
235                while self.history.len() > self.max_history {
236                    self.history.remove(0);
237                }
238            }
239        }
240    }
241
242    /// Sets the gauge value.
243    ///
244    /// # Example
245    ///
246    /// ```rust
247    /// use envision::component::MetricWidget;
248    ///
249    /// let mut widget = MetricWidget::gauge("Memory", 0, 1024);
250    /// widget.set_gauge_value(512);
251    /// assert_eq!(widget.display_value(), "512/1024");
252    /// ```
253    pub fn set_gauge_value(&mut self, value: u64) {
254        if let MetricKind::Gauge {
255            value: ref mut v,
256            max,
257        } = self.kind
258        {
259            *v = value.min(max);
260            if self.max_history > 0 {
261                self.history.push(value);
262                while self.history.len() > self.max_history {
263                    self.history.remove(0);
264                }
265            }
266        }
267    }
268
269    /// Sets the status.
270    ///
271    /// # Example
272    ///
273    /// ```rust
274    /// use envision::component::MetricWidget;
275    ///
276    /// let mut widget = MetricWidget::status("API", true);
277    /// widget.set_status(false);
278    /// assert_eq!(widget.display_value(), "DOWN");
279    /// ```
280    pub fn set_status(&mut self, up: bool) {
281        if let MetricKind::Status { up: ref mut u } = self.kind {
282            *u = up;
283        }
284    }
285
286    /// Sets the text value.
287    ///
288    /// # Example
289    ///
290    /// ```rust
291    /// use envision::component::MetricWidget;
292    ///
293    /// let mut widget = MetricWidget::text("Version", "1.0");
294    /// widget.set_text("2.0");
295    /// assert_eq!(widget.display_value(), "2.0");
296    /// ```
297    pub fn set_text(&mut self, text: impl Into<String>) {
298        if let MetricKind::Text { text: ref mut t } = self.kind {
299            *t = text.into();
300        }
301    }
302
303    /// Increments a counter by the given amount.
304    ///
305    /// # Example
306    ///
307    /// ```rust
308    /// use envision::component::MetricWidget;
309    ///
310    /// let mut widget = MetricWidget::counter("Hits", 10);
311    /// widget.increment(5);
312    /// assert_eq!(widget.display_value(), "15");
313    /// ```
314    pub fn increment(&mut self, amount: i64) {
315        if let MetricKind::Counter { ref mut value } = self.kind {
316            *value += amount;
317            if self.max_history > 0 {
318                self.history.push(value.unsigned_abs());
319                while self.history.len() > self.max_history {
320                    self.history.remove(0);
321                }
322            }
323        }
324    }
325
326    /// Returns the gauge fill percentage (0.0 to 1.0).
327    ///
328    /// Returns `None` for non-gauge widgets.
329    ///
330    /// # Example
331    ///
332    /// ```rust
333    /// use envision::component::MetricWidget;
334    ///
335    /// let widget = MetricWidget::gauge("CPU", 75, 100);
336    /// assert_eq!(widget.gauge_percentage(), Some(0.75));
337    ///
338    /// let counter = MetricWidget::counter("Ops", 10);
339    /// assert_eq!(counter.gauge_percentage(), None);
340    /// ```
341    pub fn gauge_percentage(&self) -> Option<f64> {
342        match &self.kind {
343            MetricKind::Gauge { value, max } if *max > 0 => Some(*value as f64 / *max as f64),
344            _ => None,
345        }
346    }
347}