441 lines
10 KiB
Rust
Raw Normal View History

2026-07-24 18:22:49 +03:00
//! The serializable Grafana dashboard schema — the internal data model the
//! public builders assemble into.
//!
//! Most types are `Serialize`-only, sized for what we *emit*. A handful of
//! vocabularies (`PanelType`, `Color`, and the styling enums) additionally
//! derive `Deserialize` because the lenient `input` model — which backs the
//! panel→Rust transpiler — reuses them.
2026-07-24 18:22:49 +03:00
use serde::{Deserialize, Serialize};
2026-07-24 18:22:49 +03:00
2026-07-28 02:15:43 +03:00
use crate::{DATASOURCE_UID, FieldOverride, Target, unit::Unit};
2026-07-24 18:22:49 +03:00
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DatasourceKind {
Prometheus,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Fill {
Dash,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum DrawStyle {
Line,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum MatcherKind {
ByName,
}
#[derive(Clone, Copy, Serialize)]
pub enum PropertyId {
#[serde(rename = "color")]
Color,
#[serde(rename = "custom.lineStyle")]
LineStyle,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Calc {
LastNotNull,
Last,
Max,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StatColorMode {
Value,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum GraphMode {
Area,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum LegendDisplay {
Table,
List,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Placement {
Bottom,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum TooltipMode {
Single,
Multi,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
Desc,
}
// Reused by the `input` model, hence `Deserialize`.
#[derive(Clone, Copy, Serialize, Deserialize)]
2026-07-24 18:22:49 +03:00
#[serde(rename_all = "lowercase")]
pub enum PanelType {
Stat,
Timeseries,
Gauge,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ThresholdMode {
Absolute,
/// Steps expressed as a percentage of the minmax range. The builder never
/// emits this; it exists so Grafana exports using it still parse.
Percentage,
}
/// One threshold step: the color values at or above `value` take. The base step
/// carries `value: null` — Grafana's "everything below the first threshold".
#[derive(Clone, Serialize, Deserialize)]
pub struct ThresholdStep {
pub color: String,
pub value: Option<f64>,
}
/// A threshold ladder, driving gauge/stat coloring.
#[derive(Clone, Serialize, Deserialize)]
pub struct Thresholds {
pub mode: ThresholdMode,
pub steps: Vec<ThresholdStep>,
}
impl Thresholds {
/// Start a ladder with the color used below every threshold.
pub fn base(color: impl Into<String>) -> Self {
Self {
mode: ThresholdMode::Absolute,
steps: vec![ThresholdStep {
color: color.into(),
value: None,
}],
}
}
/// Add a step: values at or above `value` render in `color`.
#[must_use]
pub fn step(mut self, value: f64, color: impl Into<String>) -> Self {
self.steps.push(ThresholdStep {
color: color.into(),
value: Some(value),
});
self
}
2026-07-24 18:22:49 +03:00
}
// Optional timeseries styling vocabularies. Each derives `PartialEq` so the
// public setters can panic when handed the Grafana default (see `styling`), and
// `Deserialize` because the `input` model reuses them.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum LineInterpolation {
Linear,
Smooth,
StepBefore,
StepAfter,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ShowPoints {
Auto,
Never,
Always,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GradientMode {
None,
Opacity,
Hue,
Scheme,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum StackingMode {
None,
Normal,
Percent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AxisPlacement {
Auto,
Left,
Right,
Hidden,
}
#[derive(Serialize)]
pub struct Stacking {
pub mode: StackingMode,
pub group: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Custom {
pub draw_style: DrawStyle,
pub line_width: u32,
pub fill_opacity: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span_nulls: Option<bool>,
// Optional styling — omitted (left at Grafana's default) unless a setter
// fills it in. See `styling`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line_interpolation: Option<LineInterpolation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_points: Option<ShowPoints>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gradient_mode: Option<GradientMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stacking: Option<Stacking>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub axis_placement: Option<AxisPlacement>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub axis_label: Option<String>,
}
2026-07-24 18:22:49 +03:00
#[derive(Clone, Serialize)]
pub struct Datasource {
#[serde(rename = "type")]
pub kind: DatasourceKind,
pub uid: String,
2026-07-24 18:22:49 +03:00
}
impl Datasource {
pub fn prometheus() -> Self {
Self {
kind: DatasourceKind::Prometheus,
uid: DATASOURCE_UID.to_owned(),
2026-07-24 18:22:49 +03:00
}
}
}
// Reused by the `input` model, hence `Deserialize`.
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(tag = "mode")]
pub enum Color {
Fixed {
#[serde(rename = "fixedColor")]
fixed_color: String,
},
PaletteClassic,
2026-07-24 18:22:49 +03:00
}
impl Color {
pub fn fixed(color: impl Into<String>) -> Self {
Self::Fixed {
fixed_color: color.into(),
2026-07-24 18:22:49 +03:00
}
}
#[must_use]
pub const fn palette_classic() -> Self {
Self::PaletteClassic
}
2026-07-24 18:22:49 +03:00
}
#[derive(Serialize)]
pub struct LineStyle {
pub dash: [u32; 2],
pub fill: Fill,
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum PropertyValue {
Color(Color),
LineStyle(LineStyle),
}
#[derive(Serialize)]
pub struct OverrideProperty {
pub id: PropertyId,
pub value: PropertyValue,
}
#[derive(Serialize)]
pub struct Matcher {
pub id: MatcherKind,
pub options: String,
}
#[derive(Serialize)]
pub struct Defaults {
#[serde(default, skip_serializing_if = "Option::is_none")]
2026-07-24 18:22:49 +03:00
pub color: Option<Color>,
#[serde(default, skip_serializing_if = "Option::is_none")]
2026-07-24 18:22:49 +03:00
pub custom: Option<Custom>,
2026-07-28 02:15:43 +03:00
pub unit: Unit,
#[serde(default, skip_serializing_if = "Option::is_none")]
2026-07-24 18:22:49 +03:00
pub decimals: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thresholds: Option<Thresholds>,
2026-07-24 18:22:49 +03:00
}
#[derive(Serialize)]
pub struct FieldConfig {
pub defaults: Defaults,
pub overrides: Vec<FieldOverride>,
}
#[derive(Serialize)]
pub struct ReduceOptions {
pub calcs: Vec<Calc>,
// Empty string means "all fields"; genuinely free-form, not a vocabulary.
pub fields: String,
2026-07-24 18:22:49 +03:00
pub values: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StatOptions {
pub color_mode: StatColorMode,
pub graph_mode: GraphMode,
pub reduce_options: ReduceOptions,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Legend {
pub display_mode: LegendDisplay,
pub placement: Placement,
pub calcs: Vec<Calc>,
}
#[derive(Serialize)]
pub struct Tooltip {
pub mode: TooltipMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
2026-07-24 18:22:49 +03:00
pub sort: Option<SortOrder>,
}
#[derive(Serialize)]
pub struct TimeSeriesOptions {
pub legend: Legend,
pub tooltip: Tooltip,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GaugeOptions {
pub reduce_options: ReduceOptions,
pub show_threshold_labels: bool,
pub show_threshold_markers: bool,
}
2026-07-24 18:22:49 +03:00
#[derive(Serialize)]
#[serde(untagged)]
pub enum Options {
Stat(StatOptions),
TimeSeries(TimeSeriesOptions),
Gauge(GaugeOptions),
2026-07-24 18:22:49 +03:00
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum VariableKind {
/// A fixed list of choices, spelled out in the dashboard itself.
Custom,
}
/// One choice in a [`Variable`] dropdown: `text` is displayed, `value` is what
/// `$name` interpolates to in a query.
#[derive(Clone, Serialize)]
pub struct VariableOption {
pub selected: bool,
pub text: String,
pub value: String,
}
/// A dashboard-level dropdown, rendered in the top bar.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Variable {
pub current: VariableOption,
pub include_all: bool,
pub label: String,
pub multi: bool,
pub name: String,
pub options: Vec<VariableOption>,
/// Grafana's own encoding of `options`, as `text : value` pairs.
pub query: String,
#[serde(rename = "type")]
pub kind: VariableKind,
}
#[derive(Serialize, Default)]
pub struct Templating {
pub list: Vec<Variable>,
}
2026-07-24 18:22:49 +03:00
#[derive(Clone, Copy, Serialize)]
pub struct GridPos {
pub h: u32,
pub w: u32,
pub x: u32,
pub y: u32,
}
/// A fully positioned panel, ready to serialize.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PanelModel {
pub datasource: Datasource,
pub field_config: FieldConfig,
pub grid_pos: GridPos,
pub id: u32,
pub options: Options,
pub targets: Vec<Target>,
pub title: String,
#[serde(rename = "type")]
pub panel_type: PanelType,
}
#[expect(
clippy::trailing_empty_array,
reason = "Grafana expects `list: []` for the blocks we don't populate"
)]
2026-07-24 18:22:49 +03:00
#[derive(Serialize, Default)]
pub struct EmptyList {
pub list: [u8; 0],
}
#[derive(Serialize)]
pub struct TimeRange {
// Free-form Grafana time expressions, not a closed vocabulary.
pub from: String,
pub to: String,
2026-07-24 18:22:49 +03:00
}