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