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    /// Returns the counter value, if this is a counter widget.
220    ///
221    /// Returns `None` for non-counter widgets.
222    ///
223    /// # Example
224    ///
225    /// ```rust
226    /// use envision::component::MetricWidget;
227    ///
228    /// let widget = MetricWidget::counter("Requests", 42);
229    /// assert_eq!(widget.counter_value(), Some(42));
230    ///
231    /// let gauge = MetricWidget::gauge("Mem", 50, 100);
232    /// assert_eq!(gauge.counter_value(), None);
233    /// ```
234    pub fn counter_value(&self) -> Option<i64> {
235        if let MetricKind::Counter { value } = &self.kind {
236            Some(*value)
237        } else {
238            None
239        }
240    }
241
242    /// Returns the gauge value and max, if this is a gauge widget.
243    ///
244    /// Returns `None` for non-gauge widgets.
245    ///
246    /// # Example
247    ///
248    /// ```rust
249    /// use envision::component::MetricWidget;
250    ///
251    /// let widget = MetricWidget::gauge("Memory", 512, 1024);
252    /// assert_eq!(widget.gauge_value(), Some((512, 1024)));
253    ///
254    /// let counter = MetricWidget::counter("Ops", 0);
255    /// assert_eq!(counter.gauge_value(), None);
256    /// ```
257    pub fn gauge_value(&self) -> Option<(u64, u64)> {
258        if let MetricKind::Gauge { value, max } = &self.kind {
259            Some((*value, *max))
260        } else {
261            None
262        }
263    }
264
265    /// Returns the status (up/down), if this is a status widget.
266    ///
267    /// Returns `None` for non-status widgets.
268    ///
269    /// # Example
270    ///
271    /// ```rust
272    /// use envision::component::MetricWidget;
273    ///
274    /// let widget = MetricWidget::status("API", true);
275    /// assert_eq!(widget.is_status(), Some(true));
276    ///
277    /// let counter = MetricWidget::counter("Ops", 0);
278    /// assert_eq!(counter.is_status(), None);
279    /// ```
280    pub fn is_status(&self) -> Option<bool> {
281        if let MetricKind::Status { up } = &self.kind {
282            Some(*up)
283        } else {
284            None
285        }
286    }
287
288    /// Returns the text value, if this is a text widget.
289    ///
290    /// Returns `None` for non-text widgets.
291    ///
292    /// # Example
293    ///
294    /// ```rust
295    /// use envision::component::MetricWidget;
296    ///
297    /// let widget = MetricWidget::text("Version", "1.2.3");
298    /// assert_eq!(widget.text_value(), Some("1.2.3"));
299    ///
300    /// let counter = MetricWidget::counter("Ops", 0);
301    /// assert_eq!(counter.text_value(), None);
302    /// ```
303    pub fn text_value(&self) -> Option<&str> {
304        if let MetricKind::Text { text } = &self.kind {
305            Some(text)
306        } else {
307            None
308        }
309    }
310
311    /// Sets the counter value.
312    ///
313    /// # Example
314    ///
315    /// ```rust
316    /// use envision::component::MetricWidget;
317    ///
318    /// let mut widget = MetricWidget::counter("Requests", 0);
319    /// widget.set_counter_value(100);
320    /// assert_eq!(widget.display_value(), "100");
321    /// ```
322    pub fn set_counter_value(&mut self, value: i64) {
323        if let MetricKind::Counter { value: ref mut v } = self.kind {
324            *v = value;
325            if self.max_history > 0 {
326                self.history.push(value.unsigned_abs());
327                while self.history.len() > self.max_history {
328                    self.history.remove(0);
329                }
330            }
331        }
332    }
333
334    /// Sets the gauge value.
335    ///
336    /// # Example
337    ///
338    /// ```rust
339    /// use envision::component::MetricWidget;
340    ///
341    /// let mut widget = MetricWidget::gauge("Memory", 0, 1024);
342    /// widget.set_gauge_value(512);
343    /// assert_eq!(widget.display_value(), "512/1024");
344    /// ```
345    pub fn set_gauge_value(&mut self, value: u64) {
346        if let MetricKind::Gauge {
347            value: ref mut v,
348            max,
349        } = self.kind
350        {
351            *v = value.min(max);
352            if self.max_history > 0 {
353                self.history.push(value);
354                while self.history.len() > self.max_history {
355                    self.history.remove(0);
356                }
357            }
358        }
359    }
360
361    /// Sets the status.
362    ///
363    /// # Example
364    ///
365    /// ```rust
366    /// use envision::component::MetricWidget;
367    ///
368    /// let mut widget = MetricWidget::status("API", true);
369    /// widget.set_status(false);
370    /// assert_eq!(widget.display_value(), "DOWN");
371    /// ```
372    pub fn set_status(&mut self, up: bool) {
373        if let MetricKind::Status { up: ref mut u } = self.kind {
374            *u = up;
375        }
376    }
377
378    /// Sets the text value.
379    ///
380    /// # Example
381    ///
382    /// ```rust
383    /// use envision::component::MetricWidget;
384    ///
385    /// let mut widget = MetricWidget::text("Version", "1.0");
386    /// widget.set_text("2.0");
387    /// assert_eq!(widget.display_value(), "2.0");
388    /// ```
389    pub fn set_text(&mut self, text: impl Into<String>) {
390        if let MetricKind::Text { text: ref mut t } = self.kind {
391            *t = text.into();
392        }
393    }
394
395    /// Increments a counter by the given amount.
396    ///
397    /// # Example
398    ///
399    /// ```rust
400    /// use envision::component::MetricWidget;
401    ///
402    /// let mut widget = MetricWidget::counter("Hits", 10);
403    /// widget.increment(5);
404    /// assert_eq!(widget.display_value(), "15");
405    /// ```
406    pub fn increment(&mut self, amount: i64) {
407        if let MetricKind::Counter { ref mut value } = self.kind {
408            *value += amount;
409            if self.max_history > 0 {
410                self.history.push(value.unsigned_abs());
411                while self.history.len() > self.max_history {
412                    self.history.remove(0);
413                }
414            }
415        }
416    }
417
418    /// Returns the gauge fill percentage (0.0 to 1.0).
419    ///
420    /// Returns `None` for non-gauge widgets.
421    ///
422    /// # Example
423    ///
424    /// ```rust
425    /// use envision::component::MetricWidget;
426    ///
427    /// let widget = MetricWidget::gauge("CPU", 75, 100);
428    /// assert_eq!(widget.gauge_percentage(), Some(0.75));
429    ///
430    /// let counter = MetricWidget::counter("Ops", 10);
431    /// assert_eq!(counter.gauge_percentage(), None);
432    /// ```
433    pub fn gauge_percentage(&self) -> Option<f64> {
434        match &self.kind {
435            MetricKind::Gauge { value, max } if *max > 0 => Some(*value as f64 / *max as f64),
436            _ => None,
437        }
438    }
439}