1use std::future::Future;
7use std::pin::Pin;
8use std::sync::Arc;
9
10use std::collections::BTreeMap;
11
12use rustc_hash::FxHashMap;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum NotificationLevel {
17 Info,
18 Warning,
19 Error,
20}
21
22impl NotificationLevel {
23 pub fn parse(s: &str) -> Self {
25 match s {
26 "error" => Self::Error,
27 "warn" | "warning" => Self::Warning,
28 _ => Self::Info,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum LogLevel {
36 Debug,
37 Info,
38 Warn,
39 Error,
40}
41
42impl LogLevel {
43 pub fn parse(s: &str) -> Self {
45 match s.to_ascii_lowercase().as_str() {
46 "debug" => Self::Debug,
47 "warn" | "warning" => Self::Warn,
48 "error" => Self::Error,
49 _ => Self::Info,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum Theme {
57 #[default]
58 Dark,
59 Light,
60 Custom,
62}
63
64pub type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
66
67#[derive(Debug, Clone)]
69pub struct HttpResponse {
70 pub status: u16,
72 pub body: String,
74 pub headers: FxHashMap<String, String>,
76}
77
78#[derive(Debug, Clone)]
80pub struct HttpError {
81 pub message: String,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89pub struct TableColumnConfig {
90 pub name: String,
92 pub key: Option<String>,
94 pub width: Option<u32>,
96}
97
98impl TableColumnConfig {
99 pub fn new(name: impl Into<String>) -> Self {
101 Self {
102 name: name.into(),
103 key: None,
104 width: None,
105 }
106 }
107
108 pub fn with_key(mut self, key: impl Into<String>) -> Self {
110 self.key = Some(key.into());
111 self
112 }
113
114 pub fn with_width(mut self, width: u32) -> Self {
116 self.width = Some(width);
117 self
118 }
119
120 pub fn width_f32(&self) -> Option<f32> {
122 self.width.map(|w| w as f32)
123 }
124
125 pub fn data_key(&self) -> &str {
127 self.key.as_deref().unwrap_or(&self.name)
128 }
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Hash)]
133pub struct CustomTableConfig {
134 pub name: String,
136 pub title: String,
138 pub columns: Vec<TableColumnConfig>,
140 pub refresh_interval: u32,
142 pub plugin_name: String,
144}
145
146#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
148pub struct CustomTableRow {
149 pub cells: BTreeMap<String, String>,
151}
152
153impl CustomTableRow {
154 pub fn new() -> Self {
156 Self::default()
157 }
158
159 pub fn with_cell(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
161 self.cells.insert(key.into(), value.into());
162 self
163 }
164
165 pub fn get(&self, key: &str) -> Option<&str> {
167 self.cells.get(key).map(|s| s.as_str())
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Hash)]
173pub struct CustomTableData {
174 pub rows: Vec<CustomTableRow>,
176 pub error: Option<String>,
178}
179
180impl CustomTableData {
181 pub fn with_rows(rows: Vec<CustomTableRow>) -> Self {
183 Self { rows, error: None }
184 }
185
186 pub fn with_error(message: impl Into<String>) -> Self {
188 Self {
189 rows: Vec::new(),
190 error: Some(message.into()),
191 }
192 }
193}
194
195#[derive(Debug, Clone, PartialEq)]
199pub struct ChartDataPoint {
200 pub timestamp: f64,
202 pub value: f64,
204}
205
206impl ChartDataPoint {
207 pub fn new(timestamp: f64, value: f64) -> Self {
209 Self { timestamp, value }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq)]
215pub struct ChartSeries {
216 pub name: String,
218 pub tags: BTreeMap<String, String>,
220 pub points: Vec<ChartDataPoint>,
222}
223
224impl ChartSeries {
225 pub fn new(name: impl Into<String>) -> Self {
227 Self {
228 name: name.into(),
229 tags: BTreeMap::new(),
230 points: Vec::new(),
231 }
232 }
233
234 pub fn with_tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
236 self.tags.insert(key.into(), value.into());
237 self
238 }
239
240 pub fn with_point(mut self, timestamp: f64, value: f64) -> Self {
242 self.points.push(ChartDataPoint::new(timestamp, value));
243 self
244 }
245
246 pub fn with_points(mut self, points: Vec<ChartDataPoint>) -> Self {
248 self.points.extend(points);
249 self
250 }
251}
252
253#[derive(Debug, Clone, PartialEq, Eq, Hash)]
255pub struct CustomChartConfig {
256 pub name: String,
258 pub title: String,
260 pub y_unit: Option<String>,
262 pub refresh_interval: u32,
264 pub plugin_name: String,
266}
267
268#[derive(Debug, Clone, PartialEq)]
270pub struct CustomChartData {
271 pub series: Vec<ChartSeries>,
273 pub error: Option<String>,
275}
276
277impl CustomChartData {
278 pub fn with_series(series: Vec<ChartSeries>) -> Self {
280 Self {
281 series,
282 error: None,
283 }
284 }
285
286 pub fn with_error(message: impl Into<String>) -> Self {
288 Self {
289 series: Vec::new(),
290 error: Some(message.into()),
291 }
292 }
293}
294
295#[derive(Debug, Clone, PartialEq)]
299pub struct ThresholdConfig {
300 pub value: f64,
302 pub color: String,
304 pub label: Option<String>,
306}
307
308impl ThresholdConfig {
309 pub fn new(value: f64, color: impl Into<String>) -> Self {
311 Self {
312 value,
313 color: color.into(),
314 label: None,
315 }
316 }
317
318 pub fn with_label(mut self, label: impl Into<String>) -> Self {
320 self.label = Some(label.into());
321 self
322 }
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Hash)]
327pub struct StatPaneConfig {
328 pub name: String,
330 pub title: String,
332 pub unit: Option<String>,
334 pub refresh_interval: u32,
336 pub plugin_name: String,
338}
339
340#[derive(Debug, Clone, PartialEq)]
342pub struct StatPaneData {
343 pub value: f64,
345 pub sparkline: Vec<f64>,
347 pub change_value: Option<f64>,
349 pub change_period: Option<String>,
351 pub thresholds: Vec<ThresholdConfig>,
353 pub error: Option<String>,
355}
356
357impl Default for StatPaneData {
358 fn default() -> Self {
359 Self {
360 value: 0.0,
361 sparkline: Vec::new(),
362 change_value: None,
363 change_period: None,
364 thresholds: Vec::new(),
365 error: None,
366 }
367 }
368}
369
370impl StatPaneData {
371 pub fn with_value(value: f64) -> Self {
373 Self {
374 value,
375 ..Default::default()
376 }
377 }
378
379 pub fn with_error(message: impl Into<String>) -> Self {
381 Self {
382 error: Some(message.into()),
383 ..Default::default()
384 }
385 }
386
387 pub fn sparkline(mut self, data: Vec<f64>) -> Self {
389 self.sparkline = data;
390 self
391 }
392
393 pub fn change(mut self, value: f64, period: impl Into<String>) -> Self {
395 self.change_value = Some(value);
396 self.change_period = Some(period.into());
397 self
398 }
399
400 pub fn threshold(mut self, threshold: ThresholdConfig) -> Self {
402 self.thresholds.push(threshold);
403 self
404 }
405}
406
407#[derive(Debug, Clone, Default, PartialEq, Eq)]
412pub struct FocusedPaneInfo {
413 pub pane_type: String,
415 pub title: Option<String>,
417 pub query: Option<String>,
419 pub metric_name: Option<String>,
421}
422
423impl FocusedPaneInfo {
424 pub fn new(pane_type: impl Into<String>) -> Self {
426 Self {
427 pane_type: pane_type.into(),
428 title: None,
429 query: None,
430 metric_name: None,
431 }
432 }
433
434 pub fn with_title(mut self, title: impl Into<String>) -> Self {
436 self.title = Some(title.into());
437 self
438 }
439
440 pub fn with_query(mut self, query: impl Into<String>) -> Self {
442 self.query = Some(query.into());
443 self
444 }
445
446 pub fn with_metric_name(mut self, metric_name: impl Into<String>) -> Self {
448 self.metric_name = Some(metric_name.into());
449 self
450 }
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Hash)]
457pub struct GaugePaneConfig {
458 pub name: String,
460 pub title: String,
462 pub unit: Option<String>,
464 pub min_scaled: i64,
466 pub max_scaled: i64,
468 pub refresh_interval: u32,
470 pub plugin_name: String,
472}
473
474const GAUGE_SCALE_FACTOR: f64 = 1_000_000.0;
476
477impl GaugePaneConfig {
478 pub fn min(&self) -> f64 {
480 self.min_scaled as f64 / GAUGE_SCALE_FACTOR
481 }
482
483 pub fn max(&self) -> f64 {
485 self.max_scaled as f64 / GAUGE_SCALE_FACTOR
486 }
487
488 pub fn set_range(&mut self, min: f64, max: f64) {
491 self.min_scaled = scale_f64_to_i64(min);
492 self.max_scaled = scale_f64_to_i64(max);
493 }
494}
495
496fn scale_f64_to_i64(value: f64) -> i64 {
499 if value.is_nan() {
500 return 0;
501 }
502 let scaled = value * GAUGE_SCALE_FACTOR;
503 if scaled >= i64::MAX as f64 {
504 i64::MAX
505 } else if scaled <= i64::MIN as f64 {
506 i64::MIN
507 } else {
508 scaled as i64
509 }
510}
511
512#[derive(Debug, Clone, PartialEq)]
514pub struct GaugePaneData {
515 pub value: f64,
517 pub thresholds: Vec<ThresholdConfig>,
519 pub error: Option<String>,
521}
522
523impl Default for GaugePaneData {
524 fn default() -> Self {
525 Self {
526 value: 0.0,
527 thresholds: Vec::new(),
528 error: None,
529 }
530 }
531}
532
533impl GaugePaneData {
534 pub fn with_value(value: f64) -> Self {
536 Self {
537 value,
538 ..Default::default()
539 }
540 }
541
542 pub fn with_error(message: impl Into<String>) -> Self {
544 Self {
545 error: Some(message.into()),
546 ..Default::default()
547 }
548 }
549
550 pub fn threshold(mut self, threshold: ThresholdConfig) -> Self {
552 self.thresholds.push(threshold);
553 self
554 }
555}
556
557pub trait PluginHost: Send + Sync {
563 fn notify(&self, level: NotificationLevel, message: &str);
565
566 fn request_repaint(&self);
568
569 fn log(&self, level: LogLevel, message: &str);
571
572 fn version(&self) -> &'static str;
574
575 fn is_wasm(&self) -> bool;
577
578 fn theme(&self) -> Theme;
580
581 fn theme_name(&self) -> &'static str;
583
584 fn clipboard_write(&self, text: &str) -> bool;
587
588 fn clipboard_read(&self) -> Option<String>;
591
592 fn spawn(&self, future: BoxFuture<()>);
594
595 fn http_get(
598 &self,
599 url: &str,
600 headers: &FxHashMap<String, String>,
601 ) -> Result<HttpResponse, HttpError>;
602
603 fn http_post(
606 &self,
607 url: &str,
608 body: &str,
609 headers: &FxHashMap<String, String>,
610 ) -> Result<HttpResponse, HttpError>;
611
612 fn add_query_pane(&self, query: &str, title: Option<&str>);
616
617 fn add_logs_pane(&self);
619
620 fn add_tracing_pane(&self, trace_id: Option<&str>);
622
623 fn add_terminal_pane(&self);
625
626 fn add_sql_pane(&self);
628
629 fn close_focused_pane(&self);
631
632 fn focus_pane(&self, direction: &str);
634
635 fn set_time_range_preset(&self, preset: &str);
639
640 fn set_time_range_absolute(&self, start_secs: f64, end_secs: f64);
642
643 fn get_time_range(&self) -> (f64, f64);
646
647 fn register_custom_table_pane(&self, config: CustomTableConfig);
651
652 fn add_custom_table_pane(&self, pane_type: &str);
654
655 fn update_custom_table_data(&self, pane_id: usize, data: CustomTableData);
658
659 fn update_custom_table_data_by_type(&self, pane_type: &str, data: CustomTableData);
662
663 fn register_custom_chart_pane(&self, config: CustomChartConfig);
667
668 fn add_custom_chart_pane(&self, pane_type: &str);
670
671 fn update_custom_chart_data_by_type(&self, pane_type: &str, data: CustomChartData);
673
674 fn register_stat_pane(&self, config: StatPaneConfig);
678
679 fn add_stat_pane(&self, pane_type: &str);
681
682 fn update_stat_data_by_type(&self, pane_type: &str, data: StatPaneData);
684
685 fn register_gauge_pane(&self, config: GaugePaneConfig);
689
690 fn add_gauge_pane(&self, pane_type: &str);
692
693 fn update_gauge_data_by_type(&self, pane_type: &str, data: GaugePaneData);
695
696 fn get_focused_pane_info(&self) -> Option<FocusedPaneInfo>;
701}
702
703pub type PluginHostRef = Arc<dyn PluginHost>;
705
706pub struct PluginContext {
708 host: PluginHostRef,
709}
710
711impl PluginContext {
712 pub fn new(host: PluginHostRef) -> Self {
714 Self { host }
715 }
716
717 pub fn notify(&self, level: &str, message: &str) {
719 self.host.notify(NotificationLevel::parse(level), message);
720 }
721
722 pub fn request_repaint(&self) {
724 self.host.request_repaint();
725 }
726
727 pub fn log(&self, level: LogLevel, message: &str) {
729 self.host.log(level, message);
730 }
731
732 pub fn editor_version(&self) -> &'static str {
734 self.host.version()
735 }
736
737 pub fn is_wasm(&self) -> bool {
739 self.host.is_wasm()
740 }
741
742 pub fn theme(&self) -> Theme {
744 self.host.theme()
745 }
746
747 pub fn theme_name(&self) -> &'static str {
749 self.host.theme_name()
750 }
751
752 pub fn clipboard_write(&self, text: &str) -> bool {
754 self.host.clipboard_write(text)
755 }
756
757 pub fn clipboard_read(&self) -> Option<String> {
759 self.host.clipboard_read()
760 }
761
762 pub fn spawn<F>(&self, future: F)
764 where
765 F: Future<Output = ()> + Send + 'static,
766 {
767 self.host.spawn(Box::pin(future));
768 }
769
770 pub fn host(&self) -> &PluginHostRef {
772 &self.host
773 }
774
775 pub fn http_get(
777 &self,
778 url: &str,
779 headers: &FxHashMap<String, String>,
780 ) -> Result<HttpResponse, HttpError> {
781 self.host.http_get(url, headers)
782 }
783
784 pub fn http_post(
786 &self,
787 url: &str,
788 body: &str,
789 headers: &FxHashMap<String, String>,
790 ) -> Result<HttpResponse, HttpError> {
791 self.host.http_post(url, body, headers)
792 }
793
794 pub fn add_query_pane(&self, query: &str, title: Option<&str>) {
798 self.host.add_query_pane(query, title);
799 }
800
801 pub fn add_logs_pane(&self) {
803 self.host.add_logs_pane();
804 }
805
806 pub fn add_tracing_pane(&self, trace_id: Option<&str>) {
808 self.host.add_tracing_pane(trace_id);
809 }
810
811 pub fn add_terminal_pane(&self) {
813 self.host.add_terminal_pane();
814 }
815
816 pub fn add_sql_pane(&self) {
818 self.host.add_sql_pane();
819 }
820
821 pub fn close_focused_pane(&self) {
823 self.host.close_focused_pane();
824 }
825
826 pub fn focus_pane(&self, direction: &str) {
828 self.host.focus_pane(direction);
829 }
830
831 pub fn set_time_range_preset(&self, preset: &str) {
835 self.host.set_time_range_preset(preset);
836 }
837
838 pub fn set_time_range_absolute(&self, start_secs: f64, end_secs: f64) {
840 self.host.set_time_range_absolute(start_secs, end_secs);
841 }
842
843 pub fn get_time_range(&self) -> (f64, f64) {
845 self.host.get_time_range()
846 }
847
848 pub fn register_custom_table_pane(&self, config: CustomTableConfig) {
852 self.host.register_custom_table_pane(config);
853 }
854
855 pub fn add_custom_table_pane(&self, pane_type: &str) {
857 self.host.add_custom_table_pane(pane_type);
858 }
859
860 pub fn update_custom_table_data(&self, pane_id: usize, data: CustomTableData) {
862 self.host.update_custom_table_data(pane_id, data);
863 }
864
865 pub fn update_custom_table_data_by_type(&self, pane_type: &str, data: CustomTableData) {
867 self.host.update_custom_table_data_by_type(pane_type, data);
868 }
869
870 pub fn register_custom_chart_pane(&self, config: CustomChartConfig) {
874 self.host.register_custom_chart_pane(config);
875 }
876
877 pub fn add_custom_chart_pane(&self, pane_type: &str) {
879 self.host.add_custom_chart_pane(pane_type);
880 }
881
882 pub fn update_custom_chart_data_by_type(&self, pane_type: &str, data: CustomChartData) {
884 self.host.update_custom_chart_data_by_type(pane_type, data);
885 }
886
887 pub fn register_stat_pane(&self, config: StatPaneConfig) {
891 self.host.register_stat_pane(config);
892 }
893
894 pub fn add_stat_pane(&self, pane_type: &str) {
896 self.host.add_stat_pane(pane_type);
897 }
898
899 pub fn update_stat_data_by_type(&self, pane_type: &str, data: StatPaneData) {
901 self.host.update_stat_data_by_type(pane_type, data);
902 }
903
904 pub fn register_gauge_pane(&self, config: GaugePaneConfig) {
908 self.host.register_gauge_pane(config);
909 }
910
911 pub fn add_gauge_pane(&self, pane_type: &str) {
913 self.host.add_gauge_pane(pane_type);
914 }
915
916 pub fn update_gauge_data_by_type(&self, pane_type: &str, data: GaugePaneData) {
918 self.host.update_gauge_data_by_type(pane_type, data);
919 }
920
921 pub fn get_focused_pane_info(&self) -> Option<FocusedPaneInfo> {
925 self.host.get_focused_pane_info()
926 }
927}
928
929pub type PluginContextRef = Arc<PluginContext>;
931
932#[cfg(test)]
933mod tests {
934 use super::*;
935
936 #[test]
937 fn test_notification_level_parse() {
938 assert_eq!(NotificationLevel::parse("error"), NotificationLevel::Error);
939 assert_eq!(NotificationLevel::parse("warn"), NotificationLevel::Warning);
940 assert_eq!(
941 NotificationLevel::parse("warning"),
942 NotificationLevel::Warning
943 );
944 assert_eq!(NotificationLevel::parse("info"), NotificationLevel::Info);
945 assert_eq!(NotificationLevel::parse("unknown"), NotificationLevel::Info);
947 assert_eq!(NotificationLevel::parse(""), NotificationLevel::Info);
948 assert_eq!(NotificationLevel::parse("ERROR"), NotificationLevel::Info); }
950
951 #[test]
952 fn test_log_level_parse() {
953 assert_eq!(LogLevel::parse("debug"), LogLevel::Debug);
954 assert_eq!(LogLevel::parse("info"), LogLevel::Info);
955 assert_eq!(LogLevel::parse("warn"), LogLevel::Warn);
956 assert_eq!(LogLevel::parse("warning"), LogLevel::Warn);
957 assert_eq!(LogLevel::parse("error"), LogLevel::Error);
958 assert_eq!(LogLevel::parse("unknown"), LogLevel::Info);
960 assert_eq!(LogLevel::parse(""), LogLevel::Info);
961 assert_eq!(LogLevel::parse("DEBUG"), LogLevel::Debug);
963 assert_eq!(LogLevel::parse("WARN"), LogLevel::Warn);
964 assert_eq!(LogLevel::parse("Error"), LogLevel::Error);
965 }
966
967 #[test]
968 fn test_theme_default() {
969 assert_eq!(Theme::default(), Theme::Dark);
970 }
971
972 #[test]
973 fn test_http_response_clone() {
974 let mut headers = FxHashMap::default();
975 headers.insert("Content-Type".to_string(), "application/json".to_string());
976
977 let response = HttpResponse {
978 status: 200,
979 body: "test body".to_string(),
980 headers,
981 };
982
983 let cloned = response.clone();
984 assert_eq!(cloned.status, 200);
985 assert_eq!(cloned.body, "test body");
986 assert_eq!(
987 cloned.headers.get("Content-Type"),
988 Some(&"application/json".to_string())
989 );
990 }
991
992 #[test]
993 fn test_http_error_clone() {
994 let error = HttpError {
995 message: "Network error".to_string(),
996 };
997
998 let cloned = error.clone();
999 assert_eq!(cloned.message, "Network error");
1000 }
1001}