@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,58 @@
1
+ /**
2
+ * Binding a client for one environment: the config lookup, the target, the
3
+ * transport. Shared by observe, export and (#2948) apply, so all three read
4
+ * and write the same Grafana.
5
+ */
6
+
7
+ import type { ChantConfig } from "@intentius/chant/config";
8
+ import type { UnobservedReason } from "@intentius/chant/observation";
9
+ import { GrafanaApiError, GrafanaClient, grafanaHttp, type GrafanaHttp } from "./client";
10
+ import { isUnresolvedTarget, resolveGrafanaTarget, type UnresolvedTarget } from "../config";
11
+
12
+ /** What callers may inject: tests pass `http` (and usually `config`), the CLI passes nothing. */
13
+ export interface BindOptions {
14
+ environment?: string;
15
+ /** The project's config. Loaded from `cwd` (default: the working directory) when omitted. */
16
+ config?: Pick<ChantConfig, "grafana">;
17
+ cwd?: string;
18
+ env?: Record<string, string | undefined>;
19
+ /** Transport override. The target is still resolved, so a test exercises the binding too. */
20
+ http?: GrafanaHttp;
21
+ }
22
+
23
+ /** A target that could not be resolved, thrown by {@link bindGrafana} and classified by {@link classifyGrafanaFailure}. */
24
+ export class GrafanaBindingError extends Error {
25
+ constructor(readonly unresolved: UnresolvedTarget) {
26
+ super(unresolved.detail);
27
+ this.name = "GrafanaBindingError";
28
+ }
29
+ }
30
+
31
+ async function loadConfig(cwd: string): Promise<Pick<ChantConfig, "grafana"> | undefined> {
32
+ try {
33
+ const { loadChantConfig } = await import("@intentius/chant/config");
34
+ return (await loadChantConfig(cwd)).config;
35
+ } catch {
36
+ // No project config (an ad-hoc export): the environment variables still bind.
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ /** Resolve the environment's target and return a client for it. Throws {@link GrafanaBindingError} when there is none. */
42
+ export async function bindGrafana(options: BindOptions = {}): Promise<GrafanaClient> {
43
+ const config = options.config ?? (await loadConfig(options.cwd ?? process.cwd()));
44
+ const target = resolveGrafanaTarget({ environment: options.environment, config, env: options.env });
45
+ if (isUnresolvedTarget(target)) throw new GrafanaBindingError(target);
46
+ return new GrafanaClient(options.http ?? grafanaHttp(target), target);
47
+ }
48
+
49
+ /** What a failed bind or read means, in the observation vocabulary. */
50
+ export function classifyGrafanaFailure(err: unknown): { reason: UnobservedReason; detail: string } {
51
+ if (err instanceof GrafanaBindingError) return { reason: err.unresolved.reason, detail: err.unresolved.detail };
52
+ if (err instanceof GrafanaApiError) {
53
+ return err.verdict === "refused"
54
+ ? { reason: "no-credentials", detail: `${err.message} (the credentials were refused)` }
55
+ : { reason: "read-failed", detail: err.message };
56
+ }
57
+ return { reason: "read-failed", detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
58
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The Grafana HTTP transport (#2946): one client for observe, export and,
3
+ * later, the API applier (#2948), so all three reach the same instance with
4
+ * the same credentials.
5
+ *
6
+ * `GrafanaHttp` is the seam. The default is `fetch` against a resolved
7
+ * target (./target.ts); a test hands in a function that answers by path, so
8
+ * nothing needs a module mock.
9
+ *
10
+ * Status mapping is in one place ({@link statusVerdict}): a 404 is the only
11
+ * answer that means "not there", 401/403 mean the credentials were refused,
12
+ * and anything else that is not a 2xx is a failed read. A reader that turned
13
+ * a 500 or a refused token into "absent" would have `chant lifecycle plan`
14
+ * propose creating a dashboard that is running.
15
+ */
16
+
17
+ /** How the client authenticates. A service account token is the usual form; basic auth suits a local instance. */
18
+ export type GrafanaAuth = { readonly token: string } | { readonly user: string; readonly password: string };
19
+
20
+ /** A resolved Grafana instance: where, as whom, which organisation, and where that came from. */
21
+ export interface GrafanaTarget {
22
+ /** Base URL, without a trailing slash: `https://grafana.example.com`. */
23
+ readonly url: string;
24
+ readonly auth?: GrafanaAuth;
25
+ /** Organisation id; 1 when not set. Selects the `X-Grafana-Org-Id` header and the API namespace. */
26
+ readonly orgId?: number;
27
+ /**
28
+ * The `dashboard.grafana.app` namespace. Defaults to `default` for org 1
29
+ * and `org-<id>` otherwise; Grafana Cloud stacks use `stacks-<id>`.
30
+ */
31
+ readonly namespace?: string;
32
+ /** Where the binding came from, in the project's vocabulary: `grafana.profiles.prod`, `env GRAFANA_URL`. */
33
+ readonly source: string;
34
+ }
35
+
36
+ export interface GrafanaResponse {
37
+ readonly status: number;
38
+ /** The parsed body, or undefined when it was empty or not JSON. */
39
+ readonly json: unknown;
40
+ }
41
+
42
+ /** One request. `path` starts with `/` and is relative to the target's URL. */
43
+ export type GrafanaHttp = (method: string, path: string, body?: unknown) => Promise<GrafanaResponse>;
44
+
45
+ /** The API namespace for a target (see {@link GrafanaTarget.namespace}). */
46
+ export function namespaceOf(target: Pick<GrafanaTarget, "orgId" | "namespace">): string {
47
+ if (target.namespace) return target.namespace;
48
+ const org = target.orgId ?? 1;
49
+ return org === 1 ? "default" : `org-${org}`;
50
+ }
51
+
52
+ function authHeader(auth: GrafanaAuth | undefined): Record<string, string> {
53
+ if (!auth) return {};
54
+ if ("token" in auth) return { authorization: `Bearer ${auth.token}` };
55
+ return { authorization: `Basic ${Buffer.from(`${auth.user}:${auth.password}`).toString("base64")}` };
56
+ }
57
+
58
+ /** The default transport: `fetch` against the target, JSON in and out. */
59
+ export function grafanaHttp(target: GrafanaTarget, fetchImpl: typeof fetch = fetch): GrafanaHttp {
60
+ const base = target.url.replace(/\/+$/, "");
61
+ const headers: Record<string, string> = {
62
+ accept: "application/json",
63
+ ...authHeader(target.auth),
64
+ ...(target.orgId !== undefined ? { "x-grafana-org-id": String(target.orgId) } : {}),
65
+ };
66
+ return async (method, path, body) => {
67
+ const res = await fetchImpl(`${base}${path}`, {
68
+ method,
69
+ headers: body === undefined ? headers : { ...headers, "content-type": "application/json" },
70
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
71
+ });
72
+ const text = await res.text();
73
+ let json: unknown;
74
+ if (text !== "") {
75
+ try {
76
+ json = JSON.parse(text);
77
+ } catch {
78
+ json = undefined;
79
+ }
80
+ }
81
+ return { status: res.status, json };
82
+ };
83
+ }
84
+
85
+ /** What a status means for a read. */
86
+ export type StatusVerdict = "ok" | "not-found" | "refused" | "failed";
87
+
88
+ export function statusVerdict(status: number): StatusVerdict {
89
+ if (status >= 200 && status < 300) return "ok";
90
+ if (status === 404) return "not-found";
91
+ if (status === 401 || status === 403) return "refused";
92
+ return "failed";
93
+ }
94
+
95
+ /** A non-2xx answer, with enough to classify it and to say where it came from. */
96
+ export class GrafanaApiError extends Error {
97
+ readonly verdict: StatusVerdict;
98
+ constructor(
99
+ readonly status: number,
100
+ readonly method: string,
101
+ readonly path: string,
102
+ body: unknown,
103
+ ) {
104
+ const message = body && typeof body === "object" && typeof (body as { message?: unknown }).message === "string" ? (body as { message: string }).message : "";
105
+ super(`${method} ${path} returned ${status}${message ? `: ${message}` : ""}`);
106
+ this.name = "GrafanaApiError";
107
+ this.verdict = statusVerdict(status);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * A bound client: the transport, the target it reaches, and per-client
113
+ * caches (the API version a server serves, folder titles) so N reads do not
114
+ * repeat the same lookup.
115
+ */
116
+ export class GrafanaClient {
117
+ private readonly memo = new Map<string, Promise<unknown>>();
118
+
119
+ constructor(
120
+ readonly http: GrafanaHttp,
121
+ readonly target: GrafanaTarget,
122
+ ) {}
123
+
124
+ get namespace(): string {
125
+ return namespaceOf(this.target);
126
+ }
127
+
128
+ /** GET, returning the body; undefined on 404; throws {@link GrafanaApiError} on any other non-2xx. */
129
+ async get<T = unknown>(path: string): Promise<T | undefined> {
130
+ const res = await this.http("GET", path);
131
+ const verdict = statusVerdict(res.status);
132
+ if (verdict === "ok") return res.json as T;
133
+ if (verdict === "not-found") return undefined;
134
+ throw new GrafanaApiError(res.status, "GET", path, res.json);
135
+ }
136
+
137
+ /** Run `fn` once per key for this client's lifetime; concurrent callers share the promise. */
138
+ once<T>(key: string, fn: () => Promise<T>): Promise<T> {
139
+ let p = this.memo.get(key) as Promise<T> | undefined;
140
+ if (!p) {
141
+ p = fn();
142
+ this.memo.set(key, p);
143
+ // A failed lookup is not cached: the next caller asks again.
144
+ p.catch(() => this.memo.delete(key));
145
+ }
146
+ return p;
147
+ }
148
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Whether what Grafana holds already is what the applier would write
3
+ * (#2948), so a re-apply reports `unchanged` and writes nothing.
4
+ *
5
+ * Grafana does not answer this itself: a `PUT` of identical content still
6
+ * bumps `generation` and `resourceVersion` (measured on 12.4.11 and 13.2.2),
7
+ * so a no-op cannot be told from an update after the fact. And the stored
8
+ * dashboard is not the one sent: Grafana fills in defaults
9
+ * (`fiscalYearStartMonth: 0`, `links: []`, `timepicker: {}`, ...), adds the
10
+ * built-in annotation, drops `null`s and empty `options`, and may migrate
11
+ * `schemaVersion`.
12
+ *
13
+ * So the test is one-sided: every value the applier would send must be in
14
+ * the live object, equal, and anything Grafana added on top is ignored. An
15
+ * array must match element for element, since a panel added in the UI is a
16
+ * change. The noise is the same list the drift hooks carry (#2946):
17
+ *
18
+ * - `null` and empty objects sent are satisfied by an absent key;
19
+ * - the built-in "Annotations & Alerts" annotation is ignored on both sides;
20
+ * - `id`, `version` and `uid` at the top level are identity, not content, and
21
+ * `schemaVersion` is Grafana's to migrate.
22
+ */
23
+
24
+ type Json = Record<string, unknown>;
25
+
26
+ function isObject(v: unknown): v is Json {
27
+ return typeof v === "object" && v !== null && !Array.isArray(v);
28
+ }
29
+
30
+ /** Top-level dashboard keys that are identity or Grafana-managed, never content. */
31
+ export const IDENTITY_KEYS: readonly string[] = ["id", "uid", "version", "schemaVersion"];
32
+
33
+ function isBuiltInAnnotation(v: unknown): boolean {
34
+ return isObject(v) && (v.builtIn === 1 || v.builtIn === true);
35
+ }
36
+
37
+ function covers(want: unknown, live: unknown, path: string[]): boolean {
38
+ if (want === null || want === undefined) return live === null || live === undefined;
39
+ if (Array.isArray(want)) {
40
+ if (!Array.isArray(live)) return want.length === 0 && live === undefined;
41
+ const annotations = path[path.length - 1] === "list" && path[path.length - 2] === "annotations";
42
+ const w = annotations ? want.filter((a) => !isBuiltInAnnotation(a)) : want;
43
+ const l = annotations ? live.filter((a) => !isBuiltInAnnotation(a)) : live;
44
+ return w.length === l.length && w.every((x, i) => covers(x, l[i], [...path, String(i)]));
45
+ }
46
+ if (isObject(want)) {
47
+ if (live === undefined || live === null) return Object.values(want).every((v) => covers(v, undefined, path));
48
+ if (!isObject(live)) return false;
49
+ return Object.entries(want).every(([k, v]) => covers(v, live[k], [...path, k]));
50
+ }
51
+ return want === live;
52
+ }
53
+
54
+ /**
55
+ * True when `live` already holds everything in `want`. For a dashboard,
56
+ * the top-level identity keys ({@link IDENTITY_KEYS}) are left out of the
57
+ * comparison.
58
+ */
59
+ export function converged(want: unknown, live: unknown, opts: { dashboard?: boolean } = {}): boolean {
60
+ if (!opts.dashboard || !isObject(want)) return covers(want, live, []);
61
+ const content = Object.fromEntries(Object.entries(want).filter(([k]) => !IDENTITY_KEYS.includes(k)));
62
+ return covers(content, live, []);
63
+ }
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Reading dashboards (#2946).
3
+ *
4
+ * Grafana 12 and 13 serve dashboards as Kubernetes-style resources under
5
+ * `/apis/dashboard.grafana.app/<version>/namespaces/<ns>/dashboards/<uid>`:
6
+ * the classic dashboard JSON in `spec`, and who manages it in
7
+ * `metadata.annotations` and `metadata.labels` (./../ownership.ts). The
8
+ * version is discovered once per client from the API group, preferring the
9
+ * newest classic (v1-family) version the server serves: `v1` on 13.x,
10
+ * `v1beta1` on 12.x. A server without the group (Grafana 11) is read over
11
+ * `GET /api/dashboards/uid/<uid>`, which has the same JSON and no metadata
12
+ * channel.
13
+ *
14
+ * ## The version branch is one place
15
+ *
16
+ * {@link classicDashboardOf} decides how a stored dashboard is read as
17
+ * classic JSON. Everything downstream (observe, deep observe, export) only
18
+ * ever sees its answer. A dashboard Grafana stores as v2 (the layout-and-
19
+ * elements schema) is not read through Grafana's own v2-to-v1 conversion,
20
+ * which drops what v1 cannot hold with nothing to show it (the importer
21
+ * refuses that read too, `lossyV1Read`). Instead {@link readDashboard} and
22
+ * {@link listDashboards} read it again at the newest v2 version the server
23
+ * serves (`v2` on 13.x, `v2beta1` on 12.x), and `classicDashboardOf`
24
+ * converts that with the importer's v2 reader (./../import/v2.ts), which
25
+ * names everything v2 holds that the classic model cannot (#2947). Only a
26
+ * v2-stored dashboard the server cannot serve at v2, or whose conversion
27
+ * failed, is reported as not readable.
28
+ */
29
+
30
+ import { looksLikeV2Dashboard } from "../detect";
31
+ import { lossyV1Read, readV2Dashboard } from "../import/v2";
32
+ import type { GrafanaClient } from "./client";
33
+
34
+ type Json = Record<string, unknown>;
35
+
36
+ function isObject(v: unknown): v is Json {
37
+ return typeof v === "object" && v !== null && !Array.isArray(v);
38
+ }
39
+
40
+ function stringMap(v: unknown): Record<string, string> {
41
+ if (!isObject(v)) return {};
42
+ return Object.fromEntries(Object.entries(v).filter(([, x]) => typeof x === "string")) as Record<string, string>;
43
+ }
44
+
45
+ export const DASHBOARD_GROUP = "dashboard.grafana.app";
46
+
47
+ /** Classic-schema versions of the dashboard API, newest first. */
48
+ export const CLASSIC_DASHBOARD_VERSIONS: readonly string[] = ["v1", "v1beta1", "v0alpha1"];
49
+
50
+ /** v2-schema versions of the dashboard API the v2 reader understands, newest first. */
51
+ export const V2_DASHBOARD_VERSIONS: readonly string[] = ["v2", "v2beta1"];
52
+
53
+ /**
54
+ * How this server serves dashboards: the classic version it is read at,
55
+ * and the v2 version a dashboard stored as v2 is read at instead (#2947).
56
+ */
57
+ export type DashboardApi = { readonly kind: "apis"; readonly version: string; readonly v2Version?: string } | { readonly kind: "legacy" };
58
+
59
+ /** The dashboard API the server behind `client` serves, discovered once per client. */
60
+ export function dashboardApi(client: GrafanaClient): Promise<DashboardApi> {
61
+ return client.once("dashboard-api", async () => {
62
+ const group = await client.get<{ versions?: Array<{ version?: string }> }>(`/apis/${DASHBOARD_GROUP}`);
63
+ const served = new Set((group?.versions ?? []).map((v) => v.version).filter((v): v is string => typeof v === "string"));
64
+ const version = CLASSIC_DASHBOARD_VERSIONS.find((v) => served.has(v));
65
+ const v2Version = V2_DASHBOARD_VERSIONS.find((v) => served.has(v));
66
+ return version ? { kind: "apis", version, ...(v2Version ? { v2Version } : {}) } : { kind: "legacy" };
67
+ });
68
+ }
69
+
70
+ /** One dashboard as Grafana holds it. */
71
+ export interface LiveDashboard {
72
+ readonly uid: string;
73
+ /**
74
+ * The stored resource as it came back: a `dashboard.grafana.app` resource
75
+ * (`{ metadata, spec, status }`) or, on the legacy API, `{ dashboard, meta }`.
76
+ * Read it through {@link classicDashboardOf}.
77
+ */
78
+ readonly raw: Json;
79
+ readonly via: "apis" | "legacy";
80
+ /** `metadata.labels`. Empty on the legacy API, which has no labels. */
81
+ readonly labels: Readonly<Record<string, string>>;
82
+ /** `metadata.annotations`. Empty on the legacy API. */
83
+ readonly annotations: Readonly<Record<string, string>>;
84
+ /** The folder's uid, when it is in one. */
85
+ readonly folderUid?: string;
86
+ /** The folder's title, when the read returned it (the legacy API does). */
87
+ readonly folderTitle?: string;
88
+ /** The request path it was read from. */
89
+ readonly address: string;
90
+ }
91
+
92
+ /** A dashboard read: there, not there (404), or there but not readable as classic JSON. */
93
+ export type DashboardRead =
94
+ | { readonly present: LiveDashboard }
95
+ | { readonly absent: true; readonly address: string };
96
+
97
+ /** The request path for one dashboard on this API (at `version` instead of the classic one, when given). */
98
+ export function dashboardPath(api: DashboardApi, namespace: string, uid: string, version?: string): string {
99
+ const id = encodeURIComponent(uid);
100
+ return api.kind === "apis"
101
+ ? `/apis/${DASHBOARD_GROUP}/${version ?? api.version}/namespaces/${encodeURIComponent(namespace)}/dashboards/${id}`
102
+ : `/api/dashboards/uid/${id}`;
103
+ }
104
+
105
+ /**
106
+ * A classic read of a dashboard Grafana stores as v2 is a lossy
107
+ * down-conversion: read it again at v2, where it is whole. Anything else,
108
+ * and a v2-stored dashboard on a server with no v2 version the reader knows,
109
+ * comes back as it was (and `classicDashboardOf` says why it cannot be read).
110
+ */
111
+ async function atStoredVersion(client: GrafanaClient, api: DashboardApi, live: LiveDashboard): Promise<LiveDashboard> {
112
+ if (api.kind !== "apis" || !api.v2Version) return live;
113
+ const lossy = lossyV1Read(live.raw);
114
+ if (!lossy || lossy.failed || !lossy.storedVersion.startsWith("v2")) return live;
115
+ const address = dashboardPath(api, client.namespace, live.uid, api.v2Version);
116
+ const body = await client.get<Json>(address);
117
+ return body === undefined ? live : fromResource(body, address);
118
+ }
119
+
120
+ function fromResource(resource: Json, address: string): LiveDashboard {
121
+ const metadata = isObject(resource.metadata) ? resource.metadata : {};
122
+ const annotations = stringMap(metadata.annotations);
123
+ const folderUid = annotations["grafana.app/folder"];
124
+ return {
125
+ uid: typeof metadata.name === "string" ? metadata.name : "",
126
+ raw: resource,
127
+ via: "apis",
128
+ labels: stringMap(metadata.labels),
129
+ annotations,
130
+ ...(folderUid ? { folderUid } : {}),
131
+ address,
132
+ };
133
+ }
134
+
135
+ function fromLegacy(body: Json, address: string): LiveDashboard {
136
+ const dashboard = isObject(body.dashboard) ? body.dashboard : {};
137
+ const meta = isObject(body.meta) ? body.meta : {};
138
+ return {
139
+ uid: typeof dashboard.uid === "string" ? dashboard.uid : "",
140
+ raw: body,
141
+ via: "legacy",
142
+ labels: {},
143
+ annotations: {},
144
+ ...(typeof meta.folderUid === "string" && meta.folderUid !== "" ? { folderUid: meta.folderUid } : {}),
145
+ ...(typeof meta.folderTitle === "string" && meta.folderTitle !== "" && meta.folderUid ? { folderTitle: meta.folderTitle } : {}),
146
+ address,
147
+ };
148
+ }
149
+
150
+ /** Read one dashboard by uid. Throws `GrafanaApiError` for anything but a 2xx or a 404. */
151
+ export async function readDashboard(client: GrafanaClient, uid: string): Promise<DashboardRead> {
152
+ const api = await dashboardApi(client);
153
+ const address = dashboardPath(api, client.namespace, uid);
154
+ const body = await client.get<Json>(address);
155
+ if (body === undefined) return { absent: true, address };
156
+ if (api.kind !== "apis") return { present: fromLegacy(body, address) };
157
+ return { present: await atStoredVersion(client, api, fromResource(body, address)) };
158
+ }
159
+
160
+ /** Every dashboard in the organisation, paged. */
161
+ export async function listDashboards(client: GrafanaClient): Promise<LiveDashboard[]> {
162
+ const api = await dashboardApi(client);
163
+ const out: LiveDashboard[] = [];
164
+ if (api.kind === "apis") {
165
+ const base = `/apis/${DASHBOARD_GROUP}/${api.version}/namespaces/${encodeURIComponent(client.namespace)}/dashboards`;
166
+ let next: string | undefined;
167
+ do {
168
+ const page = await client.get<{ items?: unknown[]; metadata?: { continue?: string } }>(
169
+ `${base}?limit=500${next ? `&continue=${encodeURIComponent(next)}` : ""}`,
170
+ );
171
+ for (const item of page?.items ?? []) {
172
+ if (!isObject(item)) continue;
173
+ const d = fromResource(item, `${base}/${encodeURIComponent(String((item.metadata as Json | undefined)?.name ?? ""))}`);
174
+ if (d.uid) out.push(await atStoredVersion(client, api, d));
175
+ }
176
+ next = page?.metadata?.continue || undefined;
177
+ } while (next);
178
+ return out;
179
+ }
180
+ for (let page = 1; ; page++) {
181
+ const hits = (await client.get<Array<{ uid?: string }>>(`/api/search?type=dash-db&limit=1000&page=${page}`)) ?? [];
182
+ for (const hit of hits) {
183
+ if (typeof hit.uid !== "string") continue;
184
+ const read = await readDashboard(client, hit.uid);
185
+ if ("present" in read) out.push(read.present);
186
+ }
187
+ if (hits.length < 1000) break;
188
+ }
189
+ return out;
190
+ }
191
+
192
+ /** A v2 dashboard (a resource, or a bare v2 spec) as classic JSON with the uid, and what the classic form cannot hold. */
193
+ function fromV2(v2: Json, uid: string): { json: Json; warnings: string[] } | { unsupported: string } {
194
+ try {
195
+ const { dashboard, warnings } = readV2Dashboard(v2);
196
+ return { json: { ...dashboard, uid: uid || dashboard.uid }, warnings };
197
+ } catch (err) {
198
+ return { unsupported: err instanceof Error ? err.message : String(err) };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * The stored dashboard as classic JSON, with its uid, or why it cannot be
204
+ * read as one. The only place the dashboard schema version branches. A v2
205
+ * dashboard is converted by the importer's v2 reader; `warnings` then names
206
+ * what v2 holds that the classic JSON does not (tabs, auto grids,
207
+ * conditional rendering), and is absent for a classic dashboard.
208
+ */
209
+ export function classicDashboardOf(dashboard: LiveDashboard): { json: Json; warnings?: string[] } | { unsupported: string } {
210
+ if (dashboard.via === "legacy") {
211
+ const json = isObject(dashboard.raw.dashboard) ? dashboard.raw.dashboard : {};
212
+ if (looksLikeV2Dashboard(json)) return fromV2(json, dashboard.uid);
213
+ return { json };
214
+ }
215
+ if (looksLikeV2Dashboard(dashboard.raw)) return fromV2(dashboard.raw, dashboard.uid);
216
+ // The importer's own test for a v1 read of a v2-stored dashboard, so import
217
+ // and observe refuse the same lossy down-conversion. readDashboard has
218
+ // already tried the v2 read; this is a server that could not give it.
219
+ const lossy = lossyV1Read(dashboard.raw);
220
+ if (lossy?.failed) return { unsupported: `Grafana could not convert it from ${lossy.storedVersion || "the version it is stored at"} to the classic schema` };
221
+ if (lossy) {
222
+ return {
223
+ unsupported:
224
+ `it is stored as a ${lossy.storedVersion} dashboard and the server did not serve it at ${V2_DASHBOARD_VERSIONS.join(" or ")}; ` +
225
+ `its ${lossy.apiVersion} copy is a lossy down-conversion, so it is not read`,
226
+ };
227
+ }
228
+ const spec = isObject(dashboard.raw.spec) ? dashboard.raw.spec : undefined;
229
+ if (!spec) return { unsupported: "the resource has no spec" };
230
+ if (looksLikeV2Dashboard(spec)) return fromV2(spec, dashboard.uid);
231
+ // The resource's name is the dashboard's uid; the spec does not repeat it.
232
+ return { json: { ...spec, uid: dashboard.uid } };
233
+ }
234
+
235
+ /**
236
+ * The title of the folder a dashboard is in, or undefined for the General
237
+ * folder. One `GET /api/folders/<uid>` per folder per client.
238
+ */
239
+ export async function folderTitleOf(client: GrafanaClient, dashboard: LiveDashboard): Promise<string | undefined> {
240
+ if (!dashboard.folderUid) return undefined;
241
+ if (dashboard.folderTitle) return dashboard.folderTitle;
242
+ const uid = dashboard.folderUid;
243
+ return client.once(`folder:${uid}`, async () => {
244
+ const folder = await client.get<{ title?: string }>(`/api/folders/${encodeURIComponent(uid)}`);
245
+ return typeof folder?.title === "string" ? folder.title : undefined;
246
+ });
247
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Reading datasources (#2946), over `GET /api/datasources/uid/<uid>` and
3
+ * `GET /api/datasources`. Grafana 12.4 and 13.2 serve no general
4
+ * datasource resource under `/apis`, so this is the one datasource API both
5
+ * have, and it carries no labels or annotations: a datasource's ownership
6
+ * cannot be read (see ../ownership.ts).
7
+ *
8
+ * The API never returns secret values. `secureJsonFields` says which
9
+ * secrets are set, and that is all this reads of them.
10
+ */
11
+
12
+ import type { GrafanaClient } from "./client";
13
+
14
+ type Json = Record<string, unknown>;
15
+
16
+ export type DatasourceRead = { readonly present: Json; readonly address: string } | { readonly absent: true; readonly address: string };
17
+
18
+ export function datasourcePath(uid: string): string {
19
+ return `/api/datasources/uid/${encodeURIComponent(uid)}`;
20
+ }
21
+
22
+ /** Read one datasource by uid. Throws `GrafanaApiError` for anything but a 2xx or a 404. */
23
+ export async function readDatasource(client: GrafanaClient, uid: string): Promise<DatasourceRead> {
24
+ const address = datasourcePath(uid);
25
+ const body = await client.get<Json>(address);
26
+ return body === undefined ? { absent: true, address } : { present: body, address };
27
+ }
28
+
29
+ /** Every datasource in the organisation. */
30
+ export async function listDatasources(client: GrafanaClient): Promise<Json[]> {
31
+ const list = await client.get<unknown[]>("/api/datasources");
32
+ return (list ?? []).filter((d): d is Json => typeof d === "object" && d !== null && !Array.isArray(d));
33
+ }