1use std::collections::{
2 BTreeMap,
3 HashMap,
4};
5
6use serde::{
7 Deserialize,
8 Serialize,
9};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub enum ConfigFieldType {
13 Text,
14 Password,
15 TextArea,
16 Boolean,
17 Select,
18 MultiSelect,
19 Number,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ConfigSection {
25 Authentication,
26 Connection,
27 Settings,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ConfigField {
32 pub key: String,
33 pub label: String,
34 pub description: Option<String>,
35 pub field_type: ConfigFieldType,
36 pub required: bool,
37 pub default_value: Option<serde_json::Value>,
38 pub options: Option<Vec<String>>,
39 pub validation_regex: Option<String>,
40 pub validation_message: Option<String>,
41 pub section: ConfigSection,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum AuthMode {
47 None,
48 Token,
49 SignIn,
50}
51
52pub const AUTH_MODE_KEY: &str = "auth_mode";
53
54pub fn is_no_auth(config: &HashMap<String, String>) -> bool {
55 config
56 .get(AUTH_MODE_KEY)
57 .is_some_and(|mode| mode.trim() == "none")
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ConfigSchema {
62 pub fields: Vec<ConfigField>,
63 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
64 pub auth_modes: BTreeMap<String, Vec<AuthMode>>,
65 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
66 pub no_auth: bool,
67}
68
69impl ConfigSchema {
70 pub fn new() -> Self {
71 Self {
72 fields: Vec::new(),
73 auth_modes: BTreeMap::new(),
74 no_auth: false,
75 }
76 }
77
78 pub fn add_field(mut self, field: ConfigField) -> Self {
79 self.fields.push(field);
80 self
81 }
82
83 pub fn add_auth_field(mut self, modes: &[AuthMode], field: ConfigField) -> Self {
84 self.auth_modes.insert(field.key.clone(), modes.to_vec());
85 self.fields.push(field);
86 self
87 }
88
89 pub fn allow_no_auth(mut self) -> Self {
90 self.no_auth = true;
91 self
92 }
93}
94
95impl Default for ConfigSchema {
96 fn default() -> Self {
97 Self::new()
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
102pub enum ColumnDataType {
103 String,
104 Number,
105 DateTime,
106 Duration,
107 Status,
108 Badge,
109 Url,
110 Json,
111 Boolean,
112 Custom(String),
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116pub enum CellRenderer {
117 Text,
118 Badge,
119 DateTime,
120 Duration,
121 StatusBadge,
122 Commit,
123 Avatar,
124 TruncatedText,
125 Link,
126 JsonViewer,
127 Custom(String),
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub enum ColumnVisibility {
132 Always,
133 WhenPresent,
134 WhenCapability(String),
135 Conditional {
136 field: String,
137 equals: serde_json::Value,
138 },
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ColumnDefinition {
143 pub id: String,
144 pub label: String,
145 pub description: Option<String>,
146 pub field_path: String,
147 pub data_type: ColumnDataType,
148 pub renderer: CellRenderer,
149 pub visibility: ColumnVisibility,
150 #[serde(default = "default_visible_true")]
151 pub default_visible: bool,
152 pub width: Option<u32>,
153 pub sortable: bool,
154 pub filterable: bool,
155}
156
157fn default_visible_true() -> bool {
158 true
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TableDefinition {
163 pub id: String,
164 pub name: String,
165 pub description: Option<String>,
166 pub columns: Vec<ColumnDefinition>,
167 pub default_sort_column: Option<String>,
168 pub default_sort_direction: Option<String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct TableSchema {
173 pub tables: Vec<TableDefinition>,
174}
175
176impl TableSchema {
177 pub fn new() -> Self {
178 Self { tables: Vec::new() }
179 }
180
181 pub fn add_table(mut self, table: TableDefinition) -> Self {
182 self.tables.push(table);
183 self
184 }
185
186 pub fn get_table(&self, table_id: &str) -> Option<&TableDefinition> {
187 self.tables.iter().find(|t| t.id == table_id)
188 }
189}
190
191impl Default for TableSchema {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use serde_json::json;
200
201 use super::*;
202
203 fn field(key: &str) -> ConfigField {
204 ConfigField {
205 key: key.to_string(),
206 label: key.to_string(),
207 description: None,
208 field_type: ConfigFieldType::Text,
209 required: false,
210 default_value: None,
211 options: None,
212 validation_regex: None,
213 validation_message: None,
214 section: ConfigSection::Connection,
215 }
216 }
217
218 #[test]
219 fn auth_fields_serialize_their_modes_by_key() {
220 let schema = ConfigSchema::new()
221 .allow_no_auth()
222 .add_field(field("deck_url"))
223 .add_auth_field(&[AuthMode::SignIn], field("sso_issuer"))
224 .add_auth_field(&[AuthMode::Token, AuthMode::SignIn], field("kubeconfig"));
225 let value = serde_json::to_value(&schema).unwrap();
226 assert_eq!(
227 value["auth_modes"],
228 json!({"sso_issuer": ["sign_in"], "kubeconfig": ["token", "sign_in"]})
229 );
230 assert_eq!(value["no_auth"], json!(true));
231 assert_eq!(value["fields"].as_array().unwrap().len(), 3);
232
233 let plain = serde_json::to_value(ConfigSchema::new()).unwrap();
234 assert!(plain.get("auth_modes").is_none());
235 assert!(plain.get("no_auth").is_none());
236 }
237
238 #[test]
239 fn no_auth_is_read_from_the_auth_mode_key() {
240 let config = |mode: &str| HashMap::from([(AUTH_MODE_KEY.to_string(), mode.to_string())]);
241 assert!(is_no_auth(&config("none")));
242 assert!(!is_no_auth(&config("token")));
243 assert!(!is_no_auth(&HashMap::new()));
244 }
245
246 #[test]
247 fn fields_serialize_their_section_in_snake_case() {
248 let mut auth = field("token");
249 auth.section = ConfigSection::Authentication;
250 assert_eq!(
251 serde_json::to_value(&auth).unwrap()["section"],
252 json!("authentication")
253 );
254 }
255}