@intentius/chant-lexicon-grafana 0.97.0 → 0.99.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.
Files changed (451) hide show
  1. package/README.md +15 -5
  2. package/dist/actions/grafana.d.ts +24 -0
  3. package/dist/actions/grafana.d.ts.map +1 -0
  4. package/dist/actions/index.d.ts +2 -0
  5. package/dist/actions/index.d.ts.map +1 -0
  6. package/dist/alerting-build.d.ts +128 -0
  7. package/dist/alerting-build.d.ts.map +1 -0
  8. package/dist/alerting.d.ts +273 -0
  9. package/dist/alerting.d.ts.map +1 -0
  10. package/dist/api/apply.d.ts +156 -0
  11. package/dist/api/apply.d.ts.map +1 -0
  12. package/dist/api/bind.d.ts +32 -0
  13. package/dist/api/bind.d.ts.map +1 -0
  14. package/dist/api/client.d.ts +76 -0
  15. package/dist/api/client.d.ts.map +1 -0
  16. package/dist/api/converge.d.ts +33 -0
  17. package/dist/api/converge.d.ts.map +1 -0
  18. package/dist/api/dashboards.d.ts +102 -0
  19. package/dist/api/dashboards.d.ts.map +1 -0
  20. package/dist/api/datasources.d.ts +26 -0
  21. package/dist/api/datasources.d.ts.map +1 -0
  22. package/dist/api/fake-grafana-writes.d.ts +41 -0
  23. package/dist/api/fake-grafana-writes.d.ts.map +1 -0
  24. package/dist/api/fake-grafana.d.ts +61 -0
  25. package/dist/api/fake-grafana.d.ts.map +1 -0
  26. package/dist/api/folders.d.ts +110 -0
  27. package/dist/api/folders.d.ts.map +1 -0
  28. package/dist/api/library-panels.d.ts +52 -0
  29. package/dist/api/library-panels.d.ts.map +1 -0
  30. package/dist/build.d.ts +30 -2
  31. package/dist/build.d.ts.map +1 -1
  32. package/dist/catalog.d.ts +1 -1
  33. package/dist/catalog.d.ts.map +1 -1
  34. package/dist/codegen/docs.d.ts.map +1 -1
  35. package/dist/codegen/schema-types.d.ts +3 -3
  36. package/dist/codegen/schema-types.d.ts.map +1 -1
  37. package/dist/composites/agent-dashboard.d.ts.map +1 -1
  38. package/dist/composites/catalog.d.ts.map +1 -1
  39. package/dist/composites/index.d.ts +4 -2
  40. package/dist/composites/index.d.ts.map +1 -1
  41. package/dist/composites/red-dashboard.d.ts +21 -2
  42. package/dist/composites/red-dashboard.d.ts.map +1 -1
  43. package/dist/composites/shared.d.ts +7 -0
  44. package/dist/composites/shared.d.ts.map +1 -1
  45. package/dist/composites/slo-alert-rules.d.ts +92 -0
  46. package/dist/composites/slo-alert-rules.d.ts.map +1 -0
  47. package/dist/config.d.ts +111 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/contact-point-secrets.d.ts +17 -0
  50. package/dist/contact-point-secrets.d.ts.map +1 -0
  51. package/dist/dashboard.d.ts +16 -2
  52. package/dist/dashboard.d.ts.map +1 -1
  53. package/dist/datasource-refs.d.ts +95 -0
  54. package/dist/datasource-refs.d.ts.map +1 -0
  55. package/dist/datasource.d.ts +29 -0
  56. package/dist/datasource.d.ts.map +1 -1
  57. package/dist/deep-observe-hooks.d.ts +64 -0
  58. package/dist/deep-observe-hooks.d.ts.map +1 -0
  59. package/dist/deep-observe.d.ts +42 -0
  60. package/dist/deep-observe.d.ts.map +1 -0
  61. package/dist/describe-resources.d.ts +49 -0
  62. package/dist/describe-resources.d.ts.map +1 -0
  63. package/dist/detect.d.ts +28 -1
  64. package/dist/detect.d.ts.map +1 -1
  65. package/dist/export-resources.d.ts +38 -0
  66. package/dist/export-resources.d.ts.map +1 -0
  67. package/dist/import/alerting.d.ts +40 -0
  68. package/dist/import/alerting.d.ts.map +1 -0
  69. package/dist/import/edits.d.ts +47 -0
  70. package/dist/import/edits.d.ts.map +1 -0
  71. package/dist/import/embedded.d.ts +14 -0
  72. package/dist/import/embedded.d.ts.map +1 -0
  73. package/dist/import/generator.d.ts +60 -0
  74. package/dist/import/generator.d.ts.map +1 -0
  75. package/dist/import/live-export.d.ts +108 -0
  76. package/dist/import/live-export.d.ts.map +1 -0
  77. package/dist/import/mappings.d.ts +83 -0
  78. package/dist/import/mappings.d.ts.map +1 -0
  79. package/dist/import/model.d.ts +98 -0
  80. package/dist/import/model.d.ts.map +1 -0
  81. package/dist/import/normalize-alerting.d.ts +26 -0
  82. package/dist/import/normalize-alerting.d.ts.map +1 -0
  83. package/dist/import/normalize.d.ts +74 -0
  84. package/dist/import/normalize.d.ts.map +1 -0
  85. package/dist/import/parser.d.ts +110 -0
  86. package/dist/import/parser.d.ts.map +1 -0
  87. package/dist/import/report.d.ts +26 -0
  88. package/dist/import/report.d.ts.map +1 -0
  89. package/dist/import/testdata/fixtures.d.ts +35 -0
  90. package/dist/import/testdata/fixtures.d.ts.map +1 -0
  91. package/dist/import/v2.d.ts +72 -0
  92. package/dist/import/v2.d.ts.map +1 -0
  93. package/dist/index.d.ts +26 -9
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/integrity.json +18 -10
  96. package/dist/k8s.d.ts +107 -0
  97. package/dist/k8s.d.ts.map +1 -0
  98. package/dist/lint/audit-catalog.d.ts +2 -1
  99. package/dist/lint/audit-catalog.d.ts.map +1 -1
  100. package/dist/lint/post-synth/graf101.d.ts +2 -2
  101. package/dist/lint/post-synth/graf102.d.ts +1 -1
  102. package/dist/lint/post-synth/graf107.d.ts +1 -1
  103. package/dist/lint/post-synth/graf108.d.ts +8 -0
  104. package/dist/lint/post-synth/graf108.d.ts.map +1 -0
  105. package/dist/lint/post-synth/graf109.d.ts +8 -0
  106. package/dist/lint/post-synth/graf109.d.ts.map +1 -0
  107. package/dist/lint/post-synth/graf111.d.ts +8 -0
  108. package/dist/lint/post-synth/graf111.d.ts.map +1 -0
  109. package/dist/lint/post-synth/graf112.d.ts +8 -0
  110. package/dist/lint/post-synth/graf112.d.ts.map +1 -0
  111. package/dist/lint/post-synth/graf113.d.ts +8 -0
  112. package/dist/lint/post-synth/graf113.d.ts.map +1 -0
  113. package/dist/lint/post-synth/graf114.d.ts +8 -0
  114. package/dist/lint/post-synth/graf114.d.ts.map +1 -0
  115. package/dist/lint/post-synth/graf115.d.ts +8 -0
  116. package/dist/lint/post-synth/graf115.d.ts.map +1 -0
  117. package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
  118. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  119. package/dist/lint/post-synth/index.d.ts.map +1 -1
  120. package/dist/lint/rules/literal-secret.d.ts +7 -3
  121. package/dist/lint/rules/literal-secret.d.ts.map +1 -1
  122. package/dist/lsp/hover.d.ts.map +1 -1
  123. package/dist/manifest.json +1 -1
  124. package/dist/members.d.ts +27 -0
  125. package/dist/members.d.ts.map +1 -0
  126. package/dist/meta.json +135 -0
  127. package/dist/okf/index.md +36 -2
  128. package/dist/okf/rules/GRAF002.md +3 -2
  129. package/dist/okf/rules/GRAF101.md +3 -2
  130. package/dist/okf/rules/GRAF102.md +1 -0
  131. package/dist/okf/rules/GRAF108.md +11 -0
  132. package/dist/okf/rules/GRAF109.md +15 -0
  133. package/dist/okf/rules/GRAF111.md +11 -0
  134. package/dist/okf/rules/GRAF112.md +16 -0
  135. package/dist/okf/rules/GRAF113.md +16 -0
  136. package/dist/okf/rules/GRAF114.md +11 -0
  137. package/dist/okf/rules/GRAF115.md +11 -0
  138. package/dist/okf/types/AlertListPanel.md +9 -0
  139. package/dist/okf/types/AlertQuery.md +9 -0
  140. package/dist/okf/types/AlertRule.md +9 -0
  141. package/dist/okf/types/AlertRuleGroup.md +9 -0
  142. package/dist/okf/types/BarChartPanel.md +9 -0
  143. package/dist/okf/types/BarGaugePanel.md +9 -0
  144. package/dist/okf/types/CanvasPanel.md +9 -0
  145. package/dist/okf/types/ClassicConditionsExpression.md +9 -0
  146. package/dist/okf/types/ContactPoint.md +14 -0
  147. package/dist/okf/types/Dashboard.md +1 -0
  148. package/dist/okf/types/Datasource.md +3 -2
  149. package/dist/okf/types/ExternalDatasource.md +15 -0
  150. package/dist/okf/types/FlameGraphPanel.md +9 -0
  151. package/dist/okf/types/GeomapPanel.md +9 -0
  152. package/dist/okf/types/HistogramPanel.md +9 -0
  153. package/dist/okf/types/MathExpression.md +9 -0
  154. package/dist/okf/types/MuteTiming.md +13 -0
  155. package/dist/okf/types/NodeGraphPanel.md +9 -0
  156. package/dist/okf/types/NotificationPolicy.md +9 -0
  157. package/dist/okf/types/NotificationTemplate.md +9 -0
  158. package/dist/okf/types/PieChartPanel.md +9 -0
  159. package/dist/okf/types/ReduceExpression.md +9 -0
  160. package/dist/okf/types/ResampleExpression.md +9 -0
  161. package/dist/okf/types/SqlExpression.md +9 -0
  162. package/dist/okf/types/StateTimelinePanel.md +9 -0
  163. package/dist/okf/types/StatusHistoryPanel.md +9 -0
  164. package/dist/okf/types/ThresholdExpression.md +9 -0
  165. package/dist/okf/types/TrendPanel.md +9 -0
  166. package/dist/okf/types/XYChartPanel.md +9 -0
  167. package/dist/op/activities/grafana-apply.d.ts +75 -0
  168. package/dist/op/activities/grafana-apply.d.ts.map +1 -0
  169. package/dist/op/activities/index.d.ts +9 -0
  170. package/dist/op/activities/index.d.ts.map +1 -0
  171. package/dist/op/builders.d.ts +24 -0
  172. package/dist/op/builders.d.ts.map +1 -0
  173. package/dist/ownership.d.ts +67 -0
  174. package/dist/ownership.d.ts.map +1 -0
  175. package/dist/panel-options.d.ts +65 -0
  176. package/dist/panel-options.d.ts.map +1 -0
  177. package/dist/panels.d.ts +46 -9
  178. package/dist/panels.d.ts.map +1 -1
  179. package/dist/pin.d.ts +29 -7
  180. package/dist/pin.d.ts.map +1 -1
  181. package/dist/plugin.d.ts.map +1 -1
  182. package/dist/promql-check.d.ts +51 -0
  183. package/dist/promql-check.d.ts.map +1 -0
  184. package/dist/query.d.ts +9 -8
  185. package/dist/query.d.ts.map +1 -1
  186. package/dist/rules/graf101.ts +3 -3
  187. package/dist/rules/graf102.ts +1 -1
  188. package/dist/rules/graf107.ts +1 -1
  189. package/dist/rules/graf108.ts +17 -0
  190. package/dist/rules/graf109.ts +17 -0
  191. package/dist/rules/graf111.ts +17 -0
  192. package/dist/rules/graf112.ts +17 -0
  193. package/dist/rules/graf113.ts +17 -0
  194. package/dist/rules/graf114.ts +17 -0
  195. package/dist/rules/graf115.ts +17 -0
  196. package/dist/rules/grafana-helpers.ts +15 -8
  197. package/dist/rules/literal-secret.ts +60 -6
  198. package/dist/schema/barchart.gen.d.ts +198 -0
  199. package/dist/schema/barchart.gen.d.ts.map +1 -0
  200. package/dist/schema/bargauge.gen.d.ts +97 -0
  201. package/dist/schema/bargauge.gen.d.ts.map +1 -0
  202. package/dist/schema/canvas.gen.d.ts +152 -0
  203. package/dist/schema/canvas.gen.d.ts.map +1 -0
  204. package/dist/schema/dashboard.gen.d.ts +196 -21
  205. package/dist/schema/dashboard.gen.d.ts.map +1 -1
  206. package/dist/schema/expr.gen.d.ts +606 -0
  207. package/dist/schema/expr.gen.d.ts.map +1 -0
  208. package/dist/schema/gauge.gen.d.ts.map +1 -1
  209. package/dist/schema/geomap.gen.d.ts +100 -0
  210. package/dist/schema/geomap.gen.d.ts.map +1 -0
  211. package/dist/schema/heatmap.gen.d.ts.map +1 -1
  212. package/dist/schema/histogram.gen.d.ts +132 -0
  213. package/dist/schema/histogram.gen.d.ts.map +1 -0
  214. package/dist/schema/index.d.ts +12 -0
  215. package/dist/schema/index.d.ts.map +1 -1
  216. package/dist/schema/logs.gen.d.ts +1 -0
  217. package/dist/schema/logs.gen.d.ts.map +1 -1
  218. package/dist/schema/loki.gen.d.ts.map +1 -1
  219. package/dist/schema/nodegraph.gen.d.ts +49 -0
  220. package/dist/schema/nodegraph.gen.d.ts.map +1 -0
  221. package/dist/schema/piechart.gen.d.ts +127 -0
  222. package/dist/schema/piechart.gen.d.ts.map +1 -0
  223. package/dist/schema/prometheus.gen.d.ts.map +1 -1
  224. package/dist/schema/stat.gen.d.ts.map +1 -1
  225. package/dist/schema/statetimeline.gen.d.ts +124 -0
  226. package/dist/schema/statetimeline.gen.d.ts.map +1 -0
  227. package/dist/schema/statushistory.gen.d.ts +114 -0
  228. package/dist/schema/statushistory.gen.d.ts.map +1 -0
  229. package/dist/schema/table.gen.d.ts +22 -3
  230. package/dist/schema/table.gen.d.ts.map +1 -1
  231. package/dist/schema/tempo.gen.d.ts.map +1 -1
  232. package/dist/schema/text.gen.d.ts.map +1 -1
  233. package/dist/schema/timeseries.gen.d.ts +5 -1
  234. package/dist/schema/timeseries.gen.d.ts.map +1 -1
  235. package/dist/schema/trend.gen.d.ts +180 -0
  236. package/dist/schema/trend.gen.d.ts.map +1 -0
  237. package/dist/schema/xychart.gen.d.ts +145 -0
  238. package/dist/schema/xychart.gen.d.ts.map +1 -0
  239. package/dist/schema-validate.d.ts +30 -4
  240. package/dist/schema-validate.d.ts.map +1 -1
  241. package/dist/serializer.d.ts +11 -3
  242. package/dist/serializer.d.ts.map +1 -1
  243. package/dist/skill-defs.d.ts.map +1 -1
  244. package/dist/skills/chant-grafana-alerting.md +49 -0
  245. package/dist/skills/chant-grafana-provisioning.md +3 -3
  246. package/dist/skills/chant-grafana.md +12 -6
  247. package/dist/spec/fetch-units-cli.d.ts +3 -0
  248. package/dist/spec/fetch-units-cli.d.ts.map +1 -0
  249. package/dist/spec/overlay.d.ts +43 -0
  250. package/dist/spec/overlay.d.ts.map +1 -0
  251. package/dist/spec/schemas.d.ts +13 -7
  252. package/dist/spec/schemas.d.ts.map +1 -1
  253. package/dist/spec/units.d.ts +35 -0
  254. package/dist/spec/units.d.ts.map +1 -0
  255. package/dist/spec/units.gen.d.ts +2 -0
  256. package/dist/spec/units.gen.d.ts.map +1 -0
  257. package/dist/transformations.d.ts +459 -0
  258. package/dist/transformations.d.ts.map +1 -0
  259. package/dist/util.d.ts +6 -0
  260. package/dist/util.d.ts.map +1 -1
  261. package/dist/validate-alerting.d.ts +65 -0
  262. package/dist/validate-alerting.d.ts.map +1 -0
  263. package/dist/validate-output.d.ts +49 -9
  264. package/dist/validate-output.d.ts.map +1 -1
  265. package/dist/validate.d.ts.map +1 -1
  266. package/dist/variables.d.ts +7 -2
  267. package/dist/variables.d.ts.map +1 -1
  268. package/package.json +12 -4
  269. package/src/actions/actions.test.ts +24 -0
  270. package/src/actions/grafana.ts +40 -0
  271. package/src/actions/index.ts +1 -0
  272. package/src/alerting-build.ts +376 -0
  273. package/src/alerting.e2e.test.ts +212 -0
  274. package/src/alerting.test.ts +272 -0
  275. package/src/alerting.ts +435 -0
  276. package/src/api/apply.test.ts +238 -0
  277. package/src/api/apply.ts +384 -0
  278. package/src/api/bind.ts +58 -0
  279. package/src/api/client.ts +148 -0
  280. package/src/api/converge.ts +63 -0
  281. package/src/api/dashboards.ts +247 -0
  282. package/src/api/datasources.ts +33 -0
  283. package/src/api/fake-grafana-writes.ts +201 -0
  284. package/src/api/fake-grafana.ts +186 -0
  285. package/src/api/folders.ts +265 -0
  286. package/src/api/library-panels.ts +101 -0
  287. package/src/build.test.ts +210 -5
  288. package/src/build.ts +180 -43
  289. package/src/catalog.ts +86 -2
  290. package/src/codegen/docs.ts +42 -9
  291. package/src/codegen/schema-types.ts +16 -6
  292. package/src/composites/agent-dashboard.ts +6 -3
  293. package/src/composites/catalog.test.ts +2 -2
  294. package/src/composites/catalog.ts +82 -0
  295. package/src/composites/composites.test.ts +73 -9
  296. package/src/composites/index.ts +4 -2
  297. package/src/composites/queries.e2e.test.ts +374 -0
  298. package/src/composites/red-dashboard.ts +83 -12
  299. package/src/composites/shared.ts +11 -0
  300. package/src/composites/slo-alert-rules.ts +149 -0
  301. package/src/config.test.ts +61 -0
  302. package/src/config.ts +138 -0
  303. package/src/contact-point-secrets.ts +63 -0
  304. package/src/dashboard.ts +16 -2
  305. package/src/datasource-refs.test.ts +56 -0
  306. package/src/datasource-refs.ts +189 -0
  307. package/src/datasource.ts +52 -0
  308. package/src/deep-observe-hooks.ts +252 -0
  309. package/src/deep-observe.test.ts +265 -0
  310. package/src/deep-observe.ts +164 -0
  311. package/src/describe-resources.test.ts +255 -0
  312. package/src/describe-resources.ts +209 -0
  313. package/src/detect.ts +79 -2
  314. package/src/export-resources.test.ts +129 -0
  315. package/src/export-resources.ts +84 -0
  316. package/src/generated/lexicon-grafana.json +135 -0
  317. package/src/import/alerting-roundtrip.test.ts +161 -0
  318. package/src/import/alerting.ts +500 -0
  319. package/src/import/edits.ts +95 -0
  320. package/src/import/embedded.test.ts +45 -0
  321. package/src/import/embedded.ts +47 -0
  322. package/src/import/generated-types.e2e.test.ts +118 -0
  323. package/src/import/generator.test.ts +210 -0
  324. package/src/import/generator.ts +462 -0
  325. package/src/import/live-export.ts +264 -0
  326. package/src/import/mappings.ts +264 -0
  327. package/src/import/model.ts +110 -0
  328. package/src/import/normalize-alerting.ts +101 -0
  329. package/src/import/normalize.test.ts +117 -0
  330. package/src/import/normalize.ts +364 -0
  331. package/src/import/parser.test.ts +440 -0
  332. package/src/import/parser.ts +1236 -0
  333. package/src/import/report.ts +62 -0
  334. package/src/import/roundtrip.test.ts +468 -0
  335. package/src/import/testdata/fixtures.ts +129 -0
  336. package/src/import/v2.test.ts +292 -0
  337. package/src/import/v2.ts +803 -0
  338. package/src/import.e2e.test.ts +31 -84
  339. package/src/import.test.ts +9 -3
  340. package/src/index.ts +190 -5
  341. package/src/k8s.test.ts +98 -0
  342. package/src/k8s.ts +197 -0
  343. package/src/lint/audit-catalog.ts +60 -3
  344. package/src/lint/core-lint.test.ts +43 -0
  345. package/src/lint/post-synth/graf101.ts +3 -3
  346. package/src/lint/post-synth/graf102.ts +1 -1
  347. package/src/lint/post-synth/graf107.ts +1 -1
  348. package/src/lint/post-synth/graf108.ts +17 -0
  349. package/src/lint/post-synth/graf109.ts +17 -0
  350. package/src/lint/post-synth/graf111.ts +17 -0
  351. package/src/lint/post-synth/graf112.ts +17 -0
  352. package/src/lint/post-synth/graf113.ts +17 -0
  353. package/src/lint/post-synth/graf114.ts +17 -0
  354. package/src/lint/post-synth/graf115.ts +17 -0
  355. package/src/lint/post-synth/grafana-helpers.ts +15 -8
  356. package/src/lint/post-synth/index.ts +14 -0
  357. package/src/lint/post-synth/post-synth.test.ts +235 -11
  358. package/src/lint/rules/literal-secret.ts +60 -6
  359. package/src/lint/rules/rules.test.ts +27 -0
  360. package/src/lsp/completions.test.ts +5 -0
  361. package/src/lsp/hover.test.ts +7 -0
  362. package/src/lsp/hover.ts +8 -1
  363. package/src/members.ts +68 -0
  364. package/src/observe.e2e.test.ts +255 -0
  365. package/src/op/activities/grafana-apply.e2e.test.ts +175 -0
  366. package/src/op/activities/grafana-apply.test.ts +229 -0
  367. package/src/op/activities/grafana-apply.ts +171 -0
  368. package/src/op/activities/index.ts +8 -0
  369. package/src/op/builders.test.ts +40 -0
  370. package/src/op/builders.ts +34 -0
  371. package/src/ownership.ts +103 -0
  372. package/src/panel-options.ts +66 -0
  373. package/src/panels.test.ts +51 -0
  374. package/src/panels.ts +148 -10
  375. package/src/pin.ts +57 -6
  376. package/src/plugin.test.ts +5 -2
  377. package/src/plugin.ts +66 -0
  378. package/src/promql-check.test.ts +108 -0
  379. package/src/promql-check.ts +189 -0
  380. package/src/query.ts +13 -8
  381. package/src/schema/barchart.gen.ts +221 -0
  382. package/src/schema/bargauge.gen.ts +111 -0
  383. package/src/schema/canvas.gen.ts +176 -0
  384. package/src/schema/dashboard.gen.ts +206 -22
  385. package/src/schema/expr.gen.ts +618 -0
  386. package/src/schema/gauge.gen.ts +2 -1
  387. package/src/schema/geomap.gen.ts +112 -0
  388. package/src/schema/heatmap.gen.ts +2 -1
  389. package/src/schema/histogram.gen.ts +151 -0
  390. package/src/schema/index.ts +12 -0
  391. package/src/schema/logs.gen.ts +3 -1
  392. package/src/schema/loki.gen.ts +2 -1
  393. package/src/schema/nodegraph.gen.ts +58 -0
  394. package/src/schema/piechart.gen.ts +146 -0
  395. package/src/schema/prometheus.gen.ts +2 -1
  396. package/src/schema/stat.gen.ts +2 -1
  397. package/src/schema/statetimeline.gen.ts +143 -0
  398. package/src/schema/statushistory.gen.ts +132 -0
  399. package/src/schema/table.gen.ts +27 -4
  400. package/src/schema/tempo.gen.ts +2 -1
  401. package/src/schema/text.gen.ts +2 -1
  402. package/src/schema/timeseries.gen.ts +7 -2
  403. package/src/schema/trend.gen.ts +208 -0
  404. package/src/schema/xychart.gen.ts +167 -0
  405. package/src/schema-overlay.test.ts +282 -0
  406. package/src/schema-validate.ts +141 -50
  407. package/src/schema.test.ts +4 -2
  408. package/src/serializer.test.ts +13 -1
  409. package/src/serializer.ts +11 -3
  410. package/src/skill-defs.ts +17 -0
  411. package/src/skills/chant-grafana-alerting.md +49 -0
  412. package/src/skills/chant-grafana-provisioning.md +3 -3
  413. package/src/skills/chant-grafana.md +12 -6
  414. package/src/spec/fetch-cli.ts +3 -3
  415. package/src/spec/fetch-units-cli.ts +37 -0
  416. package/src/spec/overlay/canvas.overlay.json +142 -0
  417. package/src/spec/overlay/dashboard.overlay.json +378 -0
  418. package/src/spec/overlay/expr.overlay.json +14 -0
  419. package/src/spec/overlay/geomap.overlay.json +35 -0
  420. package/src/spec/overlay/logs.overlay.json +14 -0
  421. package/src/spec/overlay/nodegraph.overlay.json +31 -0
  422. package/src/spec/overlay/piechart.overlay.json +17 -0
  423. package/src/spec/overlay/table.overlay.json +84 -0
  424. package/src/spec/overlay/timeseries.overlay.json +21 -0
  425. package/src/spec/overlay/trend.overlay.json +17 -0
  426. package/src/spec/overlay/xychart.overlay.json +16 -0
  427. package/src/spec/overlay.ts +105 -0
  428. package/src/spec/schemas/barchart.jsonschema.json +419 -0
  429. package/src/spec/schemas/bargauge.jsonschema.json +224 -0
  430. package/src/spec/schemas/canvas.jsonschema.json +356 -0
  431. package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
  432. package/src/spec/schemas/expr.jsonschema.json +987 -0
  433. package/src/spec/schemas/geomap.jsonschema.json +220 -0
  434. package/src/spec/schemas/histogram.jsonschema.json +308 -0
  435. package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
  436. package/src/spec/schemas/piechart.jsonschema.json +275 -0
  437. package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
  438. package/src/spec/schemas/statushistory.jsonschema.json +292 -0
  439. package/src/spec/schemas/trend.jsonschema.json +449 -0
  440. package/src/spec/schemas/xychart.jsonschema.json +428 -0
  441. package/src/spec/schemas.ts +22 -10
  442. package/src/spec/units.gen.ts +281 -0
  443. package/src/spec/units.ts +77 -0
  444. package/src/transformations.test.ts +65 -0
  445. package/src/transformations.ts +574 -0
  446. package/src/util.ts +8 -0
  447. package/src/validate-alerting.test.ts +255 -0
  448. package/src/validate-alerting.ts +428 -0
  449. package/src/validate-output.ts +332 -101
  450. package/src/validate.ts +14 -0
  451. package/src/variables.ts +7 -2
@@ -0,0 +1,803 @@
1
+ /**
2
+ * Read a v2 dashboard (`dashboard.grafana.app/v2` or `v2beta1`, Grafana 13's
3
+ * default "V2 Resource" export) as the classic (v1) dashboard JSON chant
4
+ * builds, for `chant import` (#2947).
5
+ *
6
+ * The conversion follows Grafana's own v2 -> v1 conversion at v13.2.2
7
+ * (`apps/dashboard/pkg/migration/conversion/v2alpha1_to_v1.go` and
8
+ * `v2_to_v1_layout_conversion.md`), so what comes out is what Grafana itself
9
+ * serves when the same dashboard is read at v1: tabs become expanded rows,
10
+ * an auto grid gets the fixed positions Grafana computes, nested rows are
11
+ * flattened, and row ids follow the largest panel id.
12
+ *
13
+ * Grafana does that silently. Here, everything v2 can say that the classic
14
+ * model cannot is a warning: tabs, auto grids, nested rows, a hidden row
15
+ * header after the first row, conditional rendering, section variables,
16
+ * dashboard preferences, "show in controls menu", and any key the vendored
17
+ * `dashboardv2` schema (`src/spec/schemas/dashboardv2.jsonschema.json`) does
18
+ * not know. `V2_KEYS` names, per schema definition, which keys are carried
19
+ * and which are reported; `v2.test.ts` checks it against the schema, so a
20
+ * pin bump that adds a v2 key fails until the key is placed in one of them.
21
+ *
22
+ * What the classic model can hold but chant cannot yet (library panels,
23
+ * annotations, ad hoc, group by and switch variables) is written to the
24
+ * classic JSON as Grafana would and reported by the classic importer
25
+ * (`./parser.ts`), as for any classic dashboard.
26
+ *
27
+ * A classic read of a dashboard Grafana stores as v2 is the lossy direction:
28
+ * `storedAsV2` recognises it from the resource's
29
+ * `status.conversion.storedVersion`, and the parser refuses it unless the
30
+ * caller opts in.
31
+ */
32
+
33
+ import { DASHBOARD_SCHEMA_VERSION } from "../schema/dashboard.gen";
34
+ import { isObject } from "./normalize";
35
+
36
+ type Json = Record<string, unknown>;
37
+
38
+ /**
39
+ * Per `dashboardv2` schema definition, the keys this module reads.
40
+ *
41
+ * - `carried`: written into the classic dashboard (or structural, like `kind`).
42
+ * - `reported`: v2-only, with the reason the warning gives when the key is
43
+ * set to anything but its default.
44
+ */
45
+ export const V2_KEYS: Readonly<Record<string, { carried: readonly string[]; reported: Readonly<Record<string, string>> }>> = {
46
+ Dashboard: {
47
+ carried: ["annotations", "cursorSync", "description", "editable", "elements", "layout", "links", "liveNow", "preload", "revision", "tags", "timeSettings", "title", "variables"],
48
+ reported: { preferences: "(dashboard preferences, the layout new sections start with, have no classic form)" },
49
+ },
50
+ TimeSettingsSpec: {
51
+ carried: ["timezone", "from", "to", "autoRefresh", "autoRefreshIntervals", "quickRanges", "hideTimepicker", "weekStart", "fiscalYearStartMonth", "nowDelay"],
52
+ reported: {},
53
+ },
54
+ DashboardLink: {
55
+ carried: ["title", "type", "icon", "tooltip", "url", "tags", "asDropdown", "targetBlank", "includeVars", "keepTime", "placement"],
56
+ reported: { origin: "(the datasource that provides the link; a classic dashboard link is always the dashboard's own)" },
57
+ },
58
+ PanelSpec: { carried: ["id", "title", "description", "links", "data", "vizConfig", "transparent"], reported: {} },
59
+ LibraryPanelKindSpec: { carried: ["id", "title", "libraryPanel"], reported: {} },
60
+ QueryGroupSpec: { carried: ["queries", "transformations", "queryOptions"], reported: {} },
61
+ QueryOptionsSpec: {
62
+ carried: ["timeFrom", "maxDataPoints", "timeShift", "queryCachingTTL", "interval", "cacheTimeout", "hideTimeOverride", "timeCompare"],
63
+ reported: {},
64
+ },
65
+ PanelQuerySpec: { carried: ["query", "refId", "hidden"], reported: {} },
66
+ DataQueryKind: {
67
+ carried: ["kind", "group", "version", "datasource", "spec"],
68
+ reported: { labels: "(query labels have no classic form)" },
69
+ },
70
+ TransformationKind: { carried: ["kind", "group", "spec"], reported: {} },
71
+ TransformationSpec: { carried: ["disabled", "filter", "topic", "options"], reported: {} },
72
+ VizConfigKind: { carried: ["kind", "group", "version", "spec"], reported: {} },
73
+ VizConfigSpec: { carried: ["options", "fieldConfig"], reported: {} },
74
+ GridLayoutItemSpec: { carried: ["x", "y", "width", "height", "element", "repeat"], reported: {} },
75
+ RepeatOptions: { carried: ["mode", "value", "direction", "maxPerRow"], reported: {} },
76
+ RowsLayoutRowSpec: {
77
+ carried: ["title", "collapse", "hideHeader", "repeat", "layout"],
78
+ reported: {
79
+ fillScreen: "(a classic row does not stretch to fill the screen)",
80
+ conditionalRendering: "(conditional rendering has no classic form, so the row always shows)",
81
+ variables: "(section variables have no classic form)",
82
+ },
83
+ },
84
+ TabsLayoutTabSpec: {
85
+ carried: ["title", "layout", "repeat"],
86
+ reported: {
87
+ conditionalRendering: "(conditional rendering has no classic form, so the tab's row always shows)",
88
+ variables: "(section variables have no classic form)",
89
+ },
90
+ },
91
+ AutoGridLayoutSpec: {
92
+ carried: ["maxColumnCount", "rowHeightMode", "rowHeight", "items"],
93
+ reported: {
94
+ columnWidthMode: "(a classic grid has fixed widths)",
95
+ columnWidth: "(a classic grid has fixed widths)",
96
+ fillScreen: "(a classic grid does not stretch to fill the screen)",
97
+ },
98
+ },
99
+ AutoGridLayoutItemSpec: {
100
+ carried: ["element", "repeat"],
101
+ reported: { conditionalRendering: "(conditional rendering has no classic form, so the panel always shows)" },
102
+ },
103
+ AnnotationQuerySpec: {
104
+ carried: ["query", "enable", "hide", "iconColor", "name", "builtIn", "filter", "mappings", "legacyOptions"],
105
+ reported: { placement: "(an annotation toggle in the controls menu has no classic form)" },
106
+ },
107
+ QueryVariableSpec: {
108
+ carried: ["name", "current", "label", "hide", "refresh", "skipUrlSync", "description", "query", "regex", "regexApplyTo", "sort", "definition", "options", "multi", "includeAll", "allValue", "allowCustomValue", "staticOptions", "staticOptionsOrder"],
109
+ reported: { placeholder: "(a placeholder has no classic form)", origin: ORIGIN() },
110
+ },
111
+ TextVariableSpec: { carried: ["name", "current", "query", "label", "hide", "skipUrlSync", "description"], reported: { origin: ORIGIN() } },
112
+ ConstantVariableSpec: { carried: ["name", "query", "current", "label", "hide", "skipUrlSync", "description"], reported: { origin: ORIGIN() } },
113
+ DatasourceVariableSpec: {
114
+ carried: ["name", "pluginId", "refresh", "regex", "current", "options", "multi", "includeAll", "allValue", "label", "hide", "skipUrlSync", "description", "allowCustomValue"],
115
+ reported: { origin: ORIGIN() },
116
+ },
117
+ IntervalVariableSpec: {
118
+ carried: ["name", "query", "current", "options", "auto", "auto_min", "auto_count", "refresh", "label", "hide", "skipUrlSync", "description"],
119
+ reported: { origin: ORIGIN() },
120
+ },
121
+ CustomVariableSpec: {
122
+ carried: ["name", "query", "current", "options", "multi", "includeAll", "allValue", "label", "hide", "skipUrlSync", "description", "allowCustomValue", "valuesFormat"],
123
+ reported: { origin: ORIGIN() },
124
+ },
125
+ GroupByVariableKind: { carried: ["kind", "group", "datasource", "spec"], reported: { labels: "(variable labels have no classic form)" } },
126
+ GroupByVariableSpec: {
127
+ carried: ["name", "defaultValue", "current", "options", "multi", "label", "hide", "skipUrlSync", "description"],
128
+ reported: { origin: ORIGIN() },
129
+ },
130
+ AdhocVariableKind: { carried: ["kind", "group", "datasource", "spec"], reported: { labels: "(variable labels have no classic form)" } },
131
+ AdhocVariableSpec: {
132
+ carried: ["name", "baseFilters", "filters", "defaultKeys", "label", "hide", "skipUrlSync", "description", "allowCustomValue", "enableGroupBy"],
133
+ reported: { origin: ORIGIN() },
134
+ },
135
+ SwitchVariableSpec: {
136
+ carried: ["name", "current", "enabledValue", "disabledValue", "label", "hide", "skipUrlSync", "description"],
137
+ reported: { origin: ORIGIN() },
138
+ },
139
+ };
140
+
141
+ /** Reported keys at these values say what the classic model does anyway, and are not reported. */
142
+ const V2_DEFAULTS: Readonly<Record<string, unknown>> = { columnWidthMode: "standard" };
143
+
144
+ function ORIGIN(): string {
145
+ return "(the datasource that provides the variable; a classic variable is always the dashboard's own)";
146
+ }
147
+
148
+ /** The variable kinds, and the `V2_KEYS` entry for each one's spec. */
149
+ const VARIABLE_SPECS: Readonly<Record<string, string>> = {
150
+ QueryVariable: "QueryVariableSpec",
151
+ TextVariable: "TextVariableSpec",
152
+ ConstantVariable: "ConstantVariableSpec",
153
+ DatasourceVariable: "DatasourceVariableSpec",
154
+ IntervalVariable: "IntervalVariableSpec",
155
+ CustomVariable: "CustomVariableSpec",
156
+ GroupByVariable: "GroupByVariableSpec",
157
+ AdhocVariable: "AdhocVariableSpec",
158
+ SwitchVariable: "SwitchVariableSpec",
159
+ };
160
+
161
+ const CURSOR_SYNC: Readonly<Record<string, number>> = { Off: 0, Crosshair: 1, Tooltip: 2 };
162
+ const VARIABLE_HIDE: Readonly<Record<string, number>> = { dontHide: 0, hideLabel: 1, hideVariable: 2 };
163
+ const VARIABLE_REFRESH: Readonly<Record<string, number>> = { never: 0, onDashboardLoad: 1, onTimeRangeChanged: 2 };
164
+ const VARIABLE_SORT: Readonly<Record<string, number>> = {
165
+ disabled: 0,
166
+ alphabeticalAsc: 1,
167
+ alphabeticalDesc: 2,
168
+ numericalAsc: 3,
169
+ numericalDesc: 4,
170
+ alphabeticalCaseInsensitiveAsc: 5,
171
+ alphabeticalCaseInsensitiveDesc: 6,
172
+ naturalAsc: 7,
173
+ naturalDesc: 8,
174
+ };
175
+
176
+ /** A query spec holding a query that was a plain string in v1. */
177
+ const LEGACY_STRING_VALUE_KEY = "__legacyStringValue";
178
+ /** Grafana's shared "-- Dashboard --" datasource: a query that reuses another panel's results. */
179
+ const DASHBOARD_DS = "-- Dashboard --";
180
+
181
+ const GRID_COLUMNS = 24;
182
+
183
+ function str(v: unknown): string | undefined {
184
+ return typeof v === "string" ? v : undefined;
185
+ }
186
+
187
+ function num(v: unknown): number | undefined {
188
+ return typeof v === "number" ? v : undefined;
189
+ }
190
+
191
+ function obj(v: unknown): Json {
192
+ return isObject(v) ? v : {};
193
+ }
194
+
195
+ function arr(v: unknown): unknown[] {
196
+ return Array.isArray(v) ? v : [];
197
+ }
198
+
199
+ function isSet(v: unknown): boolean {
200
+ if (v === undefined || v === null || v === false || v === "") return false;
201
+ if (Array.isArray(v)) return v.length > 0;
202
+ if (isObject(v)) return Object.keys(v).length > 0;
203
+ return true;
204
+ }
205
+
206
+ function andList(items: string[]): string {
207
+ return items.length <= 1 ? (items[0] ?? "") : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
208
+ }
209
+
210
+ function quote(title: unknown): string {
211
+ return typeof title === "string" && title !== "" ? `"${title}"` : "(untitled)";
212
+ }
213
+
214
+ /** What reading one v2 dashboard gave. */
215
+ export interface V2Read {
216
+ /** The classic dashboard JSON. */
217
+ dashboard: Json;
218
+ /** What v2 said that the classic dashboard does not. */
219
+ warnings: string[];
220
+ }
221
+
222
+ /** Collects warnings for one v2 dashboard, and the keys it saw. */
223
+ class V2Reader {
224
+ readonly warnings: string[] = [];
225
+ private readonly elements: Json;
226
+ private readonly placed = new Set<string>();
227
+ private nextRowId: number;
228
+
229
+ constructor(private readonly spec: Json) {
230
+ this.elements = obj(spec.elements);
231
+ let max = 0;
232
+ for (const el of Object.values(this.elements)) {
233
+ const id = num(obj(obj(el).spec).id);
234
+ if (id !== undefined && id > max) max = id;
235
+ }
236
+ this.nextRowId = max + 1;
237
+ }
238
+
239
+ warn(message: string): void {
240
+ this.warnings.push(message);
241
+ }
242
+
243
+ /** Report the keys of `value` (a `def` object) that are v2-only and set, or that the schema does not know. */
244
+ keys(value: Json, def: string, subject: string): void {
245
+ const table = V2_KEYS[def];
246
+ const unknown: string[] = [];
247
+ const reported = new Map<string, string[]>();
248
+ for (const key of Object.keys(value)) {
249
+ if (table.carried.includes(key)) continue;
250
+ const why = table.reported[key];
251
+ if (why === undefined) unknown.push(key);
252
+ else if (isSet(value[key]) && V2_DEFAULTS[key] !== value[key]) reported.set(why, [...(reported.get(why) ?? []), key]);
253
+ }
254
+ for (const [why, keys] of reported) this.warn(`${subject}: ${andList(keys)} ${keys.length === 1 ? "is" : "are"} not carried ${why}`);
255
+ if (unknown.length > 0) {
256
+ this.warn(`${subject}: ${andList(unknown)} ${unknown.length === 1 ? "is" : "are"} not carried (not a key the dashboardv2 schema at the pin has)`);
257
+ }
258
+ }
259
+
260
+ // ── datasources and queries ─────────────────────────────────────
261
+
262
+ /** A `DataQueryKind`'s datasource as a classic `{ type, uid }`: the group is the plugin type, the name the uid. */
263
+ datasourceOf(query: Json): Json | undefined {
264
+ const type = str(query.group);
265
+ const uid = str(obj(query.datasource).name);
266
+ if (uid !== undefined && uid !== "") return type ? { type, uid } : { uid };
267
+ return type ? { type } : undefined;
268
+ }
269
+
270
+ /** A `DataQueryKind`'s spec as classic query fields. */
271
+ querySpec(query: Json, subject: string): Json {
272
+ this.keys(query, "DataQueryKind", subject);
273
+ const out: Json = {};
274
+ for (const [k, v] of Object.entries(obj(query.spec))) {
275
+ if (k === LEGACY_STRING_VALUE_KEY) {
276
+ if (typeof v === "string") out.query = v;
277
+ continue;
278
+ }
279
+ out[k] = v;
280
+ }
281
+ return out;
282
+ }
283
+
284
+ target(q: Json, subject: string): Json {
285
+ const spec = obj(q.spec);
286
+ this.keys(spec, "PanelQuerySpec", subject);
287
+ const query = obj(spec.query);
288
+ const target: Json = this.querySpec(query, subject);
289
+ if (typeof spec.refId === "string") target.refId = spec.refId;
290
+ if (spec.hidden === true) target.hide = true;
291
+ const ds = this.datasourceOf(query);
292
+ if (ds) target.datasource = ds;
293
+ return target;
294
+ }
295
+
296
+ /** The panel's datasource, derived from its queries as Grafana does: the one they share, else mixed. */
297
+ panelDatasource(targets: Json[]): Json | undefined {
298
+ if (targets.length === 0) return undefined;
299
+ if (targets.filter((t) => obj(t.datasource).uid === DASHBOARD_DS).length > 1) return { type: "mixed", uid: "-- Mixed --" };
300
+ const key = (t: Json) => `${String(obj(t.datasource).type ?? "")}\u0000${String(obj(t.datasource).uid ?? "")}`;
301
+ const first = key(targets[0]);
302
+ if (targets.some((t) => key(t) !== first)) return { type: "mixed", uid: "-- Mixed --" };
303
+ const ds = obj(targets[0].datasource);
304
+ if (ds.uid === DASHBOARD_DS) return { type: "datasource", uid: DASHBOARD_DS };
305
+ return Object.keys(ds).length > 0 ? { ...ds } : undefined;
306
+ }
307
+
308
+ // ── panels ──────────────────────────────────────────────────────
309
+
310
+ /** The element a layout item places, as a classic panel at `gridPos`; undefined when it cannot be placed. */
311
+ element(name: unknown, gridPos: Json, repeat: Json | undefined, where: string): Json | undefined {
312
+ if (typeof name !== "string" || !(name in this.elements)) {
313
+ this.warn(`layout: ${where} places the element ${JSON.stringify(name)}, which the dashboard does not have, so it is left out`);
314
+ return undefined;
315
+ }
316
+ if (this.placed.has(name)) {
317
+ this.warn(`layout: ${where} places the element "${name}" a second time; a classic dashboard holds each panel once, so the second is left out`);
318
+ return undefined;
319
+ }
320
+ this.placed.add(name);
321
+ const el = obj(this.elements[name]);
322
+ const spec = obj(el.spec);
323
+ const panel: Json = {};
324
+ if (el.kind === "LibraryPanel") {
325
+ this.keys(spec, "LibraryPanelKindSpec", `library panel ${quote(spec.title)}`);
326
+ panel.id = spec.id;
327
+ panel.title = spec.title;
328
+ panel.gridPos = gridPos;
329
+ const lp = obj(spec.libraryPanel);
330
+ panel.libraryPanel = { uid: lp.uid, name: lp.name };
331
+ return panel;
332
+ }
333
+ if (el.kind !== "Panel") {
334
+ this.warn(`element "${name}" is a ${String(el.kind)}, which is not a panel or library panel, so it is left out`);
335
+ return undefined;
336
+ }
337
+ const subject = `panel ${quote(spec.title)}${spec.id !== undefined ? ` (id ${String(spec.id)})` : ""}`;
338
+ this.keys(spec, "PanelSpec", subject);
339
+ const viz = obj(spec.vizConfig);
340
+ this.keys(viz, "VizConfigKind", subject);
341
+ const vizSpec = obj(viz.spec);
342
+ this.keys(vizSpec, "VizConfigSpec", subject);
343
+ const data = obj(obj(spec.data).spec);
344
+ this.keys(data, "QueryGroupSpec", subject);
345
+
346
+ panel.id = spec.id;
347
+ panel.title = spec.title;
348
+ if (typeof spec.description === "string" && spec.description !== "") panel.description = spec.description;
349
+ panel.type = str(viz.group) ?? str(viz.kind);
350
+ if (typeof viz.version === "string" && viz.version !== "") panel.pluginVersion = viz.version;
351
+ panel.gridPos = gridPos;
352
+ if (repeat) Object.assign(panel, repeat);
353
+ if (vizSpec.options !== undefined) panel.options = vizSpec.options;
354
+ if (vizSpec.fieldConfig !== undefined) panel.fieldConfig = vizSpec.fieldConfig;
355
+ if (arr(spec.links).length > 0) panel.links = spec.links;
356
+
357
+ const targets = arr(data.queries).map((q) => this.target(obj(q), subject));
358
+ const ds = this.panelDatasource(targets);
359
+ if (ds) panel.datasource = ds;
360
+ panel.targets = targets;
361
+
362
+ const transformations = arr(data.transformations).map((t) => this.transformation(obj(t), subject));
363
+ if (transformations.length > 0) panel.transformations = transformations;
364
+
365
+ const qo = obj(data.queryOptions);
366
+ this.keys(qo, "QueryOptionsSpec", subject);
367
+ for (const key of V2_KEYS.QueryOptionsSpec.carried) if (qo[key] !== undefined) panel[key] = qo[key];
368
+ if (spec.transparent !== undefined) panel.transparent = spec.transparent;
369
+ return panel;
370
+ }
371
+
372
+ /** A transformation: v2 names it by `group`, v2beta1 by `kind` and `spec.id`. */
373
+ transformation(t: Json, subject: string): Json {
374
+ const spec = obj(t.spec);
375
+ const v2 = t.kind === "Transformation";
376
+ if (v2) {
377
+ this.keys(t, "TransformationKind", subject);
378
+ this.keys(spec, "TransformationSpec", subject);
379
+ }
380
+ const out: Json = { id: v2 ? t.group : (spec.id ?? t.kind) };
381
+ for (const key of ["disabled", "filter", "topic"]) if (spec[key] !== undefined) out[key] = spec[key];
382
+ out.options = spec.options ?? {};
383
+ return out;
384
+ }
385
+
386
+ // ── layouts ─────────────────────────────────────────────────────
387
+
388
+ private repeatOf(repeat: unknown, where: string): Json | undefined {
389
+ if (!isObject(repeat)) return undefined;
390
+ if (repeat.mode !== undefined && repeat.mode !== "variable") {
391
+ this.warn(`layout: ${where} repeats by ${String(repeat.mode)}, which a classic dashboard cannot, so it does not repeat`);
392
+ return undefined;
393
+ }
394
+ if (typeof repeat.value !== "string" || repeat.value === "") return undefined;
395
+ const out: Json = { repeat: repeat.value };
396
+ if (repeat.direction === "h" || repeat.direction === "v") out.repeatDirection = repeat.direction;
397
+ if (typeof repeat.maxPerRow === "number") out.maxPerRow = repeat.maxPerRow;
398
+ return out;
399
+ }
400
+
401
+ /** A grid's items, each moved down by `dy`. */
402
+ grid(layout: Json, dy: number, where: string): Json[] {
403
+ const out: Json[] = [];
404
+ for (const item of arr(obj(layout.spec).items)) {
405
+ const spec = obj(obj(item).spec);
406
+ this.keys(spec, "GridLayoutItemSpec", `layout: ${where}`);
407
+ const gridPos = { h: spec.height, w: spec.width, x: spec.x, y: (num(spec.y) ?? 0) + dy };
408
+ const repeat = isObject(spec.repeat) ? (this.keys(spec.repeat, "RepeatOptions", `layout: ${where}`), this.repeatOf(spec.repeat, where)) : undefined;
409
+ const panel = this.element(obj(spec.element).name, gridPos, repeat, where);
410
+ if (panel) out.push(panel);
411
+ }
412
+ return out;
413
+ }
414
+
415
+ /** An auto grid's items at the fixed positions Grafana gives them, from `y`. */
416
+ autoGrid(layout: Json, y: number, where: string): Json[] {
417
+ const spec = obj(layout.spec);
418
+ this.keys(spec, "AutoGridLayoutSpec", `layout: ${where}`);
419
+ const maxColumns = num(spec.maxColumnCount) !== undefined && (spec.maxColumnCount as number) > 0 ? (spec.maxColumnCount as number) : 3;
420
+ const w = Math.trunc(GRID_COLUMNS / maxColumns);
421
+ const heights: Record<string, number> = { short: 5, standard: 9, tall: 14 };
422
+ let h = heights[String(spec.rowHeightMode)] ?? 9;
423
+ if (spec.rowHeightMode === "custom" && typeof spec.rowHeight === "number" && spec.rowHeight > 0) h = Math.ceil(spec.rowHeight / 38);
424
+ const items = arr(spec.items);
425
+ this.warn(
426
+ `layout: ${where} is an auto grid, which a classic dashboard does not have. Its ${items.length} ${items.length === 1 ? "panel is" : "panels are"} ` +
427
+ `placed on a fixed grid, ${maxColumns} to a row at ${w}x${h}, as Grafana places them when it reads the dashboard at v1, and no longer resize with the screen`,
428
+ );
429
+ const out: Json[] = [];
430
+ let x = 0;
431
+ for (const item of items) {
432
+ const itemSpec = obj(obj(item).spec);
433
+ this.keys(itemSpec, "AutoGridLayoutItemSpec", `layout: a panel in ${where}`);
434
+ let repeat: Json | undefined;
435
+ if (isObject(itemSpec.repeat)) {
436
+ repeat = this.repeatOf({ ...itemSpec.repeat, direction: "h", maxPerRow: maxColumns }, where);
437
+ }
438
+ const panel = this.element(obj(itemSpec.element).name, { h, w, x, y }, repeat, where);
439
+ if (panel) out.push(panel);
440
+ x += w;
441
+ if (x >= GRID_COLUMNS) {
442
+ x = 0;
443
+ y += h;
444
+ }
445
+ }
446
+ return out;
447
+ }
448
+
449
+ private bottom(panels: Json[], from: number): number {
450
+ let max = from;
451
+ for (const p of panels) {
452
+ const g = obj(p.gridPos);
453
+ const end = (num(g.y) ?? 0) + (num(g.h) ?? 0);
454
+ if (end > max) max = end;
455
+ }
456
+ return max;
457
+ }
458
+
459
+ private rowPanel(title: unknown, y: number, extra: Json): Json {
460
+ const row: Json = { type: "row", id: this.nextRowId++, title: typeof title === "string" ? title : "", gridPos: { h: 1, w: GRID_COLUMNS, x: 0, y }, ...extra };
461
+ return row;
462
+ }
463
+
464
+ /** Panels of a layout that is not rows or tabs, from `y`, and where they end. */
465
+ private flat(layout: Json, y: number, where: string): { panels: Json[]; end: number } {
466
+ if (layout.kind === "GridLayout") {
467
+ const panels = this.grid(layout, y, where);
468
+ return { panels, end: this.bottom(panels, y) };
469
+ }
470
+ if (layout.kind === "AutoGridLayout") {
471
+ const panels = this.autoGrid(layout, y, where);
472
+ return { panels, end: this.bottom(panels, y) };
473
+ }
474
+ this.warn(`layout: ${where} has a ${String(layout.kind)}, which is not a layout chant reads, so its panels are left out`);
475
+ return { panels: [], end: y };
476
+ }
477
+
478
+ /** Rows or tabs, flattened into classic rows and panels from `y`. */
479
+ sections(layout: Json, y: number, where: string): { panels: Json[]; end: number } {
480
+ const panels: Json[] = [];
481
+ let at = y;
482
+ if (layout.kind === "RowsLayout") {
483
+ arr(obj(layout.spec).rows).forEach((row, i) => {
484
+ const r = this.row(obj(obj(row).spec), at, i, where);
485
+ panels.push(...r.panels);
486
+ at = r.end;
487
+ });
488
+ } else if (layout.kind === "TabsLayout") {
489
+ const tabs = arr(obj(layout.spec).tabs).map((t) => obj(obj(t).spec));
490
+ this.warn(
491
+ `layout: ${where === "the dashboard" ? "the dashboard's" : `${where}'s`} tabs ${andList(tabs.map((t) => quote(t.title)))} become expanded rows, ` +
492
+ "since a classic dashboard has no tabs: every tab's panels show on one page, one after another",
493
+ );
494
+ for (const tab of tabs) {
495
+ const r = this.tab(tab, at);
496
+ panels.push(...r.panels);
497
+ at = r.end;
498
+ }
499
+ } else {
500
+ return this.flat(layout, y, where);
501
+ }
502
+ return { panels, end: at };
503
+ }
504
+
505
+ private row(spec: Json, y: number, index: number, parent: string): { panels: Json[]; end: number } {
506
+ const where = `the row ${quote(spec.title)}`;
507
+ this.keys(spec, "RowsLayoutRowSpec", `layout: ${where}`);
508
+ const hidden = spec.hideHeader === true;
509
+ const layout = obj(spec.layout);
510
+ const panels: Json[] = [];
511
+ let at = y;
512
+ if (hidden && (index > 0 || parent !== "the dashboard")) {
513
+ this.warn(
514
+ `layout: ${where} has a hidden header, which a classic dashboard has only for the panels above its first row; ` +
515
+ "here its panels follow the row before them, so they become part of that row",
516
+ );
517
+ }
518
+ if (layout.kind === "RowsLayout" || layout.kind === "TabsLayout") {
519
+ if (!hidden) {
520
+ panels.push(this.rowPanel(spec.title, at, { collapsed: false, panels: [] }));
521
+ at++;
522
+ }
523
+ if (spec.collapse === true) this.warn(`layout: ${where} is collapsed, but it holds rows or tabs, which a classic row cannot, so it is written expanded`);
524
+ if (layout.kind === "RowsLayout") this.warn(`layout: the rows inside ${where} come after it as rows of their own, since classic rows do not nest`);
525
+ const nested = this.sections(layout, at, where);
526
+ panels.push(...nested.panels);
527
+ return { panels, end: this.bottom(nested.panels, at) };
528
+ }
529
+ const collapsed = spec.collapse === true;
530
+ if (!hidden) {
531
+ const extra: Json = { collapsed };
532
+ const repeat = this.repeatOf(spec.repeat, where);
533
+ if (repeat) extra.repeat = repeat.repeat;
534
+ const row = this.rowPanel(spec.title, at, extra);
535
+ at++;
536
+ if (collapsed) {
537
+ const inner = this.flat(layout, at, where);
538
+ row.panels = inner.panels;
539
+ panels.push(row);
540
+ return { panels, end: at };
541
+ }
542
+ row.panels = [];
543
+ panels.push(row);
544
+ }
545
+ const inner = this.flat(layout, at, where);
546
+ panels.push(...inner.panels);
547
+ return { panels, end: Math.max(inner.end, at) };
548
+ }
549
+
550
+ private tab(spec: Json, y: number): { panels: Json[]; end: number } {
551
+ const where = `the tab ${quote(spec.title)}`;
552
+ this.keys(spec, "TabsLayoutTabSpec", `layout: ${where}`);
553
+ const extra: Json = { collapsed: false, panels: [] };
554
+ const repeat = this.repeatOf(spec.repeat, where);
555
+ if (repeat) extra.repeat = repeat.repeat;
556
+ const panels: Json[] = [this.rowPanel(spec.title, y, extra)];
557
+ const layout = obj(spec.layout);
558
+ if (layout.kind === "RowsLayout") this.warn(`layout: the rows inside ${where} come after its row as rows of their own, since classic rows do not nest`);
559
+ const inner = this.sections(layout, y + 1, where);
560
+ panels.push(...inner.panels);
561
+ return { panels, end: Math.max(inner.end, y + 1) };
562
+ }
563
+
564
+ /** Every element no layout item placed. */
565
+ unplaced(): void {
566
+ const left = Object.keys(this.elements).filter((n) => !this.placed.has(n));
567
+ if (left.length > 0) {
568
+ this.warn(`elements: ${left.map((n) => `"${n}"`).join(", ")} ${left.length === 1 ? "is" : "are"} not placed in the layout, so Grafana does not show ${left.length === 1 ? "it" : "them"} and ${left.length === 1 ? "it is" : "they are"} left out`);
569
+ }
570
+ }
571
+
572
+ // ── variables ───────────────────────────────────────────────────
573
+
574
+ private current(v: unknown): Json {
575
+ const c = obj(v);
576
+ return { text: c.text ?? "", value: c.value ?? "" };
577
+ }
578
+
579
+ variable(kind: Json): Json | undefined {
580
+ const k = String(kind.kind);
581
+ const def = VARIABLE_SPECS[k];
582
+ const spec = obj(kind.spec);
583
+ const subject = `variable "${String(spec.name ?? "")}"`;
584
+ if (!def) {
585
+ this.warn(`${subject} is a ${k}, which is not a variable kind the dashboardv2 schema at the pin has, so it is left out`);
586
+ return undefined;
587
+ }
588
+ if (k === "GroupByVariable" || k === "AdhocVariable") this.keys(kind, `${k}Kind`, subject);
589
+ this.keys(spec, def, subject);
590
+
591
+ const out: Json = { name: spec.name };
592
+ const hide = str(spec.hide);
593
+ if (hide === "inControlsMenu") {
594
+ this.warn(`${subject}: hide is not carried (it shows in the controls menu, which a classic dashboard does not have, so it shows in the variable bar)`);
595
+ } else if (hide !== undefined && hide in VARIABLE_HIDE) {
596
+ out.hide = VARIABLE_HIDE[hide];
597
+ }
598
+ if (spec.label !== undefined) out.label = spec.label;
599
+ if (spec.description !== undefined) out.description = spec.description;
600
+ if (spec.skipUrlSync !== undefined) out.skipUrlSync = spec.skipUrlSync;
601
+ const refresh = (v: unknown) => (typeof v === "string" && v in VARIABLE_REFRESH ? VARIABLE_REFRESH[v] : undefined);
602
+ const copy = (...keys: string[]) => {
603
+ for (const key of keys) if (spec[key] !== undefined) out[key] = spec[key];
604
+ };
605
+
606
+ switch (k) {
607
+ case "QueryVariable": {
608
+ out.type = "query";
609
+ const query = obj(spec.query);
610
+ const q = this.querySpec(query, subject);
611
+ const legacy = obj(query.spec)[LEGACY_STRING_VALUE_KEY];
612
+ out.query = typeof legacy === "string" ? legacy : q;
613
+ const ds = this.datasourceOf(query);
614
+ if (ds) out.datasource = ds;
615
+ if (refresh(spec.refresh) !== undefined) out.refresh = refresh(spec.refresh);
616
+ if (typeof spec.sort === "string" && spec.sort in VARIABLE_SORT) out.sort = VARIABLE_SORT[spec.sort];
617
+ copy("regex", "regexApplyTo", "definition", "multi", "includeAll", "allValue", "allowCustomValue", "staticOptions", "staticOptionsOrder", "options");
618
+ out.current = this.current(spec.current);
619
+ break;
620
+ }
621
+ case "DatasourceVariable":
622
+ out.type = "datasource";
623
+ out.query = spec.pluginId;
624
+ if (refresh(spec.refresh) !== undefined) out.refresh = refresh(spec.refresh);
625
+ copy("regex", "multi", "includeAll", "allValue", "allowCustomValue", "options");
626
+ out.current = this.current(spec.current);
627
+ break;
628
+ case "CustomVariable":
629
+ out.type = "custom";
630
+ copy("query", "multi", "includeAll", "allValue", "allowCustomValue", "valuesFormat", "options");
631
+ out.current = this.current(spec.current);
632
+ break;
633
+ case "ConstantVariable":
634
+ out.type = "constant";
635
+ out.hide = 2; // Grafana always hides a constant.
636
+ copy("query");
637
+ out.current = this.current(spec.current);
638
+ break;
639
+ case "IntervalVariable":
640
+ out.type = "interval";
641
+ copy("query", "auto", "auto_min", "auto_count", "options");
642
+ if (refresh(spec.refresh) !== undefined) out.refresh = refresh(spec.refresh);
643
+ out.current = this.current(spec.current);
644
+ break;
645
+ case "TextVariable":
646
+ out.type = "textbox";
647
+ copy("query");
648
+ out.current = this.current(spec.current);
649
+ break;
650
+ case "GroupByVariable": {
651
+ out.type = "groupby";
652
+ const ds = this.datasourceOf(kind);
653
+ if (ds) out.datasource = ds;
654
+ copy("multi", "options");
655
+ out.current = this.current(spec.current);
656
+ if (spec.defaultValue !== undefined) out.defaultValue = this.current(spec.defaultValue);
657
+ break;
658
+ }
659
+ case "AdhocVariable": {
660
+ out.type = "adhoc";
661
+ const ds = this.datasourceOf(kind);
662
+ if (ds) out.datasource = ds;
663
+ copy("allowCustomValue", "enableGroupBy");
664
+ for (const key of ["filters", "baseFilters", "defaultKeys"]) if (arr(spec[key]).length > 0) out[key] = spec[key];
665
+ break;
666
+ }
667
+ case "SwitchVariable":
668
+ out.type = "switch";
669
+ out.query = "";
670
+ out.current = { text: spec.current, value: spec.current };
671
+ out.options = [
672
+ { text: spec.enabledValue, value: spec.enabledValue, selected: spec.current === spec.enabledValue },
673
+ { text: spec.disabledValue, value: spec.disabledValue, selected: spec.current === spec.disabledValue },
674
+ ];
675
+ break;
676
+ }
677
+ return out;
678
+ }
679
+
680
+ // ── annotations, links, time ────────────────────────────────────
681
+
682
+ annotation(kind: Json): Json {
683
+ const spec = obj(kind.spec);
684
+ const subject = `annotation ${quote(spec.name)}`;
685
+ this.keys(spec, "AnnotationQuerySpec", subject);
686
+ const out: Json = { name: spec.name, enable: spec.enable, hide: spec.hide, iconColor: spec.iconColor };
687
+ if (spec.builtIn === true) {
688
+ out.builtIn = 1;
689
+ out.type = "dashboard";
690
+ }
691
+ const query = obj(spec.query);
692
+ const ds = this.datasourceOf(query);
693
+ if (ds) out.datasource = ds;
694
+ const target = this.querySpec(query, subject);
695
+ if (Object.keys(target).length > 0) out.target = target;
696
+ if (isObject(spec.filter) && Object.keys(spec.filter).length > 0) out.filter = spec.filter;
697
+ if (isObject(spec.mappings) && Object.keys(spec.mappings).length > 0) out.mappings = spec.mappings;
698
+ const reserved = new Set(["name", "enable", "hide", "iconColor", "datasource", "target", "filter", "builtIn", "placement", "mappings"]);
699
+ for (const [k, v] of Object.entries(obj(spec.legacyOptions))) if (!reserved.has(k)) out[k] = v;
700
+ return out;
701
+ }
702
+
703
+ link(link: Json): Json {
704
+ this.keys(link, "DashboardLink", `dashboard link ${quote(link.title)}`);
705
+ const { origin: _origin, ...rest } = link;
706
+ return rest;
707
+ }
708
+
709
+ time(ts: Json, d: Json): void {
710
+ this.keys(ts, "TimeSettingsSpec", "time settings");
711
+ d.time = { from: str(ts.from) || "now-6h", to: str(ts.to) || "now" };
712
+ if (ts.timezone !== undefined) d.timezone = ts.timezone;
713
+ if (ts.autoRefresh !== undefined) d.refresh = ts.autoRefresh;
714
+ if (ts.fiscalYearStartMonth !== undefined) d.fiscalYearStartMonth = ts.fiscalYearStartMonth;
715
+ if (ts.weekStart !== undefined) d.weekStart = ts.weekStart;
716
+ const timepicker: Json = {};
717
+ if (arr(ts.autoRefreshIntervals).length > 0) timepicker.refresh_intervals = ts.autoRefreshIntervals;
718
+ if (ts.hideTimepicker === true) timepicker.hidden = true;
719
+ if (ts.nowDelay !== undefined) timepicker.nowDelay = ts.nowDelay;
720
+ if (arr(ts.quickRanges).length > 0) {
721
+ timepicker.quick_ranges = arr(ts.quickRanges).map((r) => ({ display: obj(r).display, from: obj(r).from, to: obj(r).to }));
722
+ }
723
+ if (Object.keys(timepicker).length > 0) d.timepicker = timepicker;
724
+ }
725
+
726
+ // ── the dashboard ───────────────────────────────────────────────
727
+
728
+ read(uid: string | undefined): Json {
729
+ const s = this.spec;
730
+ this.keys(s, "Dashboard", "dashboard");
731
+ const d: Json = {};
732
+ d.annotations = { list: arr(s.annotations).map((a) => this.annotation(obj(a))) };
733
+ if (s.description !== undefined) d.description = s.description;
734
+ d.editable = s.editable ?? true;
735
+ d.graphTooltip = CURSOR_SYNC[String(s.cursorSync)] ?? 0;
736
+ if (s.liveNow !== undefined) d.liveNow = s.liveNow;
737
+ if (s.preload !== undefined) d.preload = s.preload;
738
+ if (s.revision !== undefined) d.revision = s.revision;
739
+ if (arr(s.links).length > 0) d.links = arr(s.links).map((l) => this.link(obj(l)));
740
+ const layout = obj(s.layout);
741
+ d.panels = this.sections(layout, 0, "the dashboard").panels;
742
+ this.unplaced();
743
+ d.schemaVersion = DASHBOARD_SCHEMA_VERSION;
744
+ if (arr(s.tags).length > 0) d.tags = s.tags;
745
+ const variables = arr(s.variables)
746
+ .map((v) => this.variable(obj(v)))
747
+ .filter((v): v is Json => v !== undefined);
748
+ if (variables.length > 0) d.templating = { list: variables };
749
+ this.time(obj(s.timeSettings), d);
750
+ d.title = s.title ?? "";
751
+ if (uid !== undefined && uid !== "") d.uid = uid;
752
+ return d;
753
+ }
754
+ }
755
+
756
+ /** The `apiVersion` of a `dashboard.grafana.app` resource, e.g. `v2beta1`; undefined for anything else. */
757
+ function resourceVersion(data: Json): string | undefined {
758
+ const m = /^dashboard\.grafana\.app\/(.+)$/.exec(String(data.apiVersion ?? ""));
759
+ return m ? m[1] : undefined;
760
+ }
761
+
762
+ /**
763
+ * Read a v2 dashboard: a `dashboard.grafana.app/v2` or `v2beta1` resource,
764
+ * or a bare v2 spec. Throws for `v2alpha1`, whose panel queries have another
765
+ * shape; Grafana serves every stored dashboard at v2, so the fix is to read
766
+ * it there.
767
+ */
768
+ export function readV2Dashboard(data: Json): V2Read {
769
+ const version = resourceVersion(data);
770
+ if (version !== undefined && version !== "v2" && version !== "v2beta1") {
771
+ throw new Error(
772
+ `this is a dashboard.grafana.app/${version} dashboard; chant reads v2 and v2beta1. Read it from Grafana at dashboard.grafana.app/v2 and import that.`,
773
+ );
774
+ }
775
+ const spec = version !== undefined ? obj(data.spec) : data;
776
+ const meta = obj(data.metadata);
777
+ const reader = new V2Reader(spec);
778
+ const annotations = obj(meta.annotations);
779
+ if (typeof annotations["grafana.app/folder"] === "string" && annotations["grafana.app/folder"] !== "") {
780
+ reader.warn(`metadata: the folder "${String(annotations["grafana.app/folder"])}" is not carried (a dashboard's folder is set where it is provisioned)`);
781
+ }
782
+ const dashboard = reader.read(str(meta.name) ?? str(spec.uid));
783
+ return { dashboard, warnings: reader.warnings };
784
+ }
785
+
786
+ /**
787
+ * The version Grafana stores a dashboard at, when a `dashboard.grafana.app`
788
+ * resource read at v0 or v1 says it is stored as v2 (`status.conversion`):
789
+ * that read is a down-conversion that drops whatever v2 has and v1 does not
790
+ * (tabs, auto grids, conditional rendering), with nothing in the spec to show
791
+ * it. Also returns the stored version when the conversion failed. Undefined
792
+ * for everything else.
793
+ */
794
+ export function lossyV1Read(data: unknown): { apiVersion: string; storedVersion: string; failed: boolean } | undefined {
795
+ if (!isObject(data)) return undefined;
796
+ const version = resourceVersion(data);
797
+ if (version === undefined || version.startsWith("v2")) return undefined;
798
+ const conversion = obj(obj(data.status).conversion);
799
+ const storedVersion = str(conversion.storedVersion) ?? "";
800
+ const failed = conversion.failed === true;
801
+ if (!storedVersion.startsWith("v2") && !failed) return undefined;
802
+ return { apiVersion: String(data.apiVersion), storedVersion, failed };
803
+ }