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}