use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::collections::BTreeMap;
use rustc_hash::FxHashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotificationLevel {
Info,
Warning,
Error,
}
impl NotificationLevel {
pub fn parse(s: &str) -> Self {
match s {
"error" => Self::Error,
"warn" | "warning" => Self::Warning,
_ => Self::Info,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
pub fn parse(s: &str) -> Self {
match s.to_ascii_lowercase().as_str() {
"debug" => Self::Debug,
"warn" | "warning" => Self::Warn,
"error" => Self::Error,
_ => Self::Info,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Theme {
#[default]
Dark,
Light,
Custom,
}
pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
#[derive(Debug, Clone)]
pub struct HttpResponse {
pub status: u16,
pub body: String,
pub headers: FxHashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct HttpError {
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TableColumnConfig {
pub name: String,
pub key: Option<String>,
pub width: Option<u32>,
}
impl TableColumnConfig {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
key: None,
width: None,
}
}
pub fn with_key(mut self, key: impl Into<String>) -> Self {
self.key = Some(key.into());
self
}
pub fn with_width(mut self, width: u32) -> Self {
self.width = Some(width);
self
}
pub fn width_f32(&self) -> Option<f32> {
self.width.map(|w| w as f32)
}
pub fn data_key(&self) -> &str {
self.key.as_deref().unwrap_or(&self.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CustomTableConfig {
pub name: String,
pub title: String,
pub columns: Vec<TableColumnConfig>,
pub refresh_interval: u32,
pub plugin_name: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct CustomTableRow {
pub cells: BTreeMap<String, String>,
}
impl CustomTableRow {
pub fn new() -> Self {
Self::default()
}
pub fn with_cell(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.cells.insert(key.into(), value.into());
self
}
pub fn get(&self, key: &str) -> Option<&str> {
self.cells.get(key).map(|s| s.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CustomTableData {
pub rows: Vec<CustomTableRow>,
pub error: Option<String>,
}
impl CustomTableData {
pub fn with_rows(rows: Vec<CustomTableRow>) -> Self {
Self { rows, error: None }
}
pub fn with_error(message: impl Into<String>) -> Self {
Self {
rows: Vec::new(),
error: Some(message.into()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChartDataPoint {
pub timestamp: f64,
pub value: f64,
}
impl ChartDataPoint {
pub fn new(timestamp: f64, value: f64) -> Self {
Self { timestamp, value }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChartSeries {
pub name: String,
pub tags: BTreeMap<String, String>,
pub points: Vec<ChartDataPoint>,
}
impl ChartSeries {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
tags: BTreeMap::new(),
points: Vec::new(),
}
}
pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.tags.insert(key.into(), value.into());
self
}
pub fn with_point(mut self, timestamp: f64, value: f64) -> Self {
self.points.push(ChartDataPoint::new(timestamp, value));
self
}
pub fn with_points(mut self, points: Vec<ChartDataPoint>) -> Self {
self.points.extend(points);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CustomChartConfig {
pub name: String,
pub title: String,
pub y_unit: Option<String>,
pub refresh_interval: u32,
pub plugin_name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CustomChartData {
pub series: Vec<ChartSeries>,
pub error: Option<String>,
}
impl CustomChartData {
pub fn with_series(series: Vec<ChartSeries>) -> Self {
Self {
series,
error: None,
}
}
pub fn with_error(message: impl Into<String>) -> Self {
Self {
series: Vec::new(),
error: Some(message.into()),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ThresholdConfig {
pub value: f64,
pub color: String,
pub label: Option<String>,
}
impl ThresholdConfig {
pub fn new(value: f64, color: impl Into<String>) -> Self {
Self {
value,
color: color.into(),
label: None,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StatPaneConfig {
pub name: String,
pub title: String,
pub unit: Option<String>,
pub refresh_interval: u32,
pub plugin_name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct StatPaneData {
pub value: f64,
pub sparkline: Vec<f64>,
pub change_value: Option<f64>,
pub change_period: Option<String>,
pub thresholds: Vec<ThresholdConfig>,
pub error: Option<String>,
}
impl Default for StatPaneData {
fn default() -> Self {
Self {
value: 0.0,
sparkline: Vec::new(),
change_value: None,
change_period: None,
thresholds: Vec::new(),
error: None,
}
}
}
impl StatPaneData {
pub fn with_value(value: f64) -> Self {
Self {
value,
..Default::default()
}
}
pub fn with_error(message: impl Into<String>) -> Self {
Self {
error: Some(message.into()),
..Default::default()
}
}
pub fn sparkline(mut self, data: Vec<f64>) -> Self {
self.sparkline = data;
self
}
pub fn change(mut self, value: f64, period: impl Into<String>) -> Self {
self.change_value = Some(value);
self.change_period = Some(period.into());
self
}
pub fn threshold(mut self, threshold: ThresholdConfig) -> Self {
self.thresholds.push(threshold);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FocusedPaneInfo {
pub pane_type: String,
pub title: Option<String>,
pub query: Option<String>,
pub metric_name: Option<String>,
}
impl FocusedPaneInfo {
pub fn new(pane_type: impl Into<String>) -> Self {
Self {
pane_type: pane_type.into(),
title: None,
query: None,
metric_name: None,
}
}
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn with_query(mut self, query: impl Into<String>) -> Self {
self.query = Some(query.into());
self
}
pub fn with_metric_name(mut self, metric_name: impl Into<String>) -> Self {
self.metric_name = Some(metric_name.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GaugePaneConfig {
pub name: String,
pub title: String,
pub unit: Option<String>,
pub min_scaled: i64,
pub max_scaled: i64,
pub refresh_interval: u32,
pub plugin_name: String,
}
const GAUGE_SCALE_FACTOR: f64 = 1_000_000.0;
impl GaugePaneConfig {
pub fn min(&self) -> f64 {
self.min_scaled as f64 / GAUGE_SCALE_FACTOR
}
pub fn max(&self) -> f64 {
self.max_scaled as f64 / GAUGE_SCALE_FACTOR
}
pub fn set_range(&mut self, min: f64, max: f64) {
self.min_scaled = scale_f64_to_i64(min);
self.max_scaled = scale_f64_to_i64(max);
}
}
fn scale_f64_to_i64(value: f64) -> i64 {
if value.is_nan() {
return 0;
}
let scaled = value * GAUGE_SCALE_FACTOR;
if scaled >= i64::MAX as f64 {
i64::MAX
} else if scaled <= i64::MIN as f64 {
i64::MIN
} else {
scaled as i64
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GaugePaneData {
pub value: f64,
pub thresholds: Vec<ThresholdConfig>,
pub error: Option<String>,
}
impl Default for GaugePaneData {
fn default() -> Self {
Self {
value: 0.0,
thresholds: Vec::new(),
error: None,
}
}
}
impl GaugePaneData {
pub fn with_value(value: f64) -> Self {
Self {
value,
..Default::default()
}
}
pub fn with_error(message: impl Into<String>) -> Self {
Self {
error: Some(message.into()),
..Default::default()
}
}
pub fn threshold(mut self, threshold: ThresholdConfig) -> Self {
self.thresholds.push(threshold);
self
}
}
pub trait PluginHost: Send + Sync {
fn notify(&self, level: NotificationLevel, message: &str);
fn request_repaint(&self);
fn log(&self, level: LogLevel, message: &str);
fn version(&self) -> &'static str;
fn is_wasm(&self) -> bool;
fn theme(&self) -> Theme;
fn theme_name(&self) -> &'static str;
fn clipboard_write(&self, text: &str) -> bool;
fn clipboard_read(&self) -> Option<String>;
fn spawn(&self, future: BoxFuture<()>);
fn http_get(
&self,
url: &str,
headers: &FxHashMap<String, String>,
) -> Result<HttpResponse, HttpError>;
fn http_post(
&self,
url: &str,
body: &str,
headers: &FxHashMap<String, String>,
) -> Result<HttpResponse, HttpError>;
fn add_query_pane(&self, query: &str, title: Option<&str>);
fn add_logs_pane(&self);
fn add_tracing_pane(&self, trace_id: Option<&str>);
fn add_terminal_pane(&self);
fn add_sql_pane(&self);
fn close_focused_pane(&self);
fn focus_pane(&self, direction: &str);
fn set_time_range_preset(&self, preset: &str);
fn set_time_range_absolute(&self, start_secs: f64, end_secs: f64);
fn get_time_range(&self) -> (f64, f64);
fn register_custom_table_pane(&self, config: CustomTableConfig);
fn add_custom_table_pane(&self, pane_type: &str);
fn update_custom_table_data(&self, pane_id: usize, data: CustomTableData);
fn update_custom_table_data_by_type(&self, pane_type: &str, data: CustomTableData);
fn register_custom_chart_pane(&self, config: CustomChartConfig);
fn add_custom_chart_pane(&self, pane_type: &str);
fn update_custom_chart_data_by_type(&self, pane_type: &str, data: CustomChartData);
fn register_stat_pane(&self, config: StatPaneConfig);
fn add_stat_pane(&self, pane_type: &str);
fn update_stat_data_by_type(&self, pane_type: &str, data: StatPaneData);
fn register_gauge_pane(&self, config: GaugePaneConfig);
fn add_gauge_pane(&self, pane_type: &str);
fn update_gauge_data_by_type(&self, pane_type: &str, data: GaugePaneData);
fn get_focused_pane_info(&self) -> Option<FocusedPaneInfo>;
}
pub type PluginHostRef = Arc<dyn PluginHost>;
pub struct PluginContext {
host: PluginHostRef,
}
impl PluginContext {
pub fn new(host: PluginHostRef) -> Self {
Self { host }
}
pub fn notify(&self, level: &str, message: &str) {
self.host.notify(NotificationLevel::parse(level), message);
}
pub fn request_repaint(&self) {
self.host.request_repaint();
}
pub fn log(&self, level: LogLevel, message: &str) {
self.host.log(level, message);
}
pub fn editor_version(&self) -> &'static str {
self.host.version()
}
pub fn is_wasm(&self) -> bool {
self.host.is_wasm()
}
pub fn theme(&self) -> Theme {
self.host.theme()
}
pub fn theme_name(&self) -> &'static str {
self.host.theme_name()
}
pub fn clipboard_write(&self, text: &str) -> bool {
self.host.clipboard_write(text)
}
pub fn clipboard_read(&self) -> Option<String> {
self.host.clipboard_read()
}
pub fn spawn<F>(&self, future: F)
where
F: Future<Output = ()> + Send + 'static,
{
self.host.spawn(Box::pin(future));
}
pub fn host(&self) -> &PluginHostRef {
&self.host
}
pub fn http_get(
&self,
url: &str,
headers: &FxHashMap<String, String>,
) -> Result<HttpResponse, HttpError> {
self.host.http_get(url, headers)
}
pub fn http_post(
&self,
url: &str,
body: &str,
headers: &FxHashMap<String, String>,
) -> Result<HttpResponse, HttpError> {
self.host.http_post(url, body, headers)
}
pub fn add_query_pane(&self, query: &str, title: Option<&str>) {
self.host.add_query_pane(query, title);
}
pub fn add_logs_pane(&self) {
self.host.add_logs_pane();
}
pub fn add_tracing_pane(&self, trace_id: Option<&str>) {
self.host.add_tracing_pane(trace_id);
}
pub fn add_terminal_pane(&self) {
self.host.add_terminal_pane();
}
pub fn add_sql_pane(&self) {
self.host.add_sql_pane();
}
pub fn close_focused_pane(&self) {
self.host.close_focused_pane();
}
pub fn focus_pane(&self, direction: &str) {
self.host.focus_pane(direction);
}
pub fn set_time_range_preset(&self, preset: &str) {
self.host.set_time_range_preset(preset);
}
pub fn set_time_range_absolute(&self, start_secs: f64, end_secs: f64) {
self.host.set_time_range_absolute(start_secs, end_secs);
}
pub fn get_time_range(&self) -> (f64, f64) {
self.host.get_time_range()
}
pub fn register_custom_table_pane(&self, config: CustomTableConfig) {
self.host.register_custom_table_pane(config);
}
pub fn add_custom_table_pane(&self, pane_type: &str) {
self.host.add_custom_table_pane(pane_type);
}
pub fn update_custom_table_data(&self, pane_id: usize, data: CustomTableData) {
self.host.update_custom_table_data(pane_id, data);
}
pub fn update_custom_table_data_by_type(&self, pane_type: &str, data: CustomTableData) {
self.host.update_custom_table_data_by_type(pane_type, data);
}
pub fn register_custom_chart_pane(&self, config: CustomChartConfig) {
self.host.register_custom_chart_pane(config);
}
pub fn add_custom_chart_pane(&self, pane_type: &str) {
self.host.add_custom_chart_pane(pane_type);
}
pub fn update_custom_chart_data_by_type(&self, pane_type: &str, data: CustomChartData) {
self.host.update_custom_chart_data_by_type(pane_type, data);
}
pub fn register_stat_pane(&self, config: StatPaneConfig) {
self.host.register_stat_pane(config);
}
pub fn add_stat_pane(&self, pane_type: &str) {
self.host.add_stat_pane(pane_type);
}
pub fn update_stat_data_by_type(&self, pane_type: &str, data: StatPaneData) {
self.host.update_stat_data_by_type(pane_type, data);
}
pub fn register_gauge_pane(&self, config: GaugePaneConfig) {
self.host.register_gauge_pane(config);
}
pub fn add_gauge_pane(&self, pane_type: &str) {
self.host.add_gauge_pane(pane_type);
}
pub fn update_gauge_data_by_type(&self, pane_type: &str, data: GaugePaneData) {
self.host.update_gauge_data_by_type(pane_type, data);
}
pub fn get_focused_pane_info(&self) -> Option<FocusedPaneInfo> {
self.host.get_focused_pane_info()
}
}
pub type PluginContextRef = Arc<PluginContext>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_notification_level_parse() {
assert_eq!(NotificationLevel::parse("error"), NotificationLevel::Error);
assert_eq!(NotificationLevel::parse("warn"), NotificationLevel::Warning);
assert_eq!(
NotificationLevel::parse("warning"),
NotificationLevel::Warning
);
assert_eq!(NotificationLevel::parse("info"), NotificationLevel::Info);
assert_eq!(NotificationLevel::parse("unknown"), NotificationLevel::Info);
assert_eq!(NotificationLevel::parse(""), NotificationLevel::Info);
assert_eq!(NotificationLevel::parse("ERROR"), NotificationLevel::Info); }
#[test]
fn test_log_level_parse() {
assert_eq!(LogLevel::parse("debug"), LogLevel::Debug);
assert_eq!(LogLevel::parse("info"), LogLevel::Info);
assert_eq!(LogLevel::parse("warn"), LogLevel::Warn);
assert_eq!(LogLevel::parse("warning"), LogLevel::Warn);
assert_eq!(LogLevel::parse("error"), LogLevel::Error);
assert_eq!(LogLevel::parse("unknown"), LogLevel::Info);
assert_eq!(LogLevel::parse(""), LogLevel::Info);
assert_eq!(LogLevel::parse("DEBUG"), LogLevel::Debug);
assert_eq!(LogLevel::parse("WARN"), LogLevel::Warn);
assert_eq!(LogLevel::parse("Error"), LogLevel::Error);
}
#[test]
fn test_theme_default() {
assert_eq!(Theme::default(), Theme::Dark);
}
#[test]
fn test_http_response_clone() {
let mut headers = FxHashMap::default();
headers.insert("Content-Type".to_string(), "application/json".to_string());
let response = HttpResponse {
status: 200,
body: "test body".to_string(),
headers,
};
let cloned = response.clone();
assert_eq!(cloned.status, 200);
assert_eq!(cloned.body, "test body");
assert_eq!(
cloned.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
}
#[test]
fn test_http_error_clone() {
let error = HttpError {
message: "Network error".to_string(),
};
let cloned = error.clone();
assert_eq!(cloned.message, "Network error");
}
}