Skip to main content

systemprompt_models/artifacts/dashboard/
section_data.rs

1//! Per-section data payloads for dashboard sections.
2//!
3//! Each struct here is the typed body of one dashboard section kind: metric
4//! cards ([`MetricsCardsData`]/[`MetricCard`]), charts ([`ChartSectionData`]),
5//! tables ([`TableSectionData`]), service/database status
6//! ([`StatusSectionData`]), ranked lists ([`ListSectionData`]), timelines
7//! ([`TimelineSectionData`]), and free text ([`TextSectionData`]).
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12use super::super::chart::ChartDataset;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
17pub struct MetricsCardsData {
18    pub cards: Vec<MetricCard>,
19}
20
21impl MetricsCardsData {
22    pub const fn new(cards: Vec<MetricCard>) -> Self {
23        Self { cards }
24    }
25
26    pub fn add_card(mut self, card: MetricCard) -> Self {
27        self.cards.push(card);
28        self
29    }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
33pub struct MetricCard {
34    pub title: String,
35    pub value: String,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub subtitle: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub icon: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub status: Option<MetricStatus>,
42}
43
44#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, Default)]
45#[serde(rename_all = "lowercase")]
46pub enum MetricStatus {
47    Success,
48    Warning,
49    Error,
50    #[default]
51    Info,
52}
53
54impl std::str::FromStr for MetricStatus {
55    type Err = String;
56
57    fn from_str(s: &str) -> Result<Self, Self::Err> {
58        match s.to_lowercase().as_str() {
59            "success" | "healthy" | "ok" | "active" => Ok(Self::Success),
60            "warning" | "degraded" => Ok(Self::Warning),
61            "error" | "failed" | "critical" => Ok(Self::Error),
62            "info" | "unknown" => Ok(Self::Info),
63            _ => Err(format!("Invalid metric status: {s}")),
64        }
65    }
66}
67
68impl MetricCard {
69    pub fn new(title: impl Into<String>, value: impl Into<String>) -> Self {
70        Self {
71            title: title.into(),
72            value: value.into(),
73            subtitle: None,
74            icon: None,
75            status: None,
76        }
77    }
78
79    pub fn with_subtitle(mut self, subtitle: impl Into<String>) -> Self {
80        self.subtitle = Some(subtitle.into());
81        self
82    }
83
84    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
85        self.icon = Some(icon.into());
86        self
87    }
88
89    pub const fn with_status(mut self, status: MetricStatus) -> Self {
90        self.status = Some(status);
91        self
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
96pub struct ChartSectionData {
97    pub chart_type: String,
98    pub labels: Vec<String>,
99    pub datasets: Vec<ChartDataset>,
100}
101
102impl ChartSectionData {
103    pub fn new(
104        chart_type: impl Into<String>,
105        labels: Vec<String>,
106        datasets: Vec<ChartDataset>,
107    ) -> Self {
108        Self {
109            chart_type: chart_type.into(),
110            labels,
111            datasets,
112        }
113    }
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
117pub struct TableSectionData {
118    pub columns: Vec<String>,
119    pub rows: Vec<serde_json::Value>,
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub sortable: Option<bool>,
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub default_sort: Option<SortConfig>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
127pub struct SortConfig {
128    pub column: String,
129    pub order: String,
130}
131
132impl TableSectionData {
133    pub const fn new(columns: Vec<String>, rows: Vec<serde_json::Value>) -> Self {
134        Self {
135            columns,
136            rows,
137            sortable: None,
138            default_sort: None,
139        }
140    }
141
142    pub const fn with_sortable(mut self, sortable: bool) -> Self {
143        self.sortable = Some(sortable);
144        self
145    }
146
147    pub fn with_default_sort(
148        mut self,
149        column: impl Into<String>,
150        order: impl Into<String>,
151    ) -> Self {
152        self.default_sort = Some(SortConfig {
153            column: column.into(),
154            order: order.into(),
155        });
156        self
157    }
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
161pub struct StatusSectionData {
162    pub services: Vec<ServiceStatus>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub database: Option<DatabaseStatus>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub recent_errors: Option<ErrorCounts>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
170pub struct ServiceStatus {
171    pub name: String,
172    pub status: String,
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub uptime: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
178pub struct DatabaseStatus {
179    pub size_mb: f64,
180    pub status: String,
181}
182
183#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
184pub struct ErrorCounts {
185    pub critical: i32,
186    pub error: i32,
187    pub warn: i32,
188}
189
190impl StatusSectionData {
191    pub const fn new(services: Vec<ServiceStatus>) -> Self {
192        Self {
193            services,
194            database: None,
195            recent_errors: None,
196        }
197    }
198
199    pub fn with_database(mut self, status: DatabaseStatus) -> Self {
200        self.database = Some(status);
201        self
202    }
203
204    pub const fn with_error_counts(mut self, counts: ErrorCounts) -> Self {
205        self.recent_errors = Some(counts);
206        self
207    }
208}
209
210impl ServiceStatus {
211    pub fn new(name: impl Into<String>, status: impl Into<String>) -> Self {
212        Self {
213            name: name.into(),
214            status: status.into(),
215            uptime: None,
216        }
217    }
218
219    pub fn with_uptime(mut self, uptime: impl Into<String>) -> Self {
220        self.uptime = Some(uptime.into());
221        self
222    }
223}
224
225impl DatabaseStatus {
226    pub fn new(size_mb: f64, status: impl Into<String>) -> Self {
227        Self {
228            size_mb,
229            status: status.into(),
230        }
231    }
232}
233
234impl ErrorCounts {
235    pub const fn new(critical: i32, error: i32, warn: i32) -> Self {
236        Self {
237            critical,
238            error,
239            warn,
240        }
241    }
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
245pub struct ListSectionData {
246    pub lists: Vec<ItemList>,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
250pub struct ItemList {
251    pub title: String,
252    pub items: Vec<ListItem>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
256pub struct ListItem {
257    pub rank: i32,
258    pub label: String,
259    pub value: String,
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub badge: Option<String>,
262}
263
264impl ListSectionData {
265    pub const fn new(lists: Vec<ItemList>) -> Self {
266        Self { lists }
267    }
268}
269
270impl ItemList {
271    pub fn new(title: impl Into<String>, items: Vec<ListItem>) -> Self {
272        Self {
273            title: title.into(),
274            items,
275        }
276    }
277}
278
279impl ListItem {
280    pub fn new(rank: i32, label: impl Into<String>, value: impl Into<String>) -> Self {
281        Self {
282            rank,
283            label: label.into(),
284            value: value.into(),
285            badge: None,
286        }
287    }
288
289    pub fn with_badge(mut self, badge: impl Into<String>) -> Self {
290        self.badge = Some(badge.into());
291        self
292    }
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
296pub struct TextSectionData {
297    pub text: String,
298}
299
300impl TextSectionData {
301    pub fn new(text: impl Into<String>) -> Self {
302        Self { text: text.into() }
303    }
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
307pub struct TimelineSectionData {
308    pub events: Vec<TimelineEvent>,
309}
310
311impl TimelineSectionData {
312    pub const fn new(events: Vec<TimelineEvent>) -> Self {
313        Self { events }
314    }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
318pub struct TimelineEvent {
319    pub timestamp: String,
320    pub label: String,
321    #[serde(skip_serializing_if = "Option::is_none")]
322    pub description: Option<String>,
323}
324
325impl TimelineEvent {
326    pub fn new(timestamp: impl Into<String>, label: impl Into<String>) -> Self {
327        Self {
328            timestamp: timestamp.into(),
329            label: label.into(),
330            description: None,
331        }
332    }
333
334    pub fn with_description(mut self, description: impl Into<String>) -> Self {
335        self.description = Some(description.into());
336        self
337    }
338}