From: Lukas Wagner <l.wagner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v3 proxmox 13/66] notify: add template rendering
Date: Mon, 17 Jul 2023 16:59:58 +0200 [thread overview]
Message-ID: <20230717150051.710464-14-l.wagner@proxmox.com> (raw)
In-Reply-To: <20230717150051.710464-1-l.wagner@proxmox.com>
This commit adds template rendering to the `proxmox-notify` crate, based
on the `handlebars` crate.
Title and body of a notification are rendered using any `properties`
passed along with the notification. There are also a few helpers,
allowing to render tables from `serde_json::Value`.
'Value' renderers. These can also be used in table cells using the
'renderer' property in a table schema:
- {{human-bytes val}}
Render bytes with human-readable units (base 2)
- {{duration val}}
Render a duration (based on seconds)
- {{timestamp val}}
Render a unix-epoch (based on seconds)
There are also a few 'block-level' helpers.
- {{table val}}
Render a table from given val (containing a schema for the columns,
as well as the table data)
- {{object val}}
Render a value as a pretty-printed json
- {{heading_1 val}}
Render a top-level heading
- {{heading_2 val}}
Render a not-so-top-level heading
- {{verbatim val}} or {{/verbatim}}<content>{{#verbatim}}
Do not reflow text. NOP for plain text, but for HTML output the text
will be contained in a <pre> with a regular font.
- {{verbatim-monospaced val}} or
{{/verbatim-monospaced}}<content>{{#verbatim-monospaced}}
Do not reflow text. NOP for plain text, but for HTML output the text
will be contained in a <pre> with a monospaced font.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
Cargo.toml | 1 +
proxmox-notify/Cargo.toml | 6 +-
proxmox-notify/src/endpoints/gotify.rs | 40 ++-
proxmox-notify/src/endpoints/sendmail.rs | 26 +-
proxmox-notify/src/lib.rs | 6 +-
proxmox-notify/src/renderer/html.rs | 100 +++++++
proxmox-notify/src/renderer/mod.rs | 366 +++++++++++++++++++++++
proxmox-notify/src/renderer/plaintext.rs | 141 +++++++++
proxmox-notify/src/renderer/table.rs | 24 ++
9 files changed, 684 insertions(+), 26 deletions(-)
create mode 100644 proxmox-notify/src/renderer/html.rs
create mode 100644 proxmox-notify/src/renderer/mod.rs
create mode 100644 proxmox-notify/src/renderer/plaintext.rs
create mode 100644 proxmox-notify/src/renderer/table.rs
diff --git a/Cargo.toml b/Cargo.toml
index ef8a050a..c30131fe 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -88,6 +88,7 @@ proxmox-api-macro = { version = "1.0.4", path = "proxmox-api-macro" }
proxmox-async = { version = "0.4.1", path = "proxmox-async" }
proxmox-compression = { version = "0.2.0", path = "proxmox-compression" }
proxmox-http = { version = "0.9.0", path = "proxmox-http" }
+proxmox-human-byte = { version = "0.1.0", path = "proxmox-human-byte" }
proxmox-io = { version = "1.0.0", path = "proxmox-io" }
proxmox-lang = { version = "1.1", path = "proxmox-lang" }
proxmox-rest-server = { version = "0.4.0", path = "proxmox-rest-server" }
diff --git a/proxmox-notify/Cargo.toml b/proxmox-notify/Cargo.toml
index 738674ae..a635798b 100644
--- a/proxmox-notify/Cargo.toml
+++ b/proxmox-notify/Cargo.toml
@@ -8,19 +8,21 @@ repository.workspace = true
exclude.workspace = true
[dependencies]
-handlebars = { workspace = true, optional = true }
+handlebars = { workspace = true }
lazy_static.workspace = true
log.workspace = true
openssl.workspace = true
proxmox-http = { workspace = true, features = ["client-sync"], optional = true }
+proxmox-human-byte.workspace = true
proxmox-schema = { workspace = true, features = ["api-macro"]}
proxmox-section-config = { workspace = true }
proxmox-sys = { workspace = true, optional = true }
+proxmox-time.workspace = true
regex.workspace = true
serde = { workspace = true, features = ["derive"]}
serde_json.workspace = true
[features]
default = ["sendmail", "gotify"]
-sendmail = ["dep:handlebars", "dep:proxmox-sys"]
+sendmail = ["dep:proxmox-sys"]
gotify = ["dep:proxmox-http"]
diff --git a/proxmox-notify/src/endpoints/gotify.rs b/proxmox-notify/src/endpoints/gotify.rs
index 6c42100e..b278b90d 100644
--- a/proxmox-notify/src/endpoints/gotify.rs
+++ b/proxmox-notify/src/endpoints/gotify.rs
@@ -1,21 +1,16 @@
use std::collections::HashMap;
+use crate::renderer::TemplateRenderer;
use crate::schema::{COMMENT_SCHEMA, ENTITY_NAME_SCHEMA};
-use crate::{Endpoint, Error, Notification, Severity};
+use crate::{renderer, Endpoint, Error, Notification, Severity};
use serde::{Deserialize, Serialize};
+use serde_json::json;
use proxmox_http::client::sync::Client;
use proxmox_http::{HttpClient, HttpOptions};
use proxmox_schema::{api, Updater};
-#[derive(Serialize)]
-struct GotifyMessageBody<'a> {
- title: &'a str,
- message: &'a str,
- priority: u32,
-}
-
fn severity_to_priority(level: Severity) -> u32 {
match level {
Severity::Info => 1,
@@ -93,11 +88,30 @@ impl Endpoint for GotifyEndpoint {
let uri = format!("{}/message", self.config.server);
- let body = GotifyMessageBody {
- title: ¬ification.title,
- message: ¬ification.body,
- priority: severity_to_priority(notification.severity),
- };
+ let properties = notification.properties.as_ref();
+
+ let title = renderer::render_template(
+ TemplateRenderer::Plaintext,
+ ¬ification.title,
+ properties,
+ )?;
+ let message =
+ renderer::render_template(TemplateRenderer::Plaintext, ¬ification.body, properties)?;
+
+ // We don't have a TemplateRenderer::Markdown yet, so simply put everything
+ // in code tags. Otherwise tables etc. are not formatted properly
+ let message = format!("```\n{message}\n```");
+
+ let body = json!({
+ "title": &title,
+ "message": &message,
+ "priority": severity_to_priority(notification.severity),
+ "extras": {
+ "client::display": {
+ "contentType": "text/markdown"
+ }
+ }
+ });
let body = serde_json::to_vec(&body)
.map_err(|err| Error::NotifyFailed(self.name().to_string(), err.into()))?;
diff --git a/proxmox-notify/src/endpoints/sendmail.rs b/proxmox-notify/src/endpoints/sendmail.rs
index fcac6248..9d06e7c4 100644
--- a/proxmox-notify/src/endpoints/sendmail.rs
+++ b/proxmox-notify/src/endpoints/sendmail.rs
@@ -1,5 +1,6 @@
+use crate::renderer::TemplateRenderer;
use crate::schema::{COMMENT_SCHEMA, EMAIL_SCHEMA, ENTITY_NAME_SCHEMA};
-use crate::{Endpoint, Error, Notification};
+use crate::{renderer, Endpoint, Error, Notification};
use proxmox_schema::{api, Updater};
use serde::{Deserialize, Serialize};
@@ -68,12 +69,17 @@ impl Endpoint for SendmailEndpoint {
fn send(&self, notification: &Notification) -> Result<(), Error> {
let recipients: Vec<&str> = self.config.mailto.iter().map(String::as_str).collect();
- // Note: OX has serious problems displaying text mails,
- // so we include html as well
- let html = format!(
- "<html><body><pre>\n{}\n<pre>",
- handlebars::html_escape(¬ification.body)
- );
+ let properties = notification.properties.as_ref();
+
+ let subject = renderer::render_template(
+ TemplateRenderer::Plaintext,
+ ¬ification.title,
+ properties,
+ )?;
+ let html_part =
+ renderer::render_template(TemplateRenderer::Html, ¬ification.body, properties)?;
+ let text_part =
+ renderer::render_template(TemplateRenderer::Plaintext, ¬ification.body, properties)?;
// proxmox_sys::email::sendmail will set the author to
// "Proxmox Backup Server" if it is not set.
@@ -81,9 +87,9 @@ impl Endpoint for SendmailEndpoint {
proxmox_sys::email::sendmail(
&recipients,
- ¬ification.title,
- Some(¬ification.body),
- Some(&html),
+ &subject,
+ Some(&text_part),
+ Some(&html_part),
self.config.from_address.as_deref(),
author,
)
diff --git a/proxmox-notify/src/lib.rs b/proxmox-notify/src/lib.rs
index 5d408c85..548cc56f 100644
--- a/proxmox-notify/src/lib.rs
+++ b/proxmox-notify/src/lib.rs
@@ -14,8 +14,9 @@ use std::error::Error as StdError;
pub mod api;
mod config;
pub mod endpoints;
-mod filter;
+pub mod filter;
pub mod group;
+pub mod renderer;
pub mod schema;
#[derive(Debug)]
@@ -26,6 +27,7 @@ pub enum Error {
TargetDoesNotExist(String),
TargetTestFailed(Vec<Box<dyn StdError + Send + Sync + 'static>>),
FilterFailed(String),
+ RenderError(Box<dyn StdError + Send + Sync + 'static>),
}
impl Display for Error {
@@ -53,6 +55,7 @@ impl Display for Error {
Error::FilterFailed(message) => {
write!(f, "could not apply filter: {message}")
}
+ Error::RenderError(err) => write!(f, "could not render notification template: {err}"),
}
}
}
@@ -66,6 +69,7 @@ impl StdError for Error {
Error::TargetDoesNotExist(_) => None,
Error::TargetTestFailed(errs) => Some(&*errs[0]),
Error::FilterFailed(_) => None,
+ Error::RenderError(err) => Some(&**err),
}
}
}
diff --git a/proxmox-notify/src/renderer/html.rs b/proxmox-notify/src/renderer/html.rs
new file mode 100644
index 00000000..7a41e873
--- /dev/null
+++ b/proxmox-notify/src/renderer/html.rs
@@ -0,0 +1,100 @@
+use crate::define_helper_with_prefix_and_postfix;
+use crate::renderer::BlockRenderFunctions;
+use handlebars::{
+ Context, Handlebars, Helper, HelperResult, Output, RenderContext,
+ RenderError as HandlebarsRenderError,
+};
+use serde_json::Value;
+
+use super::{table::Table, value_to_string};
+
+fn render_html_table(
+ h: &Helper,
+ _: &Handlebars,
+ _: &Context,
+ _: &mut RenderContext,
+ out: &mut dyn Output,
+) -> HelperResult {
+ let param = h
+ .param(0)
+ .ok_or_else(|| HandlebarsRenderError::new("parameter not found"))?;
+
+ let value = param.value();
+
+ let table: Table = serde_json::from_value(value.clone())?;
+
+ out.write("<table style=\"border: 1px solid\";border-style=\"collapse\">\n")?;
+
+ // Write header
+ out.write(" <tr>\n")?;
+ for column in &table.schema.columns {
+ out.write(" <th style=\"border: 1px solid\">")?;
+ out.write(&handlebars::html_escape(&column.label))?;
+ out.write("</th>\n")?;
+ }
+ out.write(" </tr>\n")?;
+
+ // Write individual rows
+ for row in &table.data {
+ out.write(" <tr>\n")?;
+
+ for column in &table.schema.columns {
+ let entry = row.get(&column.id).unwrap_or(&Value::Null);
+
+ let text = if let Some(renderer) = &column.renderer {
+ renderer.render(entry)?
+ } else {
+ value_to_string(entry)
+ };
+
+ out.write(" <td style=\"border: 1px solid\">")?;
+ out.write(&handlebars::html_escape(&text))?;
+ out.write("</td>\n")?;
+ }
+ out.write(" </tr>\n")?;
+ }
+
+ out.write("</table>\n")?;
+
+ Ok(())
+}
+
+fn render_object(
+ h: &Helper,
+ _: &Handlebars,
+ _: &Context,
+ _: &mut RenderContext,
+ out: &mut dyn Output,
+) -> HelperResult {
+ let param = h
+ .param(0)
+ .ok_or_else(|| HandlebarsRenderError::new("parameter not found"))?;
+
+ let value = param.value();
+
+ out.write("\n<pre>")?;
+ out.write(&serde_json::to_string_pretty(&value)?)?;
+ out.write("\n</pre>\n")?;
+
+ Ok(())
+}
+
+define_helper_with_prefix_and_postfix!(verbatim_monospaced, "<pre>", "</pre>");
+define_helper_with_prefix_and_postfix!(heading_1, "<h1 style=\"font-size: 1.2em\">", "</h1>");
+define_helper_with_prefix_and_postfix!(heading_2, "<h2 style=\"font-size: 1em\">", "</h2>");
+define_helper_with_prefix_and_postfix!(
+ verbatim,
+ "<pre style=\"font-family: sans-serif\">",
+ "</pre>"
+);
+
+pub(super) fn block_render_functions() -> BlockRenderFunctions {
+ BlockRenderFunctions {
+ table: Box::new(render_html_table),
+ verbatim_monospaced: Box::new(verbatim_monospaced),
+ object: Box::new(render_object),
+ heading_1: Box::new(heading_1),
+ heading_2: Box::new(heading_2),
+ verbatim: Box::new(verbatim),
+ }
+}
diff --git a/proxmox-notify/src/renderer/mod.rs b/proxmox-notify/src/renderer/mod.rs
new file mode 100644
index 00000000..2cf64a9f
--- /dev/null
+++ b/proxmox-notify/src/renderer/mod.rs
@@ -0,0 +1,366 @@
+//! Module for rendering notification templates.
+
+use handlebars::{
+ Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext,
+ RenderError as HandlebarsRenderError,
+};
+use std::time::Duration;
+
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+use crate::Error;
+use proxmox_human_byte::HumanByte;
+use proxmox_time::TimeSpan;
+
+mod html;
+mod plaintext;
+mod table;
+
+/// Convert a serde_json::Value to a String.
+///
+/// The main difference between this and simply calling Value::to_string is that
+/// this will print strings without double quotes
+fn value_to_string(value: &Value) -> String {
+ match value {
+ Value::String(s) => s.clone(),
+ v => v.to_string(),
+ }
+}
+
+/// Render a serde_json::Value as a byte size with proper units (IEC, base 2)
+///
+/// Will return `None` if `val` does not contain a number.
+fn value_to_byte_size(val: &Value) -> Option<String> {
+ let size = val.as_f64()?;
+ Some(format!("{}", HumanByte::new_binary(size)))
+}
+
+/// Render a serde_json::Value as a duration.
+/// The value is expected to contain the duration in seconds.
+///
+/// Will return `None` if `val` does not contain a number.
+fn value_to_duration(val: &Value) -> Option<String> {
+ let duration = val.as_u64()?;
+ let time_span = TimeSpan::from(Duration::from_secs(duration));
+
+ Some(format!("{time_span}"))
+}
+
+/// Render as serde_json::Value as a timestamp.
+/// The value is expected to contain the timestamp as a unix epoch.
+///
+/// Will return `None` if `val` does not contain a number.
+fn value_to_timestamp(val: &Value) -> Option<String> {
+ let timestamp = val.as_i64()?;
+ proxmox_time::strftime_local("%F %H:%M:%S", timestamp).ok()
+}
+
+/// Available render functions for `serde_json::Values``
+///
+/// May be used as a handlebars helper, e.g.
+/// ```text
+/// {{human-bytes 1024}}
+/// ```
+///
+/// Value renderer can also be used for rendering values in table columns:
+/// ```text
+/// let properties = json!({
+/// "table": {
+/// "schema": {
+/// "columns": [
+/// {
+/// "label": "Size",
+/// "id": "size",
+/// "renderer": "human-bytes"
+/// }
+/// ],
+/// },
+/// "data" : [
+/// {
+/// "size": 1024 * 1024,
+/// },
+/// ]
+/// }
+/// });
+/// ```
+///
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum ValueRenderFunction {
+ HumanBytes,
+ Duration,
+ Timestamp,
+}
+
+impl ValueRenderFunction {
+ fn render(&self, value: &Value) -> Result<String, HandlebarsRenderError> {
+ match self {
+ ValueRenderFunction::HumanBytes => value_to_byte_size(value),
+ ValueRenderFunction::Duration => value_to_duration(value),
+ ValueRenderFunction::Timestamp => value_to_timestamp(value),
+ }
+ .ok_or_else(|| {
+ HandlebarsRenderError::new(format!(
+ "could not render value {value} with renderer {self:?}"
+ ))
+ })
+ }
+
+ fn register_helpers(handlebars: &mut Handlebars) {
+ ValueRenderFunction::HumanBytes.register_handlebars_helper(handlebars);
+ ValueRenderFunction::Duration.register_handlebars_helper(handlebars);
+ ValueRenderFunction::Timestamp.register_handlebars_helper(handlebars);
+ }
+
+ fn register_handlebars_helper(&'static self, handlebars: &mut Handlebars) {
+ // Use serde to get own kebab-case representation that is later used
+ // to register the helper, e.g. HumanBytes -> human-bytes
+ let tag = serde_json::to_string(self)
+ .expect("serde failed to serialize ValueRenderFunction enum");
+
+ // But as it's a string value, the generated string is quoted,
+ // so remove leading/trailing double quotes
+ let tag = tag
+ .strip_prefix('\"')
+ .and_then(|t| t.strip_suffix('\"'))
+ .expect("serde serialized string representation was not contained in double quotes");
+
+ handlebars.register_helper(
+ tag,
+ Box::new(
+ |h: &Helper,
+ _r: &Handlebars,
+ _: &Context,
+ _rc: &mut RenderContext,
+ out: &mut dyn Output|
+ -> HelperResult {
+ let param = h
+ .param(0)
+ .ok_or(HandlebarsRenderError::new("parameter not found"))?;
+
+ let value = param.value();
+ out.write(&self.render(value)?)?;
+
+ Ok(())
+ },
+ ),
+ );
+ }
+}
+
+/// Available renderers for notification templates.
+#[derive(Copy, Clone)]
+pub enum TemplateRenderer {
+ /// Render to HTML code
+ Html,
+ /// Render to plain text
+ Plaintext,
+}
+
+impl TemplateRenderer {
+ fn prefix(&self) -> &str {
+ match self {
+ TemplateRenderer::Html => "<html>\n<body>\n",
+ TemplateRenderer::Plaintext => "",
+ }
+ }
+
+ fn postfix(&self) -> &str {
+ match self {
+ TemplateRenderer::Html => "\n</body>\n</html>",
+ TemplateRenderer::Plaintext => "",
+ }
+ }
+
+ fn block_render_fns(&self) -> BlockRenderFunctions {
+ match self {
+ TemplateRenderer::Html => html::block_render_functions(),
+ TemplateRenderer::Plaintext => plaintext::block_render_functions(),
+ }
+ }
+
+ fn escape_fn(&self) -> fn(&str) -> String {
+ match self {
+ TemplateRenderer::Html => handlebars::html_escape,
+ TemplateRenderer::Plaintext => handlebars::no_escape,
+ }
+ }
+}
+
+type HelperFn = dyn HelperDef + Send + Sync;
+
+struct BlockRenderFunctions {
+ table: Box<HelperFn>,
+ verbatim_monospaced: Box<HelperFn>,
+ object: Box<HelperFn>,
+ heading_1: Box<HelperFn>,
+ heading_2: Box<HelperFn>,
+ verbatim: Box<HelperFn>,
+}
+
+impl BlockRenderFunctions {
+ fn register_helpers(self, handlebars: &mut Handlebars) {
+ handlebars.register_helper("table", self.table);
+ handlebars.register_helper("verbatim", self.verbatim);
+ handlebars.register_helper("verbatim-monospaced", self.verbatim_monospaced);
+ handlebars.register_helper("object", self.object);
+ handlebars.register_helper("heading-1", self.heading_1);
+ handlebars.register_helper("heading-2", self.heading_2);
+ }
+}
+
+fn render_template_impl(
+ template: &str,
+ properties: Option<&Value>,
+ renderer: TemplateRenderer,
+) -> Result<String, Error> {
+ let properties = properties.unwrap_or(&Value::Null);
+
+ let mut handlebars = Handlebars::new();
+ handlebars.register_escape_fn(renderer.escape_fn());
+
+ let block_render_fns = renderer.block_render_fns();
+ block_render_fns.register_helpers(&mut handlebars);
+
+ ValueRenderFunction::register_helpers(&mut handlebars);
+
+ let rendered_template = handlebars
+ .render_template(template, properties)
+ .map_err(|err| Error::RenderError(err.into()))?;
+
+ Ok(rendered_template)
+}
+
+/// Render a template string.
+///
+/// The output format can be chosen via the `renderer` parameter (see [TemplateRenderer]
+/// for available options).
+pub fn render_template(
+ renderer: TemplateRenderer,
+ template: &str,
+ properties: Option<&Value>,
+) -> Result<String, Error> {
+ let mut rendered_template = String::from(renderer.prefix());
+
+ rendered_template.push_str(&render_template_impl(template, properties, renderer)?);
+ rendered_template.push_str(renderer.postfix());
+
+ Ok(rendered_template)
+}
+
+#[macro_export]
+macro_rules! define_helper_with_prefix_and_postfix {
+ ($name:ident, $pre:expr, $post:expr) => {
+ fn $name<'reg, 'rc>(
+ h: &Helper<'reg, 'rc>,
+ handlebars: &'reg Handlebars,
+ context: &'rc Context,
+ render_context: &mut RenderContext<'reg, 'rc>,
+ out: &mut dyn Output,
+ ) -> HelperResult {
+ use handlebars::Renderable;
+
+ let block_text = h.template();
+ let param = h.param(0);
+
+ out.write($pre)?;
+ match (param, block_text) {
+ (None, Some(block_text)) => {
+ block_text.render(handlebars, context, render_context, out)
+ }
+ (Some(param), None) => {
+ let value = param.value();
+ let text = value.as_str().ok_or_else(|| {
+ HandlebarsRenderError::new(format!("value {value} is not a string"))
+ })?;
+
+ out.write(text)?;
+ Ok(())
+ }
+ (Some(_), Some(_)) => Err(HandlebarsRenderError::new(
+ "Cannot use parameter and template at the same time",
+ )),
+ (None, None) => Err(HandlebarsRenderError::new(
+ "Neither parameter nor template was provided",
+ )),
+ }?;
+ out.write($post)?;
+ Ok(())
+ }
+ };
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ #[test]
+ fn test_render_template() -> Result<(), Error> {
+ let properties = json!({
+ "dur": 12345,
+ "size": 1024 * 15,
+
+ "table": {
+ "schema": {
+ "columns": [
+ {
+ "id": "col1",
+ "label": "Column 1"
+ },
+ {
+ "id": "col2",
+ "label": "Column 2"
+ }
+ ]
+ },
+ "data": [
+ {
+ "col1": "val1",
+ "col2": "val2"
+ },
+ {
+ "col1": "val3",
+ "col2": "val4"
+ },
+ ]
+ }
+
+ });
+
+ let template = r#"
+{{heading-1 "Hello World"}}
+
+{{heading-2 "Hello World"}}
+
+{{human-bytes size}}
+{{duration dur}}
+
+{{table table}}"#;
+
+ let expected_plaintext = r#"
+Hello World
+===========
+
+Hello World
+-----------
+
+15 KiB
+3h 25min 45s
+
+Column 1 Column 2
+val1 val2
+val3 val4
+"#;
+
+ let rendered_plaintext =
+ render_template(TemplateRenderer::Plaintext, template, Some(&properties))?;
+
+ // Let's not bother about testing the HTML output, too fragile.
+
+ assert_eq!(rendered_plaintext, expected_plaintext);
+
+ Ok(())
+ }
+}
diff --git a/proxmox-notify/src/renderer/plaintext.rs b/proxmox-notify/src/renderer/plaintext.rs
new file mode 100644
index 00000000..58c51599
--- /dev/null
+++ b/proxmox-notify/src/renderer/plaintext.rs
@@ -0,0 +1,141 @@
+use crate::define_helper_with_prefix_and_postfix;
+use crate::renderer::BlockRenderFunctions;
+use handlebars::{
+ Context, Handlebars, Helper, HelperResult, Output, RenderContext,
+ RenderError as HandlebarsRenderError,
+};
+use serde_json::Value;
+use std::collections::HashMap;
+
+use super::{table::Table, value_to_string};
+
+fn optimal_column_widths(table: &Table) -> HashMap<&str, usize> {
+ let mut widths = HashMap::new();
+
+ for column in &table.schema.columns {
+ let mut min_width = column.label.len();
+
+ for row in &table.data {
+ let entry = row.get(&column.id).unwrap_or(&Value::Null);
+
+ let text = if let Some(renderer) = &column.renderer {
+ renderer.render(entry).unwrap_or_default()
+ } else {
+ value_to_string(entry)
+ };
+
+ min_width = std::cmp::max(text.len(), min_width);
+ }
+
+ widths.insert(column.label.as_str(), min_width + 4);
+ }
+
+ widths
+}
+
+fn render_plaintext_table(
+ h: &Helper,
+ _: &Handlebars,
+ _: &Context,
+ _: &mut RenderContext,
+ out: &mut dyn Output,
+) -> HelperResult {
+ let param = h
+ .param(0)
+ .ok_or_else(|| HandlebarsRenderError::new("parameter not found"))?;
+ let value = param.value();
+ let table: Table = serde_json::from_value(value.clone())?;
+ let widths = optimal_column_widths(&table);
+
+ // Write header
+ for column in &table.schema.columns {
+ let width = widths.get(column.label.as_str()).unwrap_or(&0);
+ out.write(&format!("{label:width$}", label = column.label))?;
+ }
+
+ out.write("\n")?;
+
+ // Write individual rows
+ for row in &table.data {
+ for column in &table.schema.columns {
+ let entry = row.get(&column.id).unwrap_or(&Value::Null);
+ let width = widths.get(column.label.as_str()).unwrap_or(&0);
+
+ let text = if let Some(renderer) = &column.renderer {
+ renderer.render(entry)?
+ } else {
+ value_to_string(entry)
+ };
+
+ out.write(&format!("{text:width$}",))?;
+ }
+ out.write("\n")?;
+ }
+
+ Ok(())
+}
+
+macro_rules! define_underlining_heading_fn {
+ ($name:ident, $underline:expr) => {
+ fn $name<'reg, 'rc>(
+ h: &Helper<'reg, 'rc>,
+ _handlebars: &'reg Handlebars,
+ _context: &'rc Context,
+ _render_context: &mut RenderContext<'reg, 'rc>,
+ out: &mut dyn Output,
+ ) -> HelperResult {
+ let param = h
+ .param(0)
+ .ok_or_else(|| HandlebarsRenderError::new("No parameter provided"))?;
+
+ let value = param.value();
+ let text = value.as_str().ok_or_else(|| {
+ HandlebarsRenderError::new(format!("value {value} is not a string"))
+ })?;
+
+ out.write(text)?;
+ out.write("\n")?;
+
+ for _ in 0..text.len() {
+ out.write($underline)?;
+ }
+ Ok(())
+ }
+ };
+}
+
+define_helper_with_prefix_and_postfix!(verbatim_monospaced, "", "");
+define_underlining_heading_fn!(heading_1, "=");
+define_underlining_heading_fn!(heading_2, "-");
+define_helper_with_prefix_and_postfix!(verbatim, "", "");
+
+fn render_object(
+ h: &Helper,
+ _: &Handlebars,
+ _: &Context,
+ _: &mut RenderContext,
+ out: &mut dyn Output,
+) -> HelperResult {
+ let param = h
+ .param(0)
+ .ok_or_else(|| HandlebarsRenderError::new("parameter not found"))?;
+
+ let value = param.value();
+
+ out.write("\n")?;
+ out.write(&serde_json::to_string_pretty(&value)?)?;
+ out.write("\n")?;
+
+ Ok(())
+}
+
+pub(super) fn block_render_functions() -> BlockRenderFunctions {
+ BlockRenderFunctions {
+ table: Box::new(render_plaintext_table),
+ verbatim_monospaced: Box::new(verbatim_monospaced),
+ verbatim: Box::new(verbatim),
+ object: Box::new(render_object),
+ heading_1: Box::new(heading_1),
+ heading_2: Box::new(heading_2),
+ }
+}
diff --git a/proxmox-notify/src/renderer/table.rs b/proxmox-notify/src/renderer/table.rs
new file mode 100644
index 00000000..74f68482
--- /dev/null
+++ b/proxmox-notify/src/renderer/table.rs
@@ -0,0 +1,24 @@
+use std::collections::HashMap;
+
+use serde::Deserialize;
+use serde_json::Value;
+
+use super::ValueRenderFunction;
+
+#[derive(Debug, Deserialize)]
+pub struct ColumnSchema {
+ pub label: String,
+ pub id: String,
+ pub renderer: Option<ValueRenderFunction>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct TableSchema {
+ pub columns: Vec<ColumnSchema>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Table {
+ pub schema: TableSchema,
+ pub data: Vec<HashMap<String, Value>>,
+}
--
2.39.2
next prev parent reply other threads:[~2023-07-17 15:02 UTC|newest]
Thread overview: 114+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-07-17 14:59 [pve-devel] [PATCH v3 many 00/66] fix #4156: introduce new notification system Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 01/66] add proxmox-notify crate Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 02/66] notify: preparation for the first endpoint plugin Lukas Wagner
2023-07-17 15:48 ` Maximiliano Sandoval
2023-07-18 7:19 ` Lukas Wagner
2023-07-18 10:13 ` Wolfgang Bumiller
2023-07-18 11:54 ` Wolfgang Bumiller
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 03/66] notify: preparation for the API Lukas Wagner
2023-07-18 12:02 ` Wolfgang Bumiller
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 04/66] notify: api: add API for sending notifications/testing endpoints Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 05/66] notify: add sendmail plugin Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 06/66] notify: api: add API for sendmail endpoints Lukas Wagner
2023-07-18 12:36 ` Wolfgang Bumiller
2023-07-19 11:51 ` Lukas Wagner
2023-07-19 12:09 ` Wolfgang Bumiller
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 07/66] notify: add gotify endpoint Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 08/66] notify: api: add API for gotify endpoints Lukas Wagner
2023-07-18 12:44 ` Wolfgang Bumiller
2023-07-18 13:19 ` Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 09/66] notify: add notification groups Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 10/66] notify: api: add API for groups Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 11/66] notify: add notification filter mechanism Lukas Wagner
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 12/66] notify: api: add API for filters Lukas Wagner
2023-07-17 14:59 ` Lukas Wagner [this message]
2023-07-17 14:59 ` [pve-devel] [PATCH v3 proxmox 14/66] notify: add example for template rendering Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 15/66] notify: add context Lukas Wagner
2023-07-18 12:57 ` Wolfgang Bumiller
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 16/66] notify: sendmail: allow users as recipients Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 17/66] notify: sendmail: query default author/mailfrom from context Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 18/66] notify: gotify: add proxy support Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 19/66] notify: api: allow to query entities referenced by filter/target Lukas Wagner
2023-07-18 13:02 ` Wolfgang Bumiller
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 20/66] notify: on deletion, check if a filter/endp. is still used by anything Lukas Wagner
2023-07-18 13:20 ` Wolfgang Bumiller
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 21/66] notify: ensure that filter/group/endpoint names are unique Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 22/66] notify: additional logging when sending a notification Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox 23/66] notify: add debian packaging Lukas Wagner
2023-07-18 13:25 ` Wolfgang Bumiller
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 24/66] add PVE::RS::Notify module Lukas Wagner
2023-07-19 10:10 ` Wolfgang Bumiller
2023-07-19 10:23 ` Wolfgang Bumiller
2023-07-19 10:37 ` Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 25/66] notify: add api for sending notifications/testing endpoints Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 26/66] notify: add api for notification groups Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 27/66] notify: add api for sendmail endpoints Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 28/66] notify: add api for gotify endpoints Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 29/66] notify: add api for notification filters Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 30/66] notify: sendmail: support the `mailto-user` parameter Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 31/66] notify: implement context for getting default author/mailfrom Lukas Wagner
2023-07-19 11:15 ` Wolfgang Bumiller
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 32/66] notify: add context for getting http_proxy from datacenter.cfg Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-perl-rs 33/66] notify: add wrapper for `get_referenced_entities` Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-cluster 34/66] cluster files: add notifications.cfg Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-cluster 35/66] datacenter: add APT/fencing/replication notification configuration Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-cluster 36/66] add libpve-notify-perl package Lukas Wagner
2023-07-19 12:27 ` Wolfgang Bumiller
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-guest-common 37/66] vzdump: add config options for new notification backend Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-common 38/66] JSONSchema: increase maxLength of config-digest to 64 Lukas Wagner
2023-07-19 12:31 ` Wolfgang Bumiller
2023-07-19 12:41 ` Fiona Ebner
2023-07-19 12:49 ` Wolfgang Bumiller
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-ha-manager 39/66] manager: send notifications via new notification module Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 40/66] test: fix names of .PHONY targets Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 41/66] d/control: add dependency to `libpve-notify-perl` Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 42/66] vzdump: send notifications via new notification module Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 43/66] test: rename mail_test.pl to vzdump_notification_test.pl Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 44/66] api: apt: send notification via new notification module Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 45/66] api: replication: send notifications " Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 46/66] api: prepare api handler module for notification config Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 47/66] api: notification: add api routes for groups Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 48/66] api: notification: add api routes for sendmail endpoints Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 49/66] api: notification: add api routes for gotify endpoints Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 50/66] api: notification: add api routes for filters Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 51/66] api: notification: allow fetching notification targets Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 52/66] api: notification: allow to test targets Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 53/66] api: notification: disallow removing targets if they are used Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 54/66] ui: backup: allow to select notification target for jobs Lukas Wagner
2023-07-19 12:20 ` Dominik Csapak
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 55/66] ui: backup: adapt backup job details to new notification params Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 56/66] ui: backup: allow to set notification-target for one-off backups Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 57/66] ui: allow to configure notification event -> target mapping Lukas Wagner
2023-07-19 12:45 ` Dominik Csapak
2023-07-19 15:25 ` Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 58/66] ui: add notification target configuration panel Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 59/66] ui: perm path: load notification target/filter acl entries Lukas Wagner
2023-07-19 12:53 ` Dominik Csapak
2023-07-20 7:46 ` Lukas Wagner
2023-07-20 7:54 ` Dominik Csapak
2023-07-20 8:22 ` Lukas Wagner
2023-07-20 8:29 ` Fiona Ebner
2023-07-20 9:26 ` Maximiliano Sandoval
2023-07-20 15:02 ` Thomas Lamprecht
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-manager 60/66] ui: perm path: increase width of the perm path selector combobox Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-widget-toolkit 61/66] notification: add gui for sendmail notification endpoints Lukas Wagner
2023-07-19 13:25 ` Dominik Csapak
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-widget-toolkit 62/66] notification: add gui for gotify " Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-widget-toolkit 63/66] notification: add gui for notification groups Lukas Wagner
2023-07-19 13:32 ` Dominik Csapak
2023-07-20 12:31 ` Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-widget-toolkit 64/66] notification: allow to select filter for notification targets Lukas Wagner
2023-07-17 15:00 ` [pve-devel] [PATCH v3 proxmox-widget-toolkit 65/66] notification: add ui for managing notification filters Lukas Wagner
2023-07-19 13:53 ` Dominik Csapak
2023-07-17 15:00 ` [pve-devel] [PATCH v3 pve-docs 66/66] add documentation for the new notification system Lukas Wagner
2023-07-18 12:34 ` [pve-devel] [PATCH v3 many 00/66] fix #4156: introduce " Dominik Csapak
2023-07-18 13:14 ` Lukas Wagner
2023-07-18 13:58 ` Dominik Csapak
2023-07-18 14:07 ` Lukas Wagner
2023-07-18 14:37 ` Thomas Lamprecht
2023-07-19 13:13 ` Lukas Wagner
2023-07-19 8:40 ` Lukas Wagner
2023-07-19 9:54 ` Wolfgang Bumiller
2023-07-18 13:27 ` Wolfgang Bumiller
2023-07-19 12:11 ` Wolfgang Bumiller
2023-07-19 12:17 ` Lukas Wagner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20230717150051.710464-14-l.wagner@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.