diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d98c22..9c7fccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - renamed env var `AUTOKUMA__KUMA__TAG_NAME` to `AUTOKUMA__TAG_NAME` due to package splitting - renamed env var `AUTOKUMA__KUMA__TAG_COLOR` to `AUTOKUMA__TAG_COLOR` due to package splitting - renamed env var `AUTOKUMA__KUMA__DEFAULT_SETTINGS` to `AUTOKUMA__DEFAULT_SETTINGS` due to package splitting +- automatically append `/socket.io/` to `KUMA__URL` ## [0.2.0] - 2024-01-09 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2a8e833..9f45a50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1054,6 +1054,7 @@ dependencies = [ "futures-util", "itertools", "log", + "reqwest", "rust_socketio", "serde", "serde-inline-default", @@ -1067,6 +1068,7 @@ dependencies = [ "thiserror", "time", "tokio", + "url", ] [[package]] @@ -2362,6 +2364,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1807de9..e628f03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ humantime-serde = { version = "1.1.1" } itertools = { version = "0.12.0" } log = { version = "0.4.20" } pretty_env_logger = { version = "0.5.0" } +reqwest = { version = "0.11.23", features = ["json"] } rust_socketio = { git = "https://github.com/1c3t3a/rust-socketio.git", rev = "9ccf67b", features = [ "async", ] } @@ -36,3 +37,4 @@ thiserror = { version = "1.0.56" } time = { version = "0.3.31", features = ["serde"] } tokio = { version = "1.35.1", features = ["full"] } toml = { version = "0.8.8" } +url = { version = "*", features = ["serde"] } diff --git a/README.md b/README.md index 8904b25..7b13549 100644 --- a/README.md +++ b/README.md @@ -169,12 +169,12 @@ Kuma CLI is a Command-Line Interface (CLI) tool for managing and interacting wit - [x] `get` - [x] `pause` - [x] `resume` -- [ ] Commands : `kuma status` - - [ ] `add` - - [ ] `delete` - - [ ] `edit` - - [ ] `ls` - - [ ] `get` +- [x] Commands : `kuma status-page` + - [x] `add` + - [x] `delete` + - [x] `edit` + - [x] `ls` + - [x] `get` ## 🔧 How to Install @@ -226,7 +226,7 @@ Options: All configuration options can also be specified as environment variables: ``` -KUMA__URL="http://localhost:3001/socket.io/" +KUMA__URL="http://localhost:3001/" KUMA__USERNAME="" KUMA__PASSWORD="" ... @@ -241,7 +241,7 @@ Additionally Kuma CLI will read configuration from a file named `kuma.{toml,yaml An example `.toml` config could look like the following: ```toml -url = "http://localhost:3001/socket.io/" +url = "http://localhost:3001/" username = "" password = "" ``` diff --git a/kuma-cli/src/main.rs b/kuma-cli/src/main.rs index 72d14d3..4362478 100644 --- a/kuma-cli/src/main.rs +++ b/kuma-cli/src/main.rs @@ -134,27 +134,32 @@ enum Commands { /// Manage Monitors Monitor { #[command(subcommand)] - command: Option, + command: Option, }, /// Manage Notifications Notification { #[command(subcommand)] - command: Option, + command: Option, }, /// Manage Tags Tag { #[command(subcommand)] - command: Option, + command: Option, }, /// Manage Maintenances Maintenance { #[command(subcommand)] - command: Option, + command: Option, + }, + /// Manage Status Pages + StatusPage { + #[command(subcommand)] + command: Option, }, } #[derive(Subcommand, Clone, Debug)] -enum MonitorCommands { +enum MonitorCommand { /// Add a new Monitor Add { file: PathBuf }, /// Edit a Monitor @@ -172,7 +177,7 @@ enum MonitorCommands { } #[derive(Subcommand, Clone, Debug)] -enum TagCommands { +enum TagCommand { /// Add a new Tag Add { file: PathBuf }, /// Edit a Tag @@ -186,7 +191,7 @@ enum TagCommands { } #[derive(Subcommand, Clone, Debug)] -enum NotificationCommands { +enum NotificationCommand { /// Add a new Notification Add { file: PathBuf }, /// Edit a Notification @@ -200,7 +205,7 @@ enum NotificationCommands { } #[derive(Subcommand, Clone, Debug)] -enum MaintenanceCommands { +enum MaintenanceCommand { /// Add a new Monitor Add { file: PathBuf }, /// Edit a Monitor @@ -217,6 +222,20 @@ enum MaintenanceCommands { Pause { id: i32 }, } +#[derive(Subcommand, Clone, Debug)] +enum StatusPageCommand { + /// Add a new StatusPage + Add { file: PathBuf }, + /// Edit a StatusPage + Edit { file: PathBuf }, + /// Get a StatusPage + Get { slug: String }, + /// Delete a StatusPage + Delete { slug: String }, + /// Get all StatusPages + List {}, +} + trait PrintResult { fn print_result(self, cli: &Cli); } @@ -291,45 +310,45 @@ async fn connect(config: &Config, cli: &Cli) -> kuma_client::Client { .unwrap_or_die(cli) } -async fn monitor_commands(command: &Option, config: &Config, cli: &Cli) { +async fn monitor_commands(command: &Option, config: &Config, cli: &Cli) { match command { - Some(MonitorCommands::Add { file }) => connect(config, cli) + Some(MonitorCommand::Add { file }) => connect(config, cli) .await .add_monitor(load_file(file, cli).await) .await .print_result(cli), - Some(MonitorCommands::Edit { file }) => connect(config, cli) + Some(MonitorCommand::Edit { file }) => connect(config, cli) .await .edit_monitor(load_file(file, cli).await) .await .print_result(cli), - Some(MonitorCommands::Get { id }) => connect(config, cli) + Some(MonitorCommand::Get { id }) => connect(config, cli) .await .get_monitor(*id) .await .print_result(cli), - Some(MonitorCommands::Delete { id }) => connect(config, cli) + Some(MonitorCommand::Delete { id }) => connect(config, cli) .await .delete_monitor(*id) .await .print_result(cli), - Some(MonitorCommands::List {}) => connect(config, cli) + Some(MonitorCommand::List {}) => connect(config, cli) .await .get_monitors() .await .print_result(cli), - Some(MonitorCommands::Resume { id }) => connect(config, cli) + Some(MonitorCommand::Resume { id }) => connect(config, cli) .await .resume_monitor(*id) .await .print_result(cli), - Some(MonitorCommands::Pause { id }) => connect(config, cli) + Some(MonitorCommand::Pause { id }) => connect(config, cli) .await .pause_monitor(*id) .await @@ -339,33 +358,33 @@ async fn monitor_commands(command: &Option, config: &Config, cl } } -async fn notification_commands(command: &Option, config: &Config, cli: &Cli) { +async fn notification_commands(command: &Option, config: &Config, cli: &Cli) { match command { - Some(NotificationCommands::Add { file }) => connect(config, cli) + Some(NotificationCommand::Add { file }) => connect(config, cli) .await .add_notification(load_file(file, cli).await) .await .print_result(cli), - Some(NotificationCommands::Edit { file }) => connect(config, cli) + Some(NotificationCommand::Edit { file }) => connect(config, cli) .await .edit_notification(load_file(file, cli).await) .await .print_result(cli), - Some(NotificationCommands::Get { id }) => connect(config, cli) + Some(NotificationCommand::Get { id }) => connect(config, cli) .await .get_notification(*id) .await .print_result(cli), - Some(NotificationCommands::Delete { id }) => connect(config, cli) + Some(NotificationCommand::Delete { id }) => connect(config, cli) .await .delete_notification(*id) .await .print_result(cli), - Some(NotificationCommands::List {}) => connect(config, cli) + Some(NotificationCommand::List {}) => connect(config, cli) .await .get_notifications() .await @@ -375,33 +394,33 @@ async fn notification_commands(command: &Option, config: & } } -async fn tag_commands(command: &Option, config: &Config, cli: &Cli) { +async fn tag_commands(command: &Option, config: &Config, cli: &Cli) { match command { - Some(TagCommands::Add { file }) => connect(config, cli) + Some(TagCommand::Add { file }) => connect(config, cli) .await .add_tag(load_file(file, cli).await) .await .print_result(cli), - Some(TagCommands::Edit { file }) => connect(config, cli) + Some(TagCommand::Edit { file }) => connect(config, cli) .await .edit_tag(load_file(file, cli).await) .await .print_result(cli), - Some(TagCommands::Get { id }) => connect(config, cli) + Some(TagCommand::Get { id }) => connect(config, cli) .await .get_tag(*id) .await .print_result(cli), - Some(TagCommands::Delete { id }) => connect(config, cli) + Some(TagCommand::Delete { id }) => connect(config, cli) .await .delete_tag(*id) .await .print_result(cli), - Some(TagCommands::List {}) => connect(config, cli) + Some(TagCommand::List {}) => connect(config, cli) .await .get_tags() .await @@ -411,45 +430,45 @@ async fn tag_commands(command: &Option, config: &Config, cli: &Cli) } } -async fn maintenance_commands(command: &Option, config: &Config, cli: &Cli) { +async fn maintenance_commands(command: &Option, config: &Config, cli: &Cli) { match command { - Some(MaintenanceCommands::Add { file }) => connect(config, cli) + Some(MaintenanceCommand::Add { file }) => connect(config, cli) .await .add_maintenance(load_file(file, cli).await) .await .print_result(cli), - Some(MaintenanceCommands::Edit { file }) => connect(config, cli) + Some(MaintenanceCommand::Edit { file }) => connect(config, cli) .await .edit_maintenance(load_file(file, cli).await) .await .print_result(cli), - Some(MaintenanceCommands::Get { id }) => connect(config, cli) + Some(MaintenanceCommand::Get { id }) => connect(config, cli) .await .get_maintenance(*id) .await .print_result(cli), - Some(MaintenanceCommands::Delete { id }) => connect(config, cli) + Some(MaintenanceCommand::Delete { id }) => connect(config, cli) .await .delete_maintenance(*id) .await .print_result(cli), - Some(MaintenanceCommands::List {}) => connect(config, cli) + Some(MaintenanceCommand::List {}) => connect(config, cli) .await .get_maintenances() .await .print_result(cli), - Some(MaintenanceCommands::Resume { id }) => connect(config, cli) + Some(MaintenanceCommand::Resume { id }) => connect(config, cli) .await .resume_maintenance(*id) .await .print_result(cli), - Some(MaintenanceCommands::Pause { id }) => connect(config, cli) + Some(MaintenanceCommand::Pause { id }) => connect(config, cli) .await .pause_maintenance(*id) .await @@ -459,6 +478,42 @@ async fn maintenance_commands(command: &Option, config: &Co } } +async fn status_page_commands(command: &Option, config: &Config, cli: &Cli) { + match command { + Some(StatusPageCommand::Add { file }) => connect(config, cli) + .await + .add_status_page(load_file(file, cli).await) + .await + .print_result(cli), + + Some(StatusPageCommand::Edit { file }) => connect(config, cli) + .await + .edit_status_page(load_file(file, cli).await) + .await + .print_result(cli), + + Some(StatusPageCommand::Get { slug }) => connect(config, cli) + .await + .get_status_page(slug) + .await + .print_result(cli), + + Some(StatusPageCommand::Delete { slug }) => connect(config, cli) + .await + .delete_status_page(slug) + .await + .print_result(cli), + + Some(StatusPageCommand::List {}) => connect(config, cli) + .await + .get_status_pages() + .await + .print_result(cli), + + None => {} + } +} + #[tokio::main()] async fn main() { pretty_env_logger::formatted_timed_builder() @@ -478,6 +533,9 @@ async fn main() { Some(Commands::Maintenance { command }) => { maintenance_commands(command, &config, &cli).await } + Some(Commands::StatusPage { command }) => { + status_page_commands(command, &config, &cli).await + } None => {} }; } diff --git a/kuma-client/Cargo.toml b/kuma-client/Cargo.toml index 922f654..e94be17 100644 --- a/kuma-client/Cargo.toml +++ b/kuma-client/Cargo.toml @@ -9,6 +9,7 @@ derivative = { workspace = true } futures-util = { workspace = true } itertools = { workspace = true } log = { workspace = true } +reqwest = { workspace = true } rust_socketio = { workspace = true } serde = { workspace = true } serde_alias = { workspace = true } @@ -22,6 +23,7 @@ strum = { workspace = true } thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true } +url = { workspace = true } [build-dependencies] shadow-rs = { workspace = true } diff --git a/kuma-client/src/client.rs b/kuma-client/src/client.rs index 3ce32ec..f3887b2 100644 --- a/kuma-client/src/client.rs +++ b/kuma-client/src/client.rs @@ -4,11 +4,12 @@ use super::{ }; use crate::{ maintenance::{Maintenance, MaintenanceList, MaintenanceMonitor, MaintenanceStatusPage}, - Notification, NotificationList, + Notification, NotificationList, PublicGroupList, StatusPage, StatusPageList, }; use futures_util::FutureExt; use itertools::Itertools; use log::{debug, trace, warn}; +use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use rust_socketio::{ asynchronous::{Client as SocketIO, ClientBuilder}, Event as SocketIOEvent, Payload, @@ -22,6 +23,7 @@ struct Ready { pub monitor_list: bool, pub notification_list: bool, pub maintenance_list: bool, + pub status_page_list: bool, } impl Ready { @@ -30,6 +32,7 @@ impl Ready { monitor_list: false, notification_list: false, maintenance_list: false, + status_page_list: false, } } @@ -38,7 +41,10 @@ impl Ready { } pub fn is_ready(&self) -> bool { - self.monitor_list && self.notification_list + self.monitor_list + && self.notification_list + && self.maintenance_list + && self.status_page_list } } @@ -48,22 +54,45 @@ struct Worker { monitors: Arc>, notifications: Arc>, maintenances: Arc>, + status_pages: Arc>, is_connected: Arc>, is_ready: Arc>, is_logged_in: Arc>, + reqwest: Arc>, } impl Worker { fn new(config: Config) -> Arc { Arc::new(Worker { - config: Arc::new(config), + config: Arc::new(config.clone()), socket_io: Arc::new(Mutex::new(None)), monitors: Default::default(), notifications: Default::default(), maintenances: Default::default(), + status_pages: Default::default(), is_connected: Arc::new(Mutex::new(false)), is_ready: Arc::new(Mutex::new(Ready::new())), is_logged_in: Arc::new(Mutex::new(false)), + reqwest: Arc::new(Mutex::new( + reqwest::Client::builder() + .default_headers(HeaderMap::from_iter( + config + .headers + .iter() + .filter_map(|header| header.split_once("=")) + .filter_map(|(key, value)| { + match ( + HeaderName::from_bytes(key.as_bytes()), + HeaderValue::from_bytes(value.as_bytes()), + ) { + (Ok(key), Ok(value)) => Some((key, value)), + _ => None, + } + }), + )) + .build() + .unwrap(), + )), }) } @@ -94,6 +123,13 @@ impl Worker { Ok(()) } + async fn on_status_page_list(self: &Arc, status_page_list: StatusPageList) -> Result<()> { + *self.status_pages.lock().await = status_page_list; + self.is_ready.lock().await.status_page_list = true; + + Ok(()) + } + async fn on_info(self: &Arc) -> Result<()> { *self.is_connected.lock().await = true; if let (Some(username), Some(password), true) = ( @@ -128,6 +164,10 @@ impl Worker { self.on_maintenance_list(serde_json::from_value(payload).unwrap()) .await? } + Event::StatusPageList => { + self.on_status_page_list(serde_json::from_value(payload).unwrap()) + .await? + } Event::Info => self.on_info().await?, Event::AutoLogin => self.on_auto_login().await?, _ => {} @@ -786,13 +826,119 @@ impl Worker { Ok(()) } + async fn get_public_group_list(self: &Arc, slug: &str) -> Result { + let response: Value = self + .reqwest + .lock() + .await + .get( + self.config + .url + .join(&format!("/api/status-page/{}", slug)) + .map_err(|e| Error::InvalidUrl(e.to_string()))?, + ) + .send() + .await? + .json() + .await?; + + let monitor_list = response + .clone() + .pointer("/publicGroupList") + .ok_or_else(|| { + Error::InvalidResponse(vec![response.clone()], "/publicGroupList".to_owned()) + })? + .clone(); + + Ok(serde_json::from_value(monitor_list) + .log_warn(|e| e.to_string()) + .map_err(|_| Error::UnsupportedResponse)?) + } + + pub async fn delete_status_page(self: &Arc, slug: &str) -> Result<()> { + let _: bool = self + .call("deleteStatusPage", vec![json!(slug)], "/ok", true) + .await?; + + Ok(()) + } + + pub async fn add_status_page(self: &Arc, status_page: &mut StatusPage) -> Result<()> { + let ok: bool = self + .call( + "addStatusPage", + vec![ + serde_json::to_value(status_page.title.clone()).unwrap(), + serde_json::to_value(status_page.slug.clone()).unwrap(), + ], + "/ok", + true, + ) + .await?; + + if !ok { + return Err(Error::ServerError("Unable to add status page".to_owned())); + } + + self.edit_status_page(status_page).await?; + + Ok(()) + } + + pub async fn get_status_page(self: &Arc, slug: &str) -> Result { + let mut status_page: StatusPage = self + .call( + "getStatusPage", + vec![serde_json::to_value(slug).unwrap()], + "/config", + true, + ) + .await + .map_err(|e| match e { + Error::ServerError(msg) if msg.contains("Cannot read properties of null") => { + Error::SlugNotFound("StatusPage".to_owned(), slug.to_owned()) + } + _ => e, + })?; + + status_page.public_group_list = Some( + self.get_public_group_list(&status_page.slug.clone().unwrap_or_default()) + .await?, + ); + + Ok(status_page) + } + + pub async fn edit_status_page(self: &Arc, status_page: &mut StatusPage) -> Result<()> { + let _: bool = self + .call( + "saveStatusPage", + vec![ + serde_json::to_value(status_page.slug.clone()).unwrap(), + serde_json::to_value(status_page.clone()).unwrap(), + serde_json::to_value(status_page.icon.clone()).unwrap(), + serde_json::to_value(status_page.public_group_list.clone()).unwrap(), + ], + "/ok", + true, + ) + .await?; + + Ok(()) + } + pub async fn connect(self: &Arc) -> Result<()> { self.is_ready.lock().await.reset(); *self.is_logged_in.lock().await = false; *self.socket_io.lock().await = None; - let mut builder = ClientBuilder::new(self.config.url.clone()) - .transport_type(rust_socketio::TransportType::Websocket); + let mut builder = ClientBuilder::new( + self.config + .url + .join("/socket.io/") + .map_err(|e| Error::InvalidUrl(e.to_string()))?, + ) + .transport_type(rust_socketio::TransportType::Websocket); for (key, value) in self .config @@ -1028,6 +1174,31 @@ impl Client { self.worker.resume_maintenance(maintenance_id).await } + pub async fn get_status_pages(&self) -> Result { + match self.worker.is_ready().await { + true => Ok(self.worker.status_pages.lock().await.clone()), + false => Err(Error::NotReady), + } + } + + pub async fn get_status_page(&self, slug: &str) -> Result { + self.worker.get_status_page(slug).await + } + + pub async fn add_status_page(&self, mut status_page: StatusPage) -> Result { + self.worker.add_status_page(&mut status_page).await?; + Ok(status_page) + } + + pub async fn edit_status_page(&self, mut status_page: StatusPage) -> Result { + self.worker.edit_status_page(&mut status_page).await?; + Ok(status_page) + } + + pub async fn delete_status_page(&self, slug: &str) -> Result<()> { + self.worker.delete_status_page(slug).await + } + pub async fn disconnect(&self) -> Result<()> { self.worker.disconnect().await } diff --git a/kuma-client/src/config.rs b/kuma-client/src/config.rs index f984be3..05898e0 100644 --- a/kuma-client/src/config.rs +++ b/kuma-client/src/config.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_alias::serde_alias; use serde_inline_default::serde_inline_default; use serde_with::{formats::CommaSeparator, serde_as, PickFirst, StringWithSeparator}; +use url::Url; #[serde_alias(ScreamingSnakeCase)] #[serde_inline_default] @@ -10,7 +11,7 @@ use serde_with::{formats::CommaSeparator, serde_as, PickFirst, StringWithSeparat #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Config { /// The URL for connecting to Uptime Kuma. - pub url: String, + pub url: Url, /// The username for logging into Uptime Kuma (required unless auth is disabled). . pub username: Option, diff --git a/kuma-client/src/error.rs b/kuma-client/src/error.rs index 2785e29..eff7bd9 100644 --- a/kuma-client/src/error.rs +++ b/kuma-client/src/error.rs @@ -2,6 +2,9 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum Error { + #[error("Invalid url: {0}")] + InvalidUrl(String), + #[error("Timeout while trying to connect to Uptime Kuma server")] ConnectionTimeout, @@ -40,6 +43,12 @@ pub enum Error { #[error("No {0} with id {1} could be found")] IdNotFound(String, i32), + + #[error("No {0} with slug {1} could be found")] + SlugNotFound(String, String), + + #[error(transparent)] + Reqwest(#[from] reqwest::Error), } pub type Result = std::result::Result; diff --git a/kuma-client/src/models/maintenance.rs b/kuma-client/src/models/maintenance.rs index 33dfabb..fdfce79 100644 --- a/kuma-client/src/models/maintenance.rs +++ b/kuma-client/src/models/maintenance.rs @@ -1,5 +1,3 @@ -use std::{collections::HashMap, fmt}; - use crate::deserialize::{ DeserializeBoolLenient, DeserializeNumberLenient, SerializeDateRange, SerializeTimeRange, }; @@ -12,6 +10,7 @@ use serde_inline_default::serde_inline_default; use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_with::{serde_as, skip_serializing_none}; +use std::{collections::HashMap, fmt}; use time::{PrimitiveDateTime, Time}; include!(concat!(env!("OUT_DIR"), "/timezones.rs")); diff --git a/kuma-client/src/models/mod.rs b/kuma-client/src/models/mod.rs index 87d7362..6dd1b65 100644 --- a/kuma-client/src/models/mod.rs +++ b/kuma-client/src/models/mod.rs @@ -3,10 +3,13 @@ pub mod maintenance; pub mod monitor; pub mod notification; pub mod response; +pub mod status_page; pub mod tag; pub use event::*; +pub use maintenance::*; pub use monitor::*; pub use notification::*; pub use response::*; +pub use status_page::*; pub use tag::*; diff --git a/kuma-client/src/models/status_page.rs b/kuma-client/src/models/status_page.rs new file mode 100644 index 0000000..e6baa4b --- /dev/null +++ b/kuma-client/src/models/status_page.rs @@ -0,0 +1,118 @@ +use crate::{ + deserialize::{DeserializeBoolLenient, DeserializeNumberLenient}, + MonitorType, +}; +use serde::{Deserialize, Serialize}; +use serde_inline_default::serde_inline_default; +use serde_with::{serde_as, skip_serializing_none}; +use std::collections::HashMap; + +#[serde_inline_default] +#[skip_serializing_none] +#[serde_as] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PublicGroupMonitor { + #[serde(rename = "id")] + #[serde_as(as = "Option")] + pub id: Option, + + #[serde(rename = "name")] + pub name: Option, + + #[serde(rename = "weight")] + #[serde_as(as = "Option")] + pub weight: Option, + + #[serde(rename = "type")] + pub monitor_type: Option, +} + +#[serde_inline_default] +#[skip_serializing_none] +#[serde_as] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PublicGroup { + #[serde(rename = "id")] + #[serde_as(as = "Option")] + pub id: Option, + + #[serde(rename = "name")] + pub name: Option, + + #[serde(rename = "weight")] + #[serde_as(as = "Option")] + pub weight: Option, + + #[serde(rename = "monitorList", default)] + pub monitor_list: PublicGroupMonitorList, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Theme { + #[serde(rename = "auto")] + Auto, + #[serde(rename = "light")] + Light, + #[serde(rename = "dark")] + Dark, +} + +#[serde_inline_default] +#[skip_serializing_none] +#[serde_as] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct StatusPage { + #[serde(rename = "id")] + #[serde_as(as = "Option")] + pub id: Option, + + #[serde(rename = "slug")] + pub slug: Option, + + #[serde(rename = "title")] + pub title: Option, + + #[serde(rename = "description")] + pub description: Option, + + #[serde(rename = "icon")] + pub icon: Option, + + #[serde(rename = "theme")] + pub theme: Option, + + #[serde(rename = "published")] + #[serde_as(as = "Option")] + pub published: Option, + + #[serde(rename = "showTags")] + #[serde_as(as = "Option")] + pub show_tags: Option, + + #[serde(rename = "domainNameList", default)] + pub domain_name_list: Vec, + + #[serde(rename = "customCSS")] + pub custom_css: Option, + + #[serde(rename = "footerText")] + pub footer_text: Option, + + #[serde(rename = "showPoweredBy")] + #[serde_as(as = "Option")] + pub show_powered_by: Option, + + #[serde(rename = "googleAnalyticsId")] + pub google_analytics_id: Option, + + #[serde(rename = "showCertificateExpiry")] + #[serde_as(as = "Option")] + pub show_certificate_expiry: Option, + + #[serde(rename = "publicGroupList")] + pub public_group_list: Option, +} + +pub type StatusPageList = HashMap; +pub type PublicGroupList = Vec; +pub type PublicGroupMonitorList = Vec;