metabase_api_rs/core/models/
dashboard.rs

1//! Dashboard model representing Metabase dashboards
2//!
3//! This module provides the core data structures for working with
4//! Metabase dashboards, including dashboard cards and parameters.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use super::common::{DashboardId, UserId};
10
11/// Represents a Metabase dashboard
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct Dashboard {
14    /// Unique identifier for the dashboard
15    pub id: Option<DashboardId>,
16
17    /// Dashboard name
18    pub name: String,
19
20    /// Optional description
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub description: Option<String>,
23
24    /// ID of the collection this dashboard belongs to
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub collection_id: Option<i32>,
27
28    /// Creator of the dashboard
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub creator_id: Option<UserId>,
31
32    /// Dashboard parameters for filtering
33    #[serde(default, skip_serializing_if = "Vec::is_empty")]
34    pub parameters: Vec<DashboardParameter>,
35
36    /// Cards (visualizations) on the dashboard
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub cards: Vec<DashboardCard>,
39
40    /// When the dashboard was created
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub created_at: Option<DateTime<Utc>>,
43
44    /// When the dashboard was last updated
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub updated_at: Option<DateTime<Utc>>,
47
48    /// Whether the dashboard is archived
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub archived: Option<bool>,
51
52    // Additional fields from API specification
53    /// Cache time-to-live in seconds
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub cache_ttl: Option<i32>,
56
57    /// Position in the collection
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub collection_position: Option<i32>,
60
61    /// Whether embedding is enabled
62    #[serde(default)]
63    pub enable_embedding: bool,
64
65    /// Embedding parameters
66    #[serde(default)]
67    pub embedding_params: serde_json::Value,
68}
69
70/// Represents a parameter on a dashboard for filtering
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct DashboardParameter {
73    /// Parameter ID
74    pub id: String,
75
76    /// Parameter name
77    pub name: String,
78
79    /// Parameter slug for URL
80    pub slug: String,
81
82    /// Parameter type (e.g., "category", "date", "number")
83    #[serde(rename = "type")]
84    pub parameter_type: String,
85
86    /// Default value for the parameter
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub default: Option<serde_json::Value>,
89}
90
91/// Represents a card (visualization) on a dashboard
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93pub struct DashboardCard {
94    /// Unique identifier for the dashboard card
95    pub id: i32,
96
97    /// ID of the card being displayed
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub card_id: Option<i32>,
100
101    /// Position and size on the dashboard grid
102    pub row: i32,
103    pub col: i32,
104    pub size_x: i32,
105    pub size_y: i32,
106
107    /// Visualization settings override
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub visualization_settings: Option<serde_json::Value>,
110
111    /// Parameter mappings
112    #[serde(default, skip_serializing_if = "Vec::is_empty")]
113    pub parameter_mappings: Vec<ParameterMapping>,
114}
115
116/// Represents a parameter mapping between dashboard and card
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct ParameterMapping {
119    /// Dashboard parameter ID
120    pub parameter_id: String,
121
122    /// Card parameter ID
123    pub card_id: i32,
124
125    /// Target field or variable
126    pub target: serde_json::Value,
127}
128
129/// Request payload for creating a dashboard
130#[derive(Debug, Clone, Serialize)]
131pub struct CreateDashboardRequest {
132    /// Dashboard name
133    pub name: String,
134
135    /// Optional description
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub description: Option<String>,
138
139    /// Collection to place the dashboard in
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub collection_id: Option<i32>,
142
143    /// Initial parameters
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    pub parameters: Vec<DashboardParameter>,
146}
147
148/// Request payload for updating a dashboard
149#[derive(Debug, Clone, Default, Serialize)]
150pub struct UpdateDashboardRequest {
151    /// New name
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub name: Option<String>,
154
155    /// New description
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub description: Option<String>,
158
159    /// New collection ID
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub collection_id: Option<i32>,
162
163    /// Whether to archive the dashboard
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub archived: Option<bool>,
166
167    /// Updated parameters
168    #[serde(skip_serializing_if = "Option::is_none")]
169    pub parameters: Option<Vec<DashboardParameter>>,
170}
171
172impl Dashboard {
173    /// Creates a new dashboard builder
174    pub fn builder(name: impl Into<String>) -> DashboardBuilder {
175        DashboardBuilder::new(name)
176    }
177}
178
179/// Builder for creating Dashboard instances
180pub struct DashboardBuilder {
181    name: String,
182    description: Option<String>,
183    collection_id: Option<i32>,
184    parameters: Vec<DashboardParameter>,
185    cards: Vec<DashboardCard>,
186    cache_ttl: Option<i32>,
187    collection_position: Option<i32>,
188    enable_embedding: bool,
189    embedding_params: serde_json::Value,
190}
191
192impl DashboardBuilder {
193    /// Creates a new dashboard builder with the given name
194    pub fn new(name: impl Into<String>) -> Self {
195        Self {
196            name: name.into(),
197            description: None,
198            collection_id: None,
199            parameters: Vec::new(),
200            cards: Vec::new(),
201            cache_ttl: None,
202            collection_position: None,
203            enable_embedding: false,
204            embedding_params: serde_json::Value::Object(serde_json::Map::new()),
205        }
206    }
207
208    /// Sets the dashboard description
209    pub fn description(mut self, desc: impl Into<String>) -> Self {
210        self.description = Some(desc.into());
211        self
212    }
213
214    /// Sets the collection ID
215    pub fn collection_id(mut self, id: i32) -> Self {
216        self.collection_id = Some(id);
217        self
218    }
219
220    /// Adds a parameter to the dashboard
221    pub fn add_parameter(mut self, param: DashboardParameter) -> Self {
222        self.parameters.push(param);
223        self
224    }
225
226    /// Adds a card to the dashboard
227    pub fn add_card(mut self, card: DashboardCard) -> Self {
228        self.cards.push(card);
229        self
230    }
231
232    /// Sets the cache TTL
233    pub fn cache_ttl(mut self, ttl: i32) -> Self {
234        self.cache_ttl = Some(ttl);
235        self
236    }
237
238    /// Sets the collection position
239    pub fn collection_position(mut self, position: i32) -> Self {
240        self.collection_position = Some(position);
241        self
242    }
243
244    /// Sets whether embedding is enabled
245    pub fn enable_embedding(mut self, enabled: bool) -> Self {
246        self.enable_embedding = enabled;
247        self
248    }
249
250    /// Sets the embedding parameters
251    pub fn embedding_params(mut self, params: serde_json::Value) -> Self {
252        self.embedding_params = params;
253        self
254    }
255
256    /// Builds the Dashboard instance
257    pub fn build(self) -> Dashboard {
258        Dashboard {
259            id: None, // Will be set by the server
260            name: self.name,
261            description: self.description,
262            collection_id: self.collection_id,
263            creator_id: None,
264            parameters: self.parameters,
265            cards: self.cards,
266            created_at: None,
267            updated_at: None,
268            archived: Some(false),
269            cache_ttl: self.cache_ttl,
270            collection_position: self.collection_position,
271            enable_embedding: self.enable_embedding,
272            embedding_params: self.embedding_params,
273        }
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn test_dashboard_creation() {
283        let dashboard = Dashboard::builder("Sales Dashboard")
284            .description("Monthly sales metrics")
285            .collection_id(10)
286            .build();
287
288        assert_eq!(dashboard.name, "Sales Dashboard");
289        assert_eq!(
290            dashboard.description,
291            Some("Monthly sales metrics".to_string())
292        );
293        assert_eq!(dashboard.collection_id, Some(10));
294        assert_eq!(dashboard.archived, Some(false));
295        assert!(dashboard.parameters.is_empty());
296        assert!(dashboard.cards.is_empty());
297    }
298
299    #[test]
300    fn test_dashboard_with_parameters() {
301        let param = DashboardParameter {
302            id: "date_range".to_string(),
303            name: "Date Range".to_string(),
304            slug: "date_range".to_string(),
305            parameter_type: "date/range".to_string(),
306            default: None,
307        };
308
309        let dashboard = Dashboard::builder("Analytics Dashboard")
310            .add_parameter(param.clone())
311            .build();
312
313        assert_eq!(dashboard.parameters.len(), 1);
314        assert_eq!(dashboard.parameters[0].id, "date_range");
315    }
316
317    #[test]
318    fn test_dashboard_card() {
319        let card = DashboardCard {
320            id: 1,
321            card_id: Some(100),
322            row: 0,
323            col: 0,
324            size_x: 4,
325            size_y: 3,
326            visualization_settings: None,
327            parameter_mappings: Vec::new(),
328        };
329
330        assert_eq!(card.card_id, Some(100));
331        assert_eq!(card.size_x, 4);
332        assert_eq!(card.size_y, 3);
333    }
334
335    #[test]
336    fn test_create_dashboard_request() {
337        let request = CreateDashboardRequest {
338            name: "New Dashboard".to_string(),
339            description: Some("Test dashboard".to_string()),
340            collection_id: Some(5),
341            parameters: vec![],
342        };
343
344        assert_eq!(request.name, "New Dashboard");
345        assert_eq!(request.description, Some("Test dashboard".to_string()));
346        assert_eq!(request.collection_id, Some(5));
347    }
348
349    #[test]
350    fn test_update_dashboard_request() {
351        let request = UpdateDashboardRequest {
352            name: Some("Updated Name".to_string()),
353            archived: Some(true),
354            ..Default::default()
355        };
356
357        assert_eq!(request.name, Some("Updated Name".to_string()));
358        assert_eq!(request.archived, Some(true));
359        assert!(request.description.is_none());
360        assert!(request.collection_id.is_none());
361    }
362
363    #[test]
364    fn test_dashboard_with_new_fields() {
365        let dashboard = Dashboard::builder("Enhanced Dashboard")
366            .cache_ttl(600)
367            .collection_position(1)
368            .enable_embedding(true)
369            .embedding_params(serde_json::json!({"key": "value"}))
370            .build();
371
372        assert_eq!(dashboard.name, "Enhanced Dashboard");
373        assert_eq!(dashboard.cache_ttl, Some(600));
374        assert_eq!(dashboard.collection_position, Some(1));
375        assert!(dashboard.enable_embedding);
376        assert_eq!(dashboard.embedding_params["key"], "value");
377    }
378}