perspective_viewer/config/
workspace_config.rs1use std::collections::BTreeMap;
14
15use perspective_client::config::Filter;
16
17use crate::config::{PanelViewerConfig, ViewerConfigInitial};
18use crate::utils::CssKind;
19
20#[derive(serde::Serialize, ts_rs::TS)]
29pub struct WorkspaceConfig {
30 pub version: String,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
33 #[ts(optional)]
34 pub active: Option<String>,
35
36 pub layout: Option<crate::js::Layout>,
37
38 pub panels: BTreeMap<String, PanelViewerConfig>,
43
44 #[serde(skip_serializing_if = "Vec::is_empty")]
48 #[ts(as = "Option<_>")]
49 #[ts(optional)]
50 pub global_filters: Vec<Filter>,
51
52 #[serde(skip_serializing_if = "Vec::is_empty")]
57 #[ts(as = "Option<_>")]
58 #[ts(optional)]
59 pub masters: Vec<String>,
60
61 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
65 #[ts(as = "Option<_>")]
66 #[ts(optional)]
67 pub palette: BTreeMap<String, String>,
68}
69
70#[derive(serde::Deserialize, ts_rs::TS)]
77pub struct WorkspaceConfigUpdate {
78 #[serde(default)]
79 #[ts(optional)]
80 pub active: Option<String>,
81
82 #[serde(default)]
83 #[ts(optional)]
84 pub layout: Option<crate::js::Layout>,
85
86 pub panels: BTreeMap<String, ViewerConfigInitial>,
87
88 #[serde(default)]
94 #[ts(as = "Option<_>")]
95 #[ts(optional)]
96 pub global_filters: Option<Vec<Filter>>,
97
98 #[serde(default)]
101 #[ts(as = "Option<_>")]
102 #[ts(optional)]
103 pub masters: Option<Vec<String>>,
104
105 #[serde(default)]
109 #[ts(as = "Option<_>")]
110 #[ts(optional)]
111 pub palette: Option<BTreeMap<String, String>>,
112}
113
114pub fn validate_palette(
117 palette: BTreeMap<String, String>,
118) -> Result<BTreeMap<String, String>, String> {
119 palette
120 .into_iter()
121 .map(|(name, value)| {
122 let kind = CssKind::of_var(&name).ok_or_else(|| {
123 format!(
124 "`palette` key `{name}` must start with `--psp-user--gradient-`, \
125 `--psp-user--palette-` or `--psp-user--color-`"
126 )
127 })?;
128
129 let canonical = kind
130 .canonicalize(&value)
131 .map_err(|error| format!("`palette[\"{name}\"]`: {error}"))?;
132
133 Ok((name, canonical))
134 })
135 .collect()
136}
137
138#[cfg(test)]
139mod tests {
140 use serde_json::json;
141
142 use super::*;
143
144 #[test]
145 fn palette_serializes_only_when_present() {
146 let config = WorkspaceConfig {
147 version: "x".to_owned(),
148 active: None,
149 layout: None,
150 panels: BTreeMap::new(),
151 global_filters: vec![],
152 masters: vec![],
153 palette: BTreeMap::new(),
154 };
155
156 assert_eq!(
157 serde_json::to_value(&config).unwrap(),
158 json!({ "version": "x", "layout": null, "panels": {} })
159 );
160
161 let mut palette = BTreeMap::new();
162 palette.insert("--psp-user--color-hot".to_owned(), "#ff0000".to_owned());
163
164 let config = WorkspaceConfig { palette, ..config };
165 assert_eq!(
166 serde_json::to_value(&config).unwrap(),
167 json!({
168 "version": "x",
169 "layout": null,
170 "panels": {},
171 "palette": { "--psp-user--color-hot": "#ff0000" },
172 })
173 );
174 }
175
176 #[test]
177 fn update_palette_defaults_empty_and_validates_by_prefix() {
178 let update: WorkspaceConfigUpdate =
179 serde_json::from_value(json!({ "panels": {} })).unwrap();
180 assert!(update.palette.is_none());
181
182 let update: WorkspaceConfigUpdate = serde_json::from_value(json!({
183 "panels": {},
184 "palette": {
185 "--psp-user--gradient-1": "linear-gradient(#000, #fff)",
186 "--psp-user--palette-warm": "linear-gradient(90deg, RGB(255,0,0), #ff0)",
187 "--psp-user--color-hot": "#F00",
188 },
189 }))
190 .unwrap();
191
192 let valid = validate_palette(update.palette.unwrap()).unwrap();
193 assert_eq!(
194 valid.get("--psp-user--gradient-1").unwrap(),
195 "linear-gradient(to right, #000000 0%, #ffffff 100%)"
196 );
197 assert_eq!(
198 valid.get("--psp-user--palette-warm").unwrap(),
199 "linear-gradient(to right, #ff0000, #ffff00)"
200 );
201 assert_eq!(valid.get("--psp-user--color-hot").unwrap(), "#ff0000");
202
203 let bad = |name: &str, value: &str| {
204 let mut map = BTreeMap::new();
205 map.insert(name.to_owned(), value.to_owned());
206 validate_palette(map).unwrap_err()
207 };
208
209 assert!(bad("--psp-user--other-1", "#ff0000").contains("--psp-user--other-1"));
210 assert!(bad("--psp-charts--gradient", "#ff0000").contains("must start with"));
211 assert!(
212 bad(
213 "--psp-user--palette-1",
214 "linear-gradient(#000 0%, #fff 100%)"
215 )
216 .contains("--psp-user--palette-1")
217 );
218 assert!(bad("--psp-user--gradient-1", "#ff0000").contains("linear-gradient"));
219 assert!(bad("--psp-user--color-1", "red").contains("red"));
220 }
221}