@intentius/chant-lexicon-grafana 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/build.d.ts +116 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/catalog.d.ts +26 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +8 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +21 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +7 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/codegen/schema-types.d.ts +41 -0
- package/dist/codegen/schema-types.d.ts.map +1 -0
- package/dist/composites/agent-dashboard.d.ts +70 -0
- package/dist/composites/agent-dashboard.d.ts.map +1 -0
- package/dist/composites/catalog.d.ts +3 -0
- package/dist/composites/catalog.d.ts.map +1 -0
- package/dist/composites/index.d.ts +12 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/red-dashboard.d.ts +58 -0
- package/dist/composites/red-dashboard.d.ts.map +1 -0
- package/dist/composites/shared.d.ts +68 -0
- package/dist/composites/shared.d.ts.map +1 -0
- package/dist/composites/slo-dashboard.d.ts +62 -0
- package/dist/composites/slo-dashboard.d.ts.map +1 -0
- package/dist/dashboard.d.ts +84 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/datasource.d.ts +67 -0
- package/dist/datasource.d.ts.map +1 -0
- package/dist/detect.d.ts +20 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +22 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/graf101.d.ts +8 -0
- package/dist/lint/post-synth/graf101.d.ts.map +1 -0
- package/dist/lint/post-synth/graf102.d.ts +8 -0
- package/dist/lint/post-synth/graf102.d.ts.map +1 -0
- package/dist/lint/post-synth/graf103.d.ts +8 -0
- package/dist/lint/post-synth/graf103.d.ts.map +1 -0
- package/dist/lint/post-synth/graf104.d.ts +8 -0
- package/dist/lint/post-synth/graf104.d.ts.map +1 -0
- package/dist/lint/post-synth/graf105.d.ts +8 -0
- package/dist/lint/post-synth/graf105.d.ts.map +1 -0
- package/dist/lint/post-synth/graf106.d.ts +8 -0
- package/dist/lint/post-synth/graf106.d.ts.map +1 -0
- package/dist/lint/post-synth/graf107.d.ts +8 -0
- package/dist/lint/post-synth/graf107.d.ts.map +1 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts +16 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/rules/grafana-ast.d.ts +21 -0
- package/dist/lint/rules/grafana-ast.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +6 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/literal-secret.d.ts +12 -0
- package/dist/lint/rules/literal-secret.d.ts.map +1 -0
- package/dist/lint/rules/uid-syntax.d.ts +12 -0
- package/dist/lint/rules/uid-syntax.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +4 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +4 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +107 -0
- package/dist/okf/index.md +39 -0
- package/dist/okf/rules/GRAF001.md +16 -0
- package/dist/okf/rules/GRAF002.md +15 -0
- package/dist/okf/rules/GRAF101.md +15 -0
- package/dist/okf/rules/GRAF102.md +15 -0
- package/dist/okf/rules/GRAF103.md +11 -0
- package/dist/okf/rules/GRAF104.md +15 -0
- package/dist/okf/rules/GRAF105.md +11 -0
- package/dist/okf/rules/GRAF106.md +11 -0
- package/dist/okf/rules/GRAF107.md +11 -0
- package/dist/okf/types/ConstantVariable.md +9 -0
- package/dist/okf/types/CustomVariable.md +9 -0
- package/dist/okf/types/Dashboard.md +14 -0
- package/dist/okf/types/DashboardProvider.md +9 -0
- package/dist/okf/types/Datasource.md +16 -0
- package/dist/okf/types/DatasourceVariable.md +9 -0
- package/dist/okf/types/GaugePanel.md +9 -0
- package/dist/okf/types/HeatmapPanel.md +9 -0
- package/dist/okf/types/IntervalVariable.md +9 -0
- package/dist/okf/types/LogsPanel.md +9 -0
- package/dist/okf/types/LokiQuery.md +9 -0
- package/dist/okf/types/PromQuery.md +9 -0
- package/dist/okf/types/QueryVariable.md +9 -0
- package/dist/okf/types/Row.md +9 -0
- package/dist/okf/types/StatPanel.md +9 -0
- package/dist/okf/types/TablePanel.md +9 -0
- package/dist/okf/types/TempoQuery.md +9 -0
- package/dist/okf/types/TextPanel.md +9 -0
- package/dist/okf/types/TextboxVariable.md +9 -0
- package/dist/okf/types/TimeSeriesPanel.md +9 -0
- package/dist/okf/types/TracesPanel.md +9 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/panels.d.ts +138 -0
- package/dist/panels.d.ts.map +1 -0
- package/dist/pin.d.ts +32 -0
- package/dist/pin.d.ts.map +1 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/query.d.ts +81 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/rules/graf101.ts +17 -0
- package/dist/rules/graf102.ts +17 -0
- package/dist/rules/graf103.ts +17 -0
- package/dist/rules/graf104.ts +17 -0
- package/dist/rules/graf105.ts +17 -0
- package/dist/rules/graf106.ts +17 -0
- package/dist/rules/graf107.ts +17 -0
- package/dist/rules/grafana-ast.ts +70 -0
- package/dist/rules/grafana-helpers.ts +68 -0
- package/dist/rules/literal-secret.ts +48 -0
- package/dist/rules/uid-syntax.ts +56 -0
- package/dist/schema/dashboard.gen.d.ts +1231 -0
- package/dist/schema/dashboard.gen.d.ts.map +1 -0
- package/dist/schema/gauge.gen.d.ts +57 -0
- package/dist/schema/gauge.gen.d.ts.map +1 -0
- package/dist/schema/heatmap.gen.d.ts +295 -0
- package/dist/schema/heatmap.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +18 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/logs.gen.d.ts +29 -0
- package/dist/schema/logs.gen.d.ts.map +1 -0
- package/dist/schema/loki.gen.d.ts +69 -0
- package/dist/schema/loki.gen.d.ts.map +1 -0
- package/dist/schema/prometheus.gen.d.ts +169 -0
- package/dist/schema/prometheus.gen.d.ts.map +1 -0
- package/dist/schema/stat.gen.d.ts +75 -0
- package/dist/schema/stat.gen.d.ts.map +1 -0
- package/dist/schema/table.gen.d.ts +295 -0
- package/dist/schema/table.gen.d.ts.map +1 -0
- package/dist/schema/tempo.gen.d.ts +136 -0
- package/dist/schema/tempo.gen.d.ts.map +1 -0
- package/dist/schema/text.gen.d.ts +16 -0
- package/dist/schema/text.gen.d.ts.map +1 -0
- package/dist/schema/timeseries.gen.d.ts +179 -0
- package/dist/schema/timeseries.gen.d.ts.map +1 -0
- package/dist/schema-validate.d.ts +20 -0
- package/dist/schema-validate.d.ts.map +1 -0
- package/dist/serializer.d.ts +22 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skill-defs.d.ts +3 -0
- package/dist/skill-defs.d.ts.map +1 -0
- package/dist/skills/chant-grafana-provisioning.md +42 -0
- package/dist/skills/chant-grafana.md +93 -0
- package/dist/spec/fetch-cli.d.ts +3 -0
- package/dist/spec/fetch-cli.d.ts.map +1 -0
- package/dist/spec/schemas.d.ts +25 -0
- package/dist/spec/schemas.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/util.d.ts +17 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate-output.d.ts +42 -0
- package/dist/validate-output.d.ts.map +1 -0
- package/dist/validate.d.ts +12 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/variables.d.ts +100 -0
- package/dist/variables.d.ts.map +1 -0
- package/package.json +79 -0
- package/src/build.test.ts +210 -0
- package/src/build.ts +573 -0
- package/src/catalog.ts +116 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +97 -0
- package/src/codegen/generate-cli.ts +8 -0
- package/src/codegen/generate.ts +83 -0
- package/src/codegen/package.ts +41 -0
- package/src/codegen/schema-types.ts +173 -0
- package/src/composites/agent-dashboard.ts +235 -0
- package/src/composites/catalog.test.ts +37 -0
- package/src/composites/catalog.ts +223 -0
- package/src/composites/composites.test.ts +314 -0
- package/src/composites/index.ts +12 -0
- package/src/composites/red-dashboard.ts +159 -0
- package/src/composites/shared.ts +94 -0
- package/src/composites/slo-dashboard.ts +177 -0
- package/src/dashboard.ts +123 -0
- package/src/datasource.ts +93 -0
- package/src/detect.ts +30 -0
- package/src/generated/lexicon-grafana.json +107 -0
- package/src/import.e2e.test.ts +147 -0
- package/src/import.test.ts +45 -0
- package/src/index.ts +178 -0
- package/src/lint/audit-catalog.ts +87 -0
- package/src/lint/post-synth/graf101.ts +17 -0
- package/src/lint/post-synth/graf102.ts +17 -0
- package/src/lint/post-synth/graf103.ts +17 -0
- package/src/lint/post-synth/graf104.ts +17 -0
- package/src/lint/post-synth/graf105.ts +17 -0
- package/src/lint/post-synth/graf106.ts +17 -0
- package/src/lint/post-synth/graf107.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +68 -0
- package/src/lint/post-synth/index.ts +19 -0
- package/src/lint/post-synth/post-synth.test.ts +231 -0
- package/src/lint/rules/grafana-ast.ts +70 -0
- package/src/lint/rules/index.ts +9 -0
- package/src/lint/rules/literal-secret.ts +48 -0
- package/src/lint/rules/rules.test.ts +70 -0
- package/src/lint/rules/uid-syntax.ts +56 -0
- package/src/lsp/completions.test.ts +31 -0
- package/src/lsp/completions.ts +15 -0
- package/src/lsp/hover.test.ts +25 -0
- package/src/lsp/hover.ts +28 -0
- package/src/package-cli.ts +17 -0
- package/src/panels.ts +275 -0
- package/src/pin.ts +68 -0
- package/src/plugin.test.ts +50 -0
- package/src/plugin.ts +116 -0
- package/src/query.ts +160 -0
- package/src/schema/dashboard.gen.ts +1289 -0
- package/src/schema/gauge.gen.ts +64 -0
- package/src/schema/heatmap.gen.ts +321 -0
- package/src/schema/index.ts +17 -0
- package/src/schema/logs.gen.ts +34 -0
- package/src/schema/loki.gen.ts +77 -0
- package/src/schema/prometheus.gen.ts +181 -0
- package/src/schema/stat.gen.ts +86 -0
- package/src/schema/table.gen.ts +334 -0
- package/src/schema/tempo.gen.ts +146 -0
- package/src/schema/text.gen.ts +22 -0
- package/src/schema/timeseries.gen.ts +208 -0
- package/src/schema-validate.ts +143 -0
- package/src/schema.test.ts +76 -0
- package/src/serializer.test.ts +170 -0
- package/src/serializer.ts +37 -0
- package/src/skill-defs.ts +31 -0
- package/src/skills/chant-grafana-provisioning.md +42 -0
- package/src/skills/chant-grafana.md +93 -0
- package/src/spec/fetch-cli.ts +27 -0
- package/src/spec/schemas/dashboard.jsonschema.json +1901 -0
- package/src/spec/schemas/gauge.jsonschema.json +114 -0
- package/src/spec/schemas/heatmap.jsonschema.json +480 -0
- package/src/spec/schemas/logs.jsonschema.json +94 -0
- package/src/spec/schemas/loki.jsonschema.json +118 -0
- package/src/spec/schemas/prometheus.jsonschema.json +276 -0
- package/src/spec/schemas/stat.jsonschema.json +163 -0
- package/src/spec/schemas/table.jsonschema.json +751 -0
- package/src/spec/schemas/tempo.jsonschema.json +225 -0
- package/src/spec/schemas/text.jsonschema.json +72 -0
- package/src/spec/schemas/timeseries.jsonschema.json +471 -0
- package/src/spec/schemas.ts +56 -0
- package/src/util.ts +43 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate-output.ts +380 -0
- package/src/validate.ts +108 -0
- package/src/variables.ts +161 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Code generated by chant generate from the vendored Grafana schema
|
|
2
|
+
// src/spec/schemas/timeseries.jsonschema.json (see src/pin.ts). DO NOT EDIT.
|
|
3
|
+
/* eslint-disable */
|
|
4
|
+
|
|
5
|
+
export interface Options {
|
|
6
|
+
timezone?: TimeZone[];
|
|
7
|
+
legend: VizLegendOptions;
|
|
8
|
+
tooltip: VizTooltipOptions;
|
|
9
|
+
orientation?: VizOrientation;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type FieldConfig = GraphFieldConfig;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A specific timezone from https://en.wikipedia.org/wiki/Tz_database
|
|
16
|
+
*/
|
|
17
|
+
export type TimeZone = string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* TODO docs
|
|
21
|
+
*/
|
|
22
|
+
export interface VizLegendOptions {
|
|
23
|
+
displayMode: LegendDisplayMode;
|
|
24
|
+
placement: LegendPlacement;
|
|
25
|
+
showLegend: boolean;
|
|
26
|
+
asTable?: boolean;
|
|
27
|
+
isVisible?: boolean;
|
|
28
|
+
sortBy?: string;
|
|
29
|
+
sortDesc?: boolean;
|
|
30
|
+
width?: number;
|
|
31
|
+
calcs: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* TODO docs
|
|
36
|
+
*/
|
|
37
|
+
export interface VizTooltipOptions {
|
|
38
|
+
mode: TooltipDisplayMode;
|
|
39
|
+
sort: SortOrder;
|
|
40
|
+
maxWidth?: number;
|
|
41
|
+
maxHeight?: number;
|
|
42
|
+
hideZeros?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* TODO docs
|
|
47
|
+
*/
|
|
48
|
+
export type VizOrientation = "auto" | "vertical" | "horizontal";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* TODO docs
|
|
52
|
+
*/
|
|
53
|
+
export interface GraphFieldConfig {
|
|
54
|
+
drawStyle?: GraphDrawStyle;
|
|
55
|
+
gradientMode?: GraphGradientMode;
|
|
56
|
+
thresholdsStyle?: GraphThresholdsStyleConfig;
|
|
57
|
+
transform?: GraphTransform;
|
|
58
|
+
lineColor?: string;
|
|
59
|
+
lineWidth?: number;
|
|
60
|
+
lineInterpolation?: LineInterpolation;
|
|
61
|
+
lineStyle?: LineStyle;
|
|
62
|
+
fillColor?: string;
|
|
63
|
+
fillOpacity?: number;
|
|
64
|
+
showPoints?: VisibilityMode;
|
|
65
|
+
pointSize?: number;
|
|
66
|
+
pointColor?: string;
|
|
67
|
+
axisPlacement?: AxisPlacement;
|
|
68
|
+
axisColorMode?: AxisColorMode;
|
|
69
|
+
axisLabel?: string;
|
|
70
|
+
axisWidth?: number;
|
|
71
|
+
axisSoftMin?: number;
|
|
72
|
+
axisSoftMax?: number;
|
|
73
|
+
axisGridShow?: boolean;
|
|
74
|
+
scaleDistribution?: ScaleDistributionConfig;
|
|
75
|
+
axisCenteredZero?: boolean;
|
|
76
|
+
barAlignment?: BarAlignment;
|
|
77
|
+
barWidthFactor?: number;
|
|
78
|
+
stacking?: StackingConfig;
|
|
79
|
+
hideFrom?: HideSeriesConfig;
|
|
80
|
+
insertNulls?: boolean | number;
|
|
81
|
+
/**
|
|
82
|
+
* Indicate if null values should be treated as gaps or connected.
|
|
83
|
+
* When the value is a number, it represents the maximum delta in the
|
|
84
|
+
* X axis that should be considered connected. For timeseries, this is milliseconds
|
|
85
|
+
*/
|
|
86
|
+
spanNulls?: boolean | number;
|
|
87
|
+
fillBelowTo?: string;
|
|
88
|
+
pointSymbol?: string;
|
|
89
|
+
axisBorderShow?: boolean;
|
|
90
|
+
barMaxWidth?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* TODO docs
|
|
95
|
+
* Note: "hidden" needs to remain as an option for plugins compatibility
|
|
96
|
+
*/
|
|
97
|
+
export type LegendDisplayMode = "list" | "table" | "hidden";
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* TODO docs
|
|
101
|
+
*/
|
|
102
|
+
export type LegendPlacement = "bottom" | "right";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* TODO docs
|
|
106
|
+
*/
|
|
107
|
+
export type TooltipDisplayMode = "single" | "multi" | "none";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* TODO docs
|
|
111
|
+
*/
|
|
112
|
+
export type SortOrder = "asc" | "desc" | "none";
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* TODO docs
|
|
116
|
+
*/
|
|
117
|
+
export type GraphDrawStyle = "line" | "bars" | "points";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* TODO docs
|
|
121
|
+
*/
|
|
122
|
+
export type GraphGradientMode = "none" | "opacity" | "hue" | "scheme";
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* TODO docs
|
|
126
|
+
*/
|
|
127
|
+
export interface GraphThresholdsStyleConfig {
|
|
128
|
+
mode: GraphThresholdsStyleMode;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* TODO docs
|
|
133
|
+
*/
|
|
134
|
+
export type GraphTransform = "constant" | "negative-Y";
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* TODO docs
|
|
138
|
+
*/
|
|
139
|
+
export type LineInterpolation = "linear" | "smooth" | "stepBefore" | "stepAfter";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* TODO docs
|
|
143
|
+
*/
|
|
144
|
+
export interface LineStyle {
|
|
145
|
+
fill?: "solid" | "dash" | "dot" | "square";
|
|
146
|
+
dash?: number[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* TODO docs
|
|
151
|
+
*/
|
|
152
|
+
export type VisibilityMode = "auto" | "never" | "always";
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* TODO docs
|
|
156
|
+
*/
|
|
157
|
+
export type AxisPlacement = "auto" | "top" | "right" | "bottom" | "left" | "hidden";
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* TODO docs
|
|
161
|
+
*/
|
|
162
|
+
export type AxisColorMode = "text" | "series";
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* TODO docs
|
|
166
|
+
*/
|
|
167
|
+
export interface ScaleDistributionConfig {
|
|
168
|
+
type: ScaleDistribution;
|
|
169
|
+
log?: number;
|
|
170
|
+
linearThreshold?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* TODO docs
|
|
175
|
+
*/
|
|
176
|
+
export type BarAlignment = -1 | 0 | 1;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* TODO docs
|
|
180
|
+
*/
|
|
181
|
+
export interface StackingConfig {
|
|
182
|
+
mode?: StackingMode;
|
|
183
|
+
group?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* TODO docs
|
|
188
|
+
*/
|
|
189
|
+
export interface HideSeriesConfig {
|
|
190
|
+
tooltip: boolean;
|
|
191
|
+
legend: boolean;
|
|
192
|
+
viz: boolean;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* TODO docs
|
|
197
|
+
*/
|
|
198
|
+
export type GraphThresholdsStyleMode = "off" | "line" | "dashed" | "area" | "line+area" | "dashed+area" | "series";
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* TODO docs
|
|
202
|
+
*/
|
|
203
|
+
export type ScaleDistribution = "linear" | "log" | "ordinal" | "symlog";
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* TODO docs
|
|
207
|
+
*/
|
|
208
|
+
export type StackingMode = "none" | "normal" | "percent";
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate built Grafana JSON against the vendored schemas at
|
|
3
|
+
* `GRAFANA_SCHEMA_PIN`, offline.
|
|
4
|
+
*
|
|
5
|
+
* The dashboard is checked against the dashboard schema as it stands. Panel
|
|
6
|
+
* options, `fieldConfig.defaults.custom` and each query are checked against
|
|
7
|
+
* their plugin's schema with every `required` list removed, because Grafana
|
|
8
|
+
* fills in options a panel leaves out: what still fails is a key the schema
|
|
9
|
+
* doesn't have or a value it doesn't allow.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Ajv, { type ValidateFunction, type ErrorObject } from "ajv";
|
|
13
|
+
import type { SchemaName } from "./pin";
|
|
14
|
+
import { loadSchema } from "./spec/schemas";
|
|
15
|
+
import { panelDefinitionFor } from "./panels";
|
|
16
|
+
import { queryDefinitionFor } from "./query";
|
|
17
|
+
|
|
18
|
+
export interface SchemaProblem {
|
|
19
|
+
/** JSON pointer into the dashboard. */
|
|
20
|
+
path: string;
|
|
21
|
+
message: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let ajv: Ajv | undefined;
|
|
25
|
+
function getAjv(): Ajv {
|
|
26
|
+
ajv ??= new Ajv({ strict: false, allErrors: true, validateFormats: false });
|
|
27
|
+
return ajv;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const compiled = new Map<string, ValidateFunction>();
|
|
31
|
+
|
|
32
|
+
function stripRequired(node: unknown): unknown {
|
|
33
|
+
if (Array.isArray(node)) return node.map(stripRequired);
|
|
34
|
+
if (node && typeof node === "object") {
|
|
35
|
+
const out: Record<string, unknown> = {};
|
|
36
|
+
for (const [k, v] of Object.entries(node)) {
|
|
37
|
+
if (k === "required" && Array.isArray(v)) continue;
|
|
38
|
+
out[k] = stripRequired(v);
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A validator for one definition of one vendored schema (the root `$ref` when `def` is left out). */
|
|
46
|
+
function validatorFor(name: SchemaName, def: string | undefined, lenient: boolean): ValidateFunction | undefined {
|
|
47
|
+
const key = `${name}#${def ?? ""}#${lenient}`;
|
|
48
|
+
const hit = compiled.get(key);
|
|
49
|
+
if (hit) return hit;
|
|
50
|
+
const schema = loadSchema(name);
|
|
51
|
+
const definitions = (schema.definitions ?? {}) as Record<string, unknown>;
|
|
52
|
+
const ref = def ? `#/definitions/${def}` : (schema.$ref as string | undefined);
|
|
53
|
+
if (!ref) return undefined;
|
|
54
|
+
if (def && !(def in definitions)) return undefined;
|
|
55
|
+
const root: Record<string, unknown> = {
|
|
56
|
+
$schema: schema.$schema,
|
|
57
|
+
$ref: ref,
|
|
58
|
+
definitions: lenient ? stripRequired(definitions) : definitions,
|
|
59
|
+
};
|
|
60
|
+
const fn = getAjv().compile(root);
|
|
61
|
+
compiled.set(key, fn);
|
|
62
|
+
return fn;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function problems(errors: ErrorObject[] | null | undefined, prefix: string): SchemaProblem[] {
|
|
66
|
+
const seen = new Set<string>();
|
|
67
|
+
const out: SchemaProblem[] = [];
|
|
68
|
+
for (const e of errors ?? []) {
|
|
69
|
+
// oneOf/anyOf wrappers repeat what their branches already said.
|
|
70
|
+
if (e.keyword === "oneOf" || e.keyword === "anyOf" || e.keyword === "if") continue;
|
|
71
|
+
const extra =
|
|
72
|
+
e.keyword === "additionalProperties"
|
|
73
|
+
? ` ("${(e.params as { additionalProperty: string }).additionalProperty}")`
|
|
74
|
+
: e.keyword === "enum"
|
|
75
|
+
? `: ${(e.params as { allowedValues: unknown[] }).allowedValues.map((v) => JSON.stringify(v)).join(", ")}`
|
|
76
|
+
: "";
|
|
77
|
+
const path = `${prefix}${e.instancePath}` || "/";
|
|
78
|
+
const message = `${e.message ?? "is invalid"}${extra}`;
|
|
79
|
+
const k = `${path} ${message}`;
|
|
80
|
+
if (seen.has(k)) continue;
|
|
81
|
+
seen.add(k);
|
|
82
|
+
out.push({ path, message });
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Json = Record<string, unknown>;
|
|
88
|
+
|
|
89
|
+
function panelsOf(dashboard: Json): Array<{ panel: Json; path: string }> {
|
|
90
|
+
const out: Array<{ panel: Json; path: string }> = [];
|
|
91
|
+
const top = Array.isArray(dashboard.panels) ? (dashboard.panels as Json[]) : [];
|
|
92
|
+
top.forEach((p, i) => {
|
|
93
|
+
out.push({ panel: p, path: `/panels/${i}` });
|
|
94
|
+
if (p?.type === "row" && Array.isArray(p.panels)) {
|
|
95
|
+
(p.panels as Json[]).forEach((c, j) => out.push({ panel: c, path: `/panels/${i}/panels/${j}` }));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Everything in one dashboard that the pinned schemas reject. */
|
|
102
|
+
export function validateDashboardSchema(dashboard: Json): SchemaProblem[] {
|
|
103
|
+
const out: SchemaProblem[] = [];
|
|
104
|
+
// The envelope and each panel are checked separately: the schema's
|
|
105
|
+
// `oneOf [Panel, RowPanel]` would otherwise report every panel error twice,
|
|
106
|
+
// once per branch.
|
|
107
|
+
const whole = validatorFor("dashboard", undefined, false)!;
|
|
108
|
+
const envelope = Array.isArray(dashboard.panels) ? { ...dashboard, panels: [] } : dashboard;
|
|
109
|
+
if (!whole(envelope)) out.push(...problems(whole.errors, ""));
|
|
110
|
+
const panelSchema = validatorFor("dashboard", "Panel", false)!;
|
|
111
|
+
const rowSchema = validatorFor("dashboard", "RowPanel", false)!;
|
|
112
|
+
|
|
113
|
+
for (const { panel, path } of panelsOf(dashboard)) {
|
|
114
|
+
if (!panel || typeof panel !== "object") {
|
|
115
|
+
out.push({ path, message: "must be object" });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (panel.type === "row") {
|
|
119
|
+
const header = Array.isArray(panel.panels) ? { ...panel, panels: [] } : panel;
|
|
120
|
+
if (!rowSchema(header)) out.push(...problems(rowSchema.errors, path));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (!panelSchema(panel)) out.push(...problems(panelSchema.errors, path));
|
|
124
|
+
const def = typeof panel.type === "string" ? panelDefinitionFor(panel.type) : undefined;
|
|
125
|
+
if (def?.schema) {
|
|
126
|
+
const options = validatorFor(def.schema, "Options", true);
|
|
127
|
+
if (options && panel.options !== undefined && !options(panel.options)) out.push(...problems(options.errors, `${path}/options`));
|
|
128
|
+
const custom = (panel.fieldConfig as { defaults?: { custom?: unknown } } | undefined)?.defaults?.custom;
|
|
129
|
+
const fc = validatorFor(def.schema, "FieldConfig", true);
|
|
130
|
+
if (fc && custom !== undefined && !fc(custom)) out.push(...problems(fc.errors, `${path}/fieldConfig/defaults/custom`));
|
|
131
|
+
}
|
|
132
|
+
const panelType = (panel.datasource as { type?: string } | undefined)?.type;
|
|
133
|
+
const targets = Array.isArray(panel.targets) ? (panel.targets as Json[]) : [];
|
|
134
|
+
targets.forEach((t, k) => {
|
|
135
|
+
const type = (t?.datasource as { type?: string } | undefined)?.type ?? panelType;
|
|
136
|
+
const qdef = type ? queryDefinitionFor(type) : undefined;
|
|
137
|
+
if (!qdef?.schema) return;
|
|
138
|
+
const v = validatorFor(qdef.schema, undefined, true);
|
|
139
|
+
if (v && !v(t)) out.push(...problems(v.errors, `${path}/targets/${k}`));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { GRAFANA_SCHEMA_PIN, SCHEMA_NAMES, schemaUrl } from "./pin";
|
|
6
|
+
import { digestMismatches, loadSchema } from "./spec/schemas";
|
|
7
|
+
import { schemaModules } from "./codegen/generate";
|
|
8
|
+
import { schemaModule, tsType } from "./codegen/schema-types";
|
|
9
|
+
import { DASHBOARD_SCHEMA_VERSION } from "./schema/dashboard.gen";
|
|
10
|
+
|
|
11
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
12
|
+
|
|
13
|
+
describe("the schema pin", () => {
|
|
14
|
+
test("names a commit and a digest for every vendored schema", () => {
|
|
15
|
+
expect(GRAFANA_SCHEMA_PIN.commit).toMatch(/^[0-9a-f]{40}$/);
|
|
16
|
+
expect(Object.keys(GRAFANA_SCHEMA_PIN.files).sort()).toEqual([...SCHEMA_NAMES].sort());
|
|
17
|
+
expect(schemaUrl("stat")).toBe(`https://raw.githubusercontent.com/grafana/grafana-foundation-sdk/${GRAFANA_SCHEMA_PIN.commit}/jsonschema/stat.jsonschema.json`);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("every vendored schema matches its pinned digest", () => {
|
|
21
|
+
expect(digestMismatches()).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("the committed schema types are what generate writes", () => {
|
|
25
|
+
for (const [rel, source] of Object.entries(schemaModules())) {
|
|
26
|
+
expect({ rel, same: readFileSync(join(pkgDir, rel), "utf-8") === source }).toEqual({ rel, same: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("the emitted schemaVersion is the pinned dashboard schema's default", () => {
|
|
31
|
+
const defs = loadSchema("dashboard").definitions as Record<string, { properties: Record<string, { default?: unknown }> }>;
|
|
32
|
+
expect(DASHBOARD_SCHEMA_VERSION).toBe(defs.Dashboard.properties.schemaVersion.default);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("schema-types", () => {
|
|
37
|
+
test("maps the draft-07 subset cog emits", () => {
|
|
38
|
+
expect(tsType({ type: "string" })).toBe("string");
|
|
39
|
+
expect(tsType({ type: "integer" })).toBe("number");
|
|
40
|
+
expect(tsType({ enum: ["a", "b"] })).toBe('"a" | "b"');
|
|
41
|
+
expect(tsType({ const: "row" })).toBe('"row"');
|
|
42
|
+
expect(tsType({ $ref: "#/definitions/dataquery" })).toBe("Dataquery");
|
|
43
|
+
expect(tsType({ anyOf: [{ type: "integer" }, { type: "null" }] })).toBe("number | null");
|
|
44
|
+
expect(tsType({ type: "array", items: { anyOf: [{ type: "string" }, { type: "number" }] } })).toBe("(string | number)[]");
|
|
45
|
+
expect(tsType({ type: "object", additionalProperties: { type: "string" } })).toBe("Record<string, string>");
|
|
46
|
+
expect(tsType({ type: "object", additionalProperties: {} })).toBe("Record<string, unknown>");
|
|
47
|
+
expect(tsType({})).toBe("unknown");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("keeps required, marks the rest optional, and carries descriptions and deprecations", () => {
|
|
51
|
+
const { source, types } = schemaModule("text", {
|
|
52
|
+
definitions: {
|
|
53
|
+
Options: {
|
|
54
|
+
type: "object",
|
|
55
|
+
required: ["mode"],
|
|
56
|
+
properties: {
|
|
57
|
+
mode: { $ref: "#/definitions/TextMode", description: "How content renders" },
|
|
58
|
+
"odd-key": { type: "boolean", deprecated: true },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
TextMode: { enum: ["html", "markdown"] },
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
expect(types).toEqual(["Options", "TextMode"]);
|
|
65
|
+
expect(source).toContain("export interface Options {");
|
|
66
|
+
expect(source).toContain(" mode: TextMode;");
|
|
67
|
+
expect(source).toContain(' "odd-key"?: boolean;');
|
|
68
|
+
expect(source).toContain(" * How content renders");
|
|
69
|
+
expect(source).toContain(" * @deprecated");
|
|
70
|
+
expect(source).toContain('export type TextMode = "html" | "markdown";');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("refuses a remote $ref", () => {
|
|
74
|
+
expect(() => tsType({ $ref: "common.json#/definitions/X" })).toThrow(/local/);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The grafana serializer, sectioned by the serializer checklist in the
|
|
3
|
+
* lexicon-authoring docs (testing.mdx), plus format-specific cases.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { load } from "js-yaml";
|
|
7
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
8
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
9
|
+
import { grafanaSerializer } from "./serializer";
|
|
10
|
+
import { Datasource } from "./datasource";
|
|
11
|
+
import { Dashboard, DashboardProvider } from "./dashboard";
|
|
12
|
+
import { StatPanel, TimeSeriesPanel } from "./panels";
|
|
13
|
+
import { PromQuery } from "./query";
|
|
14
|
+
import { DATASOURCES_FILE, DASHBOARD_PROVIDERS_FILE } from "./build";
|
|
15
|
+
import { DASHBOARD_SCHEMA_VERSION } from "./schema/dashboard.gen";
|
|
16
|
+
import { validateDashboardSchema } from "./schema-validate";
|
|
17
|
+
|
|
18
|
+
function run(entries: Record<string, Declarable>): SerializerResult {
|
|
19
|
+
const out = grafanaSerializer.serialize(new Map(Object.entries(entries)));
|
|
20
|
+
if (typeof out === "string") throw new Error(`expected files, got ${JSON.stringify(out)}`);
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
|
|
25
|
+
const up = new PromQuery({ expr: "sum(up)" });
|
|
26
|
+
const upPanel = new StatPanel({ title: "Up", datasource: prometheus, targets: [up] });
|
|
27
|
+
|
|
28
|
+
describe("grafana serializer", () => {
|
|
29
|
+
// 1, 2
|
|
30
|
+
test("name and rule prefix", () => {
|
|
31
|
+
expect(grafanaSerializer.name).toBe("grafana");
|
|
32
|
+
expect(grafanaSerializer.rulePrefix).toBe("GRAF");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 3
|
|
36
|
+
test("an empty build serializes to an empty string", () => {
|
|
37
|
+
expect(grafanaSerializer.serialize(new Map())).toBe("");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 4
|
|
41
|
+
test("one dashboard: valid dashboard JSON and an index", () => {
|
|
42
|
+
const out = run({ overview: new Dashboard({ title: "Overview", panels: [upPanel] }) });
|
|
43
|
+
const json = JSON.parse(out.files!["dashboards/overview.json"]);
|
|
44
|
+
expect(json.title).toBe("Overview");
|
|
45
|
+
expect(json.panels).toHaveLength(1);
|
|
46
|
+
expect(validateDashboardSchema(json)).toEqual([]);
|
|
47
|
+
expect(JSON.parse(out.primary).dashboards).toEqual([{ uid: "overview", title: "Overview", file: "dashboards/overview.json" }]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// 5
|
|
51
|
+
test("the uid defaults to the export name as a uid", () => {
|
|
52
|
+
const out = run({ checkoutServiceOverview: new Dashboard({ title: "Checkout" }) });
|
|
53
|
+
expect(Object.keys(out.files!)).toContain("dashboards/checkout-service-overview.json");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 6
|
|
57
|
+
test("an explicit uid is kept", () => {
|
|
58
|
+
const out = run({ overview: new Dashboard({ title: "Overview", uid: "svc-ovw" }) });
|
|
59
|
+
expect(JSON.parse(out.files!["dashboards/svc-ovw.json"]).uid).toBe("svc-ovw");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// 7
|
|
63
|
+
test("several dashboards and datasources: one file each, one datasources file", () => {
|
|
64
|
+
const tempo = new Datasource({ name: "Tempo", type: "tempo", url: "http://tempo:3200" });
|
|
65
|
+
const out = run({ prometheus, tempo, a: new Dashboard({ title: "A" }), b: new Dashboard({ title: "B", folder: "Team" }) });
|
|
66
|
+
expect(Object.keys(out.files!).sort()).toEqual([
|
|
67
|
+
"dashboards/Team/b.json",
|
|
68
|
+
"dashboards/a.json",
|
|
69
|
+
DASHBOARD_PROVIDERS_FILE,
|
|
70
|
+
DATASOURCES_FILE,
|
|
71
|
+
]);
|
|
72
|
+
const ds = load(out.files![DATASOURCES_FILE]) as { datasources: Array<{ name: string; uid: string }> };
|
|
73
|
+
expect(ds.datasources.map((d) => [d.name, d.uid])).toEqual([
|
|
74
|
+
["Prometheus", "prometheus"],
|
|
75
|
+
["Tempo", "tempo"],
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 8
|
|
80
|
+
test("defaults Grafana needs are filled in", () => {
|
|
81
|
+
const out = run({ prometheus, overview: new Dashboard({ title: "Overview", panels: [upPanel] }) });
|
|
82
|
+
const json = JSON.parse(out.files!["dashboards/overview.json"]);
|
|
83
|
+
expect(json.schemaVersion).toBe(DASHBOARD_SCHEMA_VERSION);
|
|
84
|
+
expect(json.time).toEqual({ from: "now-6h", to: "now" });
|
|
85
|
+
expect(json.templating).toEqual({ list: [] });
|
|
86
|
+
expect(json.annotations).toEqual({ list: [] });
|
|
87
|
+
expect(json.panels[0].fieldConfig).toEqual({ defaults: {}, overrides: [] });
|
|
88
|
+
const ds = load(out.files![DATASOURCES_FILE]) as { datasources: Array<Record<string, unknown>> };
|
|
89
|
+
expect(ds.datasources[0]).toMatchObject({ access: "proxy", editable: false });
|
|
90
|
+
const providers = load(out.files![DASHBOARD_PROVIDERS_FILE]) as { providers: Array<Record<string, unknown>> };
|
|
91
|
+
expect(providers.providers).toEqual([
|
|
92
|
+
{
|
|
93
|
+
name: "chant",
|
|
94
|
+
orgId: 1,
|
|
95
|
+
folder: "",
|
|
96
|
+
type: "file",
|
|
97
|
+
disableDeletion: false,
|
|
98
|
+
allowUiUpdates: false,
|
|
99
|
+
updateIntervalSeconds: 30,
|
|
100
|
+
options: { path: "/var/lib/grafana/dashboards", foldersFromFilesStructure: true },
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 9
|
|
106
|
+
test("explicit values override defaults", () => {
|
|
107
|
+
const provider = new DashboardProvider({ name: "team", path: "/dashboards", folder: "Team", allowUiUpdates: true });
|
|
108
|
+
const out = run({
|
|
109
|
+
provider,
|
|
110
|
+
overview: new Dashboard({ title: "Overview", time: { from: "now-1h", to: "now" }, editable: false }),
|
|
111
|
+
});
|
|
112
|
+
const json = JSON.parse(out.files!["dashboards/overview.json"]);
|
|
113
|
+
expect(json.time).toEqual({ from: "now-1h", to: "now" });
|
|
114
|
+
expect(json.editable).toBe(false);
|
|
115
|
+
const providers = load(out.files![DASHBOARD_PROVIDERS_FILE]) as { providers: Array<Record<string, unknown>> };
|
|
116
|
+
expect(providers.providers).toHaveLength(1);
|
|
117
|
+
expect(providers.providers[0]).toMatchObject({
|
|
118
|
+
name: "team",
|
|
119
|
+
folder: "Team",
|
|
120
|
+
allowUiUpdates: true,
|
|
121
|
+
options: { path: "/dashboards", foldersFromFilesStructure: false },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 10
|
|
126
|
+
test("exported panels and queries are inlined, not emitted on their own", () => {
|
|
127
|
+
const out = run({ up, upPanel, prometheus });
|
|
128
|
+
expect(Object.keys(out.files!)).toEqual([DATASOURCES_FILE]);
|
|
129
|
+
expect(JSON.parse(out.primary).dashboards).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 11
|
|
133
|
+
test("output is the same whatever order the props were written in", () => {
|
|
134
|
+
const a = run({ d: new Dashboard({ title: "T", tags: ["x"], refresh: "1m" }) });
|
|
135
|
+
const b = run({ d: new Dashboard({ refresh: "1m", tags: ["x"], title: "T" }) });
|
|
136
|
+
expect(a.files).toEqual(b.files);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 12
|
|
140
|
+
test("datasource references are written as { type, uid }; the index names every file", () => {
|
|
141
|
+
const loki = new Datasource({ name: "Loki", type: "loki", url: "http://loki:3100" });
|
|
142
|
+
const settings = { tracesToLogsV2: { datasourceUid: loki } };
|
|
143
|
+
const tempo = new Datasource({ name: "Tempo", type: "tempo", jsonData: settings });
|
|
144
|
+
const out = run({ loki, tempo, prometheus, overview: new Dashboard({ title: "Overview", panels: [upPanel] }) });
|
|
145
|
+
const json = JSON.parse(out.files!["dashboards/overview.json"]);
|
|
146
|
+
expect(json.panels[0].datasource).toEqual({ type: "prometheus", uid: "prometheus" });
|
|
147
|
+
expect(json.panels[0].targets[0]).toEqual({ datasource: { type: "prometheus", uid: "prometheus" }, refId: "A", expr: "sum(up)" });
|
|
148
|
+
const ds = load(out.files![DATASOURCES_FILE]) as { datasources: Array<{ name: string; jsonData?: unknown }> };
|
|
149
|
+
expect(ds.datasources.find((d) => d.name === "Tempo")?.jsonData).toEqual({ tracesToLogsV2: { datasourceUid: "loki" } });
|
|
150
|
+
expect(JSON.parse(out.primary).files).toEqual(Object.keys(out.files!).sort());
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("round trip: the emitted text parses back to the declared values", () => {
|
|
154
|
+
const panel = new TimeSeriesPanel({ title: "Rate", datasource: prometheus, targets: [up], interval: "30s" });
|
|
155
|
+
const out = run({ d: new Dashboard({ title: "Round trip", description: "desc", tags: ["a", "b"], panels: [panel] }) });
|
|
156
|
+
const json = JSON.parse(out.files!["dashboards/d.json"]);
|
|
157
|
+
expect(json).toMatchObject({ title: "Round trip", description: "desc", tags: ["a", "b"] });
|
|
158
|
+
expect(json.panels[0]).toMatchObject({ type: "timeseries", title: "Rate", interval: "30s" });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("two dashboards with one uid keep both files, so GRAF104 can see them", () => {
|
|
162
|
+
const out = run({ a: new Dashboard({ title: "A", uid: "same" }), b: new Dashboard({ title: "B", uid: "same" }) });
|
|
163
|
+
expect(Object.keys(out.files!).filter((f) => f.startsWith("dashboards/")).sort()).toEqual(["dashboards/same-2.json", "dashboards/same.json"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("entities of other lexicons are ignored", () => {
|
|
167
|
+
const foreign = { lexicon: "k8s", entityType: "K8s::Core::ConfigMap", kind: "resource", props: {} } as unknown as Declarable;
|
|
168
|
+
expect(grafanaSerializer.serialize(new Map([["cm", foreign]]))).toBe("");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grafana serializer.
|
|
3
|
+
*
|
|
4
|
+
* Emits Grafana's own files, as `SerializerResult.files` keyed by path
|
|
5
|
+
* relative to the output directory:
|
|
6
|
+
*
|
|
7
|
+
* - `dashboards/[<folder>/]<uid>.json`: one per dashboard, the JSON Grafana
|
|
8
|
+
* imports as it is (UI import, the HTTP API, or file provisioning).
|
|
9
|
+
* - `provisioning/datasources/chant.yaml`: every declared datasource.
|
|
10
|
+
* - `provisioning/dashboards/chant.yaml`: the provider that loads the
|
|
11
|
+
* dashboard files, a default one unless a `DashboardProvider` is declared.
|
|
12
|
+
*
|
|
13
|
+
* Mount `provisioning/` at `/etc/grafana/provisioning` and `dashboards/` at
|
|
14
|
+
* the provider's path (`/var/lib/grafana/dashboards` by default).
|
|
15
|
+
*
|
|
16
|
+
* The primary output is a small JSON index of what was built. Grafana's
|
|
17
|
+
* files have no metadata channel chant could use without changing what
|
|
18
|
+
* Grafana imports, so no ownership marker is stamped.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
22
|
+
import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
23
|
+
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
24
|
+
import { buildGrafana } from "./build";
|
|
25
|
+
|
|
26
|
+
export const grafanaSerializer: Serializer = {
|
|
27
|
+
name: "grafana",
|
|
28
|
+
rulePrefix: "GRAF",
|
|
29
|
+
|
|
30
|
+
serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): string | SerializerResult {
|
|
31
|
+
const grafana = new Map([...entities].filter(([, e]) => e?.lexicon === "grafana"));
|
|
32
|
+
if (grafana.size === 0) return "";
|
|
33
|
+
const built = buildGrafana(grafana);
|
|
34
|
+
const primary = `${JSON.stringify(built.index, null, 2)}\n`;
|
|
35
|
+
return { primary, files: built.files };
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
|
|
2
|
+
|
|
3
|
+
/** The grafana lexicon's AI skills, read from src/skills/. */
|
|
4
|
+
export const grafanaSkills = createSkillsLoader(import.meta.url, [
|
|
5
|
+
{
|
|
6
|
+
file: "chant-grafana.md",
|
|
7
|
+
name: "chant-grafana",
|
|
8
|
+
description:
|
|
9
|
+
"Declare Grafana dashboards, panels, typed Prometheus/Tempo/Loki queries and datasources as chant entities, and build dashboard JSON Grafana imports as it is",
|
|
10
|
+
triggers: [
|
|
11
|
+
{ type: "context" as const, value: "grafana" },
|
|
12
|
+
{ type: "context" as const, value: "grafana dashboard" },
|
|
13
|
+
{ type: "file-pattern" as const, value: "*.dashboard.ts" },
|
|
14
|
+
],
|
|
15
|
+
examples: [
|
|
16
|
+
{
|
|
17
|
+
title: "A request-rate panel on a declared Prometheus datasource",
|
|
18
|
+
output:
|
|
19
|
+
'const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });\n' +
|
|
20
|
+
'const rate = new PromQuery({ expr: "sum(rate(http_requests_total[$__rate_interval]))" });\n' +
|
|
21
|
+
'export const overview = new Dashboard({ title: "Overview", panels: [new TimeSeriesPanel({ title: "Rate", datasource: prometheus, targets: [rate] })] });',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
file: "chant-grafana-provisioning.md",
|
|
27
|
+
name: "chant-grafana-provisioning",
|
|
28
|
+
description: "Load chant-built Grafana dashboards and datasources into a running Grafana through its provisioning directories, in Docker or Kubernetes",
|
|
29
|
+
triggers: [{ type: "context" as const, value: "grafana provisioning" }],
|
|
30
|
+
},
|
|
31
|
+
]);
|