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}