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