@intentius/chant-lexicon-grafana 0.98.0 → 0.100.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 (532) hide show
  1. package/README.md +24 -8
  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/annotations.d.ts +61 -0
  11. package/dist/annotations.d.ts.map +1 -0
  12. package/dist/api/apply.d.ts +160 -0
  13. package/dist/api/apply.d.ts.map +1 -0
  14. package/dist/api/bind.d.ts +32 -0
  15. package/dist/api/bind.d.ts.map +1 -0
  16. package/dist/api/client.d.ts +76 -0
  17. package/dist/api/client.d.ts.map +1 -0
  18. package/dist/api/converge.d.ts +33 -0
  19. package/dist/api/converge.d.ts.map +1 -0
  20. package/dist/api/dashboards.d.ts +102 -0
  21. package/dist/api/dashboards.d.ts.map +1 -0
  22. package/dist/api/datasources.d.ts +26 -0
  23. package/dist/api/datasources.d.ts.map +1 -0
  24. package/dist/api/fake-grafana-writes.d.ts +41 -0
  25. package/dist/api/fake-grafana-writes.d.ts.map +1 -0
  26. package/dist/api/fake-grafana.d.ts +63 -0
  27. package/dist/api/fake-grafana.d.ts.map +1 -0
  28. package/dist/api/folders.d.ts +146 -0
  29. package/dist/api/folders.d.ts.map +1 -0
  30. package/dist/api/library-panels.d.ts +52 -0
  31. package/dist/api/library-panels.d.ts.map +1 -0
  32. package/dist/build.d.ts +55 -6
  33. package/dist/build.d.ts.map +1 -1
  34. package/dist/catalog.d.ts +1 -1
  35. package/dist/catalog.d.ts.map +1 -1
  36. package/dist/codegen/docs.d.ts.map +1 -1
  37. package/dist/codegen/generate.d.ts +7 -1
  38. package/dist/codegen/generate.d.ts.map +1 -1
  39. package/dist/codegen/schema-types.d.ts +3 -3
  40. package/dist/codegen/schema-types.d.ts.map +1 -1
  41. package/dist/composites/agent-dashboard.d.ts.map +1 -1
  42. package/dist/composites/catalog.d.ts.map +1 -1
  43. package/dist/composites/index.d.ts +4 -2
  44. package/dist/composites/index.d.ts.map +1 -1
  45. package/dist/composites/red-dashboard.d.ts +21 -2
  46. package/dist/composites/red-dashboard.d.ts.map +1 -1
  47. package/dist/composites/shared.d.ts +11 -3
  48. package/dist/composites/shared.d.ts.map +1 -1
  49. package/dist/composites/slo-alert-rules.d.ts +92 -0
  50. package/dist/composites/slo-alert-rules.d.ts.map +1 -0
  51. package/dist/config.d.ts +111 -0
  52. package/dist/config.d.ts.map +1 -0
  53. package/dist/contact-point-secrets.d.ts +17 -0
  54. package/dist/contact-point-secrets.d.ts.map +1 -0
  55. package/dist/dashboard.d.ts +51 -9
  56. package/dist/dashboard.d.ts.map +1 -1
  57. package/dist/datasource-refs.d.ts +99 -0
  58. package/dist/datasource-refs.d.ts.map +1 -0
  59. package/dist/datasource-settings.d.ts +834 -0
  60. package/dist/datasource-settings.d.ts.map +1 -0
  61. package/dist/datasource.d.ts +82 -11
  62. package/dist/datasource.d.ts.map +1 -1
  63. package/dist/deep-observe-hooks.d.ts +64 -0
  64. package/dist/deep-observe-hooks.d.ts.map +1 -0
  65. package/dist/deep-observe.d.ts +47 -0
  66. package/dist/deep-observe.d.ts.map +1 -0
  67. package/dist/describe-resources.d.ts +50 -0
  68. package/dist/describe-resources.d.ts.map +1 -0
  69. package/dist/detect.d.ts +32 -3
  70. package/dist/detect.d.ts.map +1 -1
  71. package/dist/export-resources.d.ts +38 -0
  72. package/dist/export-resources.d.ts.map +1 -0
  73. package/dist/folder.d.ts +68 -0
  74. package/dist/folder.d.ts.map +1 -0
  75. package/dist/import/alerting.d.ts +40 -0
  76. package/dist/import/alerting.d.ts.map +1 -0
  77. package/dist/import/edits.d.ts +47 -0
  78. package/dist/import/edits.d.ts.map +1 -0
  79. package/dist/import/embedded.d.ts +14 -0
  80. package/dist/import/embedded.d.ts.map +1 -0
  81. package/dist/import/generator.d.ts +60 -0
  82. package/dist/import/generator.d.ts.map +1 -0
  83. package/dist/import/live-export.d.ts +108 -0
  84. package/dist/import/live-export.d.ts.map +1 -0
  85. package/dist/import/mappings.d.ts +89 -0
  86. package/dist/import/mappings.d.ts.map +1 -0
  87. package/dist/import/model.d.ts +105 -0
  88. package/dist/import/model.d.ts.map +1 -0
  89. package/dist/import/normalize-alerting.d.ts +26 -0
  90. package/dist/import/normalize-alerting.d.ts.map +1 -0
  91. package/dist/import/normalize.d.ts +74 -0
  92. package/dist/import/normalize.d.ts.map +1 -0
  93. package/dist/import/parser.d.ts +125 -0
  94. package/dist/import/parser.d.ts.map +1 -0
  95. package/dist/import/report.d.ts +26 -0
  96. package/dist/import/report.d.ts.map +1 -0
  97. package/dist/import/testdata/fixtures.d.ts +41 -0
  98. package/dist/import/testdata/fixtures.d.ts.map +1 -0
  99. package/dist/import/v2.d.ts +73 -0
  100. package/dist/import/v2.d.ts.map +1 -0
  101. package/dist/index.d.ts +31 -12
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/init-templates.d.ts +27 -0
  104. package/dist/init-templates.d.ts.map +1 -0
  105. package/dist/integrity.json +22 -12
  106. package/dist/k8s.d.ts +107 -0
  107. package/dist/k8s.d.ts.map +1 -0
  108. package/dist/lint/audit-catalog.d.ts +2 -1
  109. package/dist/lint/audit-catalog.d.ts.map +1 -1
  110. package/dist/lint/post-synth/graf101.d.ts +2 -2
  111. package/dist/lint/post-synth/graf102.d.ts +1 -1
  112. package/dist/lint/post-synth/graf104.d.ts +2 -2
  113. package/dist/lint/post-synth/graf106.d.ts +2 -2
  114. package/dist/lint/post-synth/graf107.d.ts +1 -1
  115. package/dist/lint/post-synth/graf108.d.ts +8 -0
  116. package/dist/lint/post-synth/graf108.d.ts.map +1 -0
  117. package/dist/lint/post-synth/graf109.d.ts +8 -0
  118. package/dist/lint/post-synth/graf109.d.ts.map +1 -0
  119. package/dist/lint/post-synth/graf110.d.ts +8 -0
  120. package/dist/lint/post-synth/graf110.d.ts.map +1 -0
  121. package/dist/lint/post-synth/graf111.d.ts +8 -0
  122. package/dist/lint/post-synth/graf111.d.ts.map +1 -0
  123. package/dist/lint/post-synth/graf112.d.ts +8 -0
  124. package/dist/lint/post-synth/graf112.d.ts.map +1 -0
  125. package/dist/lint/post-synth/graf113.d.ts +8 -0
  126. package/dist/lint/post-synth/graf113.d.ts.map +1 -0
  127. package/dist/lint/post-synth/graf114.d.ts +8 -0
  128. package/dist/lint/post-synth/graf114.d.ts.map +1 -0
  129. package/dist/lint/post-synth/graf115.d.ts +8 -0
  130. package/dist/lint/post-synth/graf115.d.ts.map +1 -0
  131. package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
  132. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  133. package/dist/lint/post-synth/index.d.ts.map +1 -1
  134. package/dist/lint/rules/literal-secret.d.ts +7 -3
  135. package/dist/lint/rules/literal-secret.d.ts.map +1 -1
  136. package/dist/lsp/hover.d.ts.map +1 -1
  137. package/dist/manifest.json +1 -1
  138. package/dist/members.d.ts +27 -0
  139. package/dist/members.d.ts.map +1 -0
  140. package/dist/meta.json +205 -0
  141. package/dist/okf/index.md +53 -4
  142. package/dist/okf/rules/GRAF002.md +3 -2
  143. package/dist/okf/rules/GRAF101.md +3 -2
  144. package/dist/okf/rules/GRAF102.md +1 -0
  145. package/dist/okf/rules/GRAF104.md +2 -2
  146. package/dist/okf/rules/GRAF106.md +2 -2
  147. package/dist/okf/rules/GRAF108.md +11 -0
  148. package/dist/okf/rules/GRAF109.md +15 -0
  149. package/dist/okf/rules/GRAF110.md +11 -0
  150. package/dist/okf/rules/GRAF111.md +11 -0
  151. package/dist/okf/rules/GRAF112.md +16 -0
  152. package/dist/okf/rules/GRAF113.md +16 -0
  153. package/dist/okf/rules/GRAF114.md +11 -0
  154. package/dist/okf/rules/GRAF115.md +11 -0
  155. package/dist/okf/types/AdhocVariable.md +9 -0
  156. package/dist/okf/types/AlertListPanel.md +9 -0
  157. package/dist/okf/types/AlertQuery.md +9 -0
  158. package/dist/okf/types/AlertRule.md +9 -0
  159. package/dist/okf/types/AlertRuleGroup.md +9 -0
  160. package/dist/okf/types/AzureMonitorQuery.md +9 -0
  161. package/dist/okf/types/BarChartPanel.md +9 -0
  162. package/dist/okf/types/BarGaugePanel.md +9 -0
  163. package/dist/okf/types/BigQueryQuery.md +9 -0
  164. package/dist/okf/types/CanvasPanel.md +9 -0
  165. package/dist/okf/types/ClassicConditionsExpression.md +9 -0
  166. package/dist/okf/types/CloudMonitoringQuery.md +9 -0
  167. package/dist/okf/types/CloudWatchQuery.md +9 -0
  168. package/dist/okf/types/ContactPoint.md +14 -0
  169. package/dist/okf/types/Dashboard.md +2 -1
  170. package/dist/okf/types/Datasource.md +3 -2
  171. package/dist/okf/types/DatasourceProvisioning.md +9 -0
  172. package/dist/okf/types/ElasticsearchQuery.md +9 -0
  173. package/dist/okf/types/ExternalDatasource.md +15 -0
  174. package/dist/okf/types/FlameGraphPanel.md +9 -0
  175. package/dist/okf/types/Folder.md +9 -0
  176. package/dist/okf/types/GeomapPanel.md +9 -0
  177. package/dist/okf/types/GroupByVariable.md +9 -0
  178. package/dist/okf/types/HistogramPanel.md +9 -0
  179. package/dist/okf/types/MSSQLQuery.md +9 -0
  180. package/dist/okf/types/MathExpression.md +9 -0
  181. package/dist/okf/types/MuteTiming.md +13 -0
  182. package/dist/okf/types/MySQLQuery.md +9 -0
  183. package/dist/okf/types/NodeGraphPanel.md +9 -0
  184. package/dist/okf/types/NotificationPolicy.md +9 -0
  185. package/dist/okf/types/NotificationTemplate.md +9 -0
  186. package/dist/okf/types/PieChartPanel.md +9 -0
  187. package/dist/okf/types/PostgresQuery.md +9 -0
  188. package/dist/okf/types/PyroscopeQuery.md +9 -0
  189. package/dist/okf/types/ReduceExpression.md +9 -0
  190. package/dist/okf/types/ResampleExpression.md +9 -0
  191. package/dist/okf/types/SqlExpression.md +9 -0
  192. package/dist/okf/types/StateTimelinePanel.md +9 -0
  193. package/dist/okf/types/StatusHistoryPanel.md +9 -0
  194. package/dist/okf/types/SwitchVariable.md +9 -0
  195. package/dist/okf/types/ThresholdExpression.md +9 -0
  196. package/dist/okf/types/TrendPanel.md +9 -0
  197. package/dist/okf/types/XYChartPanel.md +9 -0
  198. package/dist/op/activities/grafana-apply.d.ts +86 -0
  199. package/dist/op/activities/grafana-apply.d.ts.map +1 -0
  200. package/dist/op/activities/index.d.ts +9 -0
  201. package/dist/op/activities/index.d.ts.map +1 -0
  202. package/dist/op/builders.d.ts +24 -0
  203. package/dist/op/builders.d.ts.map +1 -0
  204. package/dist/ownership.d.ts +78 -0
  205. package/dist/ownership.d.ts.map +1 -0
  206. package/dist/panel-options.d.ts +65 -0
  207. package/dist/panel-options.d.ts.map +1 -0
  208. package/dist/panels.d.ts +46 -9
  209. package/dist/panels.d.ts.map +1 -1
  210. package/dist/pin.d.ts +29 -7
  211. package/dist/pin.d.ts.map +1 -1
  212. package/dist/plugin.d.ts.map +1 -1
  213. package/dist/promql-check.d.ts +51 -0
  214. package/dist/promql-check.d.ts.map +1 -0
  215. package/dist/query-models.d.ts +95 -0
  216. package/dist/query-models.d.ts.map +1 -0
  217. package/dist/query.d.ts +116 -18
  218. package/dist/query.d.ts.map +1 -1
  219. package/dist/rules/graf101.ts +3 -3
  220. package/dist/rules/graf102.ts +1 -1
  221. package/dist/rules/graf104.ts +3 -3
  222. package/dist/rules/graf106.ts +3 -3
  223. package/dist/rules/graf107.ts +1 -1
  224. package/dist/rules/graf108.ts +17 -0
  225. package/dist/rules/graf109.ts +17 -0
  226. package/dist/rules/graf110.ts +17 -0
  227. package/dist/rules/graf111.ts +17 -0
  228. package/dist/rules/graf112.ts +17 -0
  229. package/dist/rules/graf113.ts +17 -0
  230. package/dist/rules/graf114.ts +17 -0
  231. package/dist/rules/graf115.ts +17 -0
  232. package/dist/rules/grafana-helpers.ts +27 -10
  233. package/dist/rules/literal-secret.ts +60 -6
  234. package/dist/schema/azuremonitor.gen.d.ts +354 -0
  235. package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
  236. package/dist/schema/barchart.gen.d.ts +198 -0
  237. package/dist/schema/barchart.gen.d.ts.map +1 -0
  238. package/dist/schema/bargauge.gen.d.ts +97 -0
  239. package/dist/schema/bargauge.gen.d.ts.map +1 -0
  240. package/dist/schema/bigquery.gen.d.ts +93 -0
  241. package/dist/schema/bigquery.gen.d.ts.map +1 -0
  242. package/dist/schema/canvas.gen.d.ts +152 -0
  243. package/dist/schema/canvas.gen.d.ts.map +1 -0
  244. package/dist/schema/cloudwatch.gen.d.ts +394 -0
  245. package/dist/schema/cloudwatch.gen.d.ts.map +1 -0
  246. package/dist/schema/dashboard.gen.d.ts +196 -21
  247. package/dist/schema/dashboard.gen.d.ts.map +1 -1
  248. package/dist/schema/elasticsearch.gen.d.ts +444 -0
  249. package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
  250. package/dist/schema/expr.gen.d.ts +606 -0
  251. package/dist/schema/expr.gen.d.ts.map +1 -0
  252. package/dist/schema/gauge.gen.d.ts.map +1 -1
  253. package/dist/schema/geomap.gen.d.ts +100 -0
  254. package/dist/schema/geomap.gen.d.ts.map +1 -0
  255. package/dist/schema/googlecloudmonitoring.gen.d.ts +312 -0
  256. package/dist/schema/googlecloudmonitoring.gen.d.ts.map +1 -0
  257. package/dist/schema/grafanapyroscope.gen.d.ts +60 -0
  258. package/dist/schema/grafanapyroscope.gen.d.ts.map +1 -0
  259. package/dist/schema/heatmap.gen.d.ts.map +1 -1
  260. package/dist/schema/histogram.gen.d.ts +132 -0
  261. package/dist/schema/histogram.gen.d.ts.map +1 -0
  262. package/dist/schema/index.d.ts +18 -0
  263. package/dist/schema/index.d.ts.map +1 -1
  264. package/dist/schema/logs.gen.d.ts +1 -0
  265. package/dist/schema/logs.gen.d.ts.map +1 -1
  266. package/dist/schema/loki.gen.d.ts.map +1 -1
  267. package/dist/schema/nodegraph.gen.d.ts +49 -0
  268. package/dist/schema/nodegraph.gen.d.ts.map +1 -0
  269. package/dist/schema/piechart.gen.d.ts +127 -0
  270. package/dist/schema/piechart.gen.d.ts.map +1 -0
  271. package/dist/schema/prometheus.gen.d.ts.map +1 -1
  272. package/dist/schema/stat.gen.d.ts.map +1 -1
  273. package/dist/schema/statetimeline.gen.d.ts +124 -0
  274. package/dist/schema/statetimeline.gen.d.ts.map +1 -0
  275. package/dist/schema/statushistory.gen.d.ts +114 -0
  276. package/dist/schema/statushistory.gen.d.ts.map +1 -0
  277. package/dist/schema/table.gen.d.ts +35 -3
  278. package/dist/schema/table.gen.d.ts.map +1 -1
  279. package/dist/schema/tempo.gen.d.ts.map +1 -1
  280. package/dist/schema/text.gen.d.ts.map +1 -1
  281. package/dist/schema/timeseries.gen.d.ts +5 -1
  282. package/dist/schema/timeseries.gen.d.ts.map +1 -1
  283. package/dist/schema/trend.gen.d.ts +180 -0
  284. package/dist/schema/trend.gen.d.ts.map +1 -0
  285. package/dist/schema/xychart.gen.d.ts +145 -0
  286. package/dist/schema/xychart.gen.d.ts.map +1 -0
  287. package/dist/schema-validate.d.ts +39 -4
  288. package/dist/schema-validate.d.ts.map +1 -1
  289. package/dist/serializer.d.ts +11 -3
  290. package/dist/serializer.d.ts.map +1 -1
  291. package/dist/skill-defs.d.ts.map +1 -1
  292. package/dist/skills/chant-grafana-alerting.md +49 -0
  293. package/dist/skills/chant-grafana-operations.md +108 -0
  294. package/dist/skills/chant-grafana-provisioning.md +3 -3
  295. package/dist/skills/chant-grafana.md +15 -8
  296. package/dist/spec/fetch-units-cli.d.ts +3 -0
  297. package/dist/spec/fetch-units-cli.d.ts.map +1 -0
  298. package/dist/spec/overlay.d.ts +43 -0
  299. package/dist/spec/overlay.d.ts.map +1 -0
  300. package/dist/spec/schemas.d.ts +21 -13
  301. package/dist/spec/schemas.d.ts.map +1 -1
  302. package/dist/spec/schemas.gen.d.ts +3 -0
  303. package/dist/spec/schemas.gen.d.ts.map +1 -0
  304. package/dist/spec/units.d.ts +35 -0
  305. package/dist/spec/units.d.ts.map +1 -0
  306. package/dist/spec/units.gen.d.ts +2 -0
  307. package/dist/spec/units.gen.d.ts.map +1 -0
  308. package/dist/transformations.d.ts +459 -0
  309. package/dist/transformations.d.ts.map +1 -0
  310. package/dist/util.d.ts +6 -0
  311. package/dist/util.d.ts.map +1 -1
  312. package/dist/validate-alerting.d.ts +65 -0
  313. package/dist/validate-alerting.d.ts.map +1 -0
  314. package/dist/validate-output.d.ts +62 -9
  315. package/dist/validate-output.d.ts.map +1 -1
  316. package/dist/validate.d.ts.map +1 -1
  317. package/dist/validation.d.ts +14 -0
  318. package/dist/validation.d.ts.map +1 -0
  319. package/dist/variables.d.ts +109 -7
  320. package/dist/variables.d.ts.map +1 -1
  321. package/package.json +12 -4
  322. package/src/actions/actions.test.ts +24 -0
  323. package/src/actions/grafana.ts +40 -0
  324. package/src/actions/index.ts +1 -0
  325. package/src/alerting-build.ts +376 -0
  326. package/src/alerting.e2e.test.ts +212 -0
  327. package/src/alerting.test.ts +272 -0
  328. package/src/alerting.ts +435 -0
  329. package/src/annotations.ts +84 -0
  330. package/src/api/apply.test.ts +256 -0
  331. package/src/api/apply.ts +394 -0
  332. package/src/api/bind.ts +58 -0
  333. package/src/api/client.ts +148 -0
  334. package/src/api/converge.ts +63 -0
  335. package/src/api/dashboards.ts +247 -0
  336. package/src/api/datasources.ts +33 -0
  337. package/src/api/fake-grafana-writes.ts +201 -0
  338. package/src/api/fake-grafana.ts +190 -0
  339. package/src/api/folders.ts +339 -0
  340. package/src/api/library-panels.ts +101 -0
  341. package/src/build.test.ts +351 -10
  342. package/src/build.ts +294 -55
  343. package/src/catalog.ts +104 -2
  344. package/src/codegen/docs.ts +50 -10
  345. package/src/codegen/generate.ts +27 -1
  346. package/src/codegen/schema-types.ts +16 -6
  347. package/src/composites/agent-dashboard.ts +6 -3
  348. package/src/composites/catalog.test.ts +2 -2
  349. package/src/composites/catalog.ts +82 -0
  350. package/src/composites/composites.test.ts +73 -9
  351. package/src/composites/index.ts +4 -2
  352. package/src/composites/queries.e2e.test.ts +374 -0
  353. package/src/composites/red-dashboard.ts +83 -12
  354. package/src/composites/shared.ts +14 -2
  355. package/src/composites/slo-alert-rules.ts +149 -0
  356. package/src/config.test.ts +61 -0
  357. package/src/config.ts +138 -0
  358. package/src/contact-point-secrets.ts +63 -0
  359. package/src/dashboard.ts +68 -9
  360. package/src/datasource-refs.test.ts +56 -0
  361. package/src/datasource-refs.ts +213 -0
  362. package/src/datasource-settings.test.ts +106 -0
  363. package/src/datasource-settings.ts +941 -0
  364. package/src/datasource.ts +123 -11
  365. package/src/deep-observe-hooks.ts +267 -0
  366. package/src/deep-observe.test.ts +311 -0
  367. package/src/deep-observe.ts +187 -0
  368. package/src/describe-resources.test.ts +271 -0
  369. package/src/describe-resources.ts +244 -0
  370. package/src/detect.ts +82 -5
  371. package/src/export-resources.test.ts +129 -0
  372. package/src/export-resources.ts +84 -0
  373. package/src/folder.ts +118 -0
  374. package/src/generated/lexicon-grafana.json +205 -0
  375. package/src/import/alerting-roundtrip.test.ts +161 -0
  376. package/src/import/alerting.ts +500 -0
  377. package/src/import/edits.ts +95 -0
  378. package/src/import/embedded.test.ts +45 -0
  379. package/src/import/embedded.ts +47 -0
  380. package/src/import/generated-types.e2e.test.ts +130 -0
  381. package/src/import/generator.test.ts +210 -0
  382. package/src/import/generator.ts +463 -0
  383. package/src/import/live-export.ts +264 -0
  384. package/src/import/mappings.ts +336 -0
  385. package/src/import/model.ts +117 -0
  386. package/src/import/normalize-alerting.ts +101 -0
  387. package/src/import/normalize.test.ts +117 -0
  388. package/src/import/normalize.ts +393 -0
  389. package/src/import/parser.test.ts +516 -0
  390. package/src/import/parser.ts +1387 -0
  391. package/src/import/report.ts +62 -0
  392. package/src/import/roundtrip.test.ts +563 -0
  393. package/src/import/testdata/fixtures.ts +137 -0
  394. package/src/import/v2.test.ts +292 -0
  395. package/src/import/v2.ts +804 -0
  396. package/src/import.e2e.test.ts +31 -84
  397. package/src/import.test.ts +9 -3
  398. package/src/index.ts +308 -14
  399. package/src/init-templates.test.ts +74 -0
  400. package/src/init-templates.ts +349 -0
  401. package/src/k8s.test.ts +98 -0
  402. package/src/k8s.ts +197 -0
  403. package/src/lint/audit-catalog.ts +68 -3
  404. package/src/lint/core-lint.test.ts +43 -0
  405. package/src/lint/post-synth/graf101.ts +3 -3
  406. package/src/lint/post-synth/graf102.ts +1 -1
  407. package/src/lint/post-synth/graf104.ts +3 -3
  408. package/src/lint/post-synth/graf106.ts +3 -3
  409. package/src/lint/post-synth/graf107.ts +1 -1
  410. package/src/lint/post-synth/graf108.ts +17 -0
  411. package/src/lint/post-synth/graf109.ts +17 -0
  412. package/src/lint/post-synth/graf110.ts +17 -0
  413. package/src/lint/post-synth/graf111.ts +17 -0
  414. package/src/lint/post-synth/graf112.ts +17 -0
  415. package/src/lint/post-synth/graf113.ts +17 -0
  416. package/src/lint/post-synth/graf114.ts +17 -0
  417. package/src/lint/post-synth/graf115.ts +17 -0
  418. package/src/lint/post-synth/grafana-helpers.ts +27 -10
  419. package/src/lint/post-synth/index.ts +16 -0
  420. package/src/lint/post-synth/post-synth.test.ts +333 -12
  421. package/src/lint/rules/literal-secret.ts +60 -6
  422. package/src/lint/rules/rules.test.ts +27 -0
  423. package/src/load-cost.test.ts +35 -0
  424. package/src/lsp/completions.test.ts +5 -0
  425. package/src/lsp/hover.test.ts +7 -0
  426. package/src/lsp/hover.ts +8 -1
  427. package/src/members.ts +68 -0
  428. package/src/observe.e2e.test.ts +255 -0
  429. package/src/op/activities/grafana-apply.e2e.test.ts +184 -0
  430. package/src/op/activities/grafana-apply.test.ts +243 -0
  431. package/src/op/activities/grafana-apply.ts +197 -0
  432. package/src/op/activities/index.ts +8 -0
  433. package/src/op/builders.test.ts +40 -0
  434. package/src/op/builders.ts +34 -0
  435. package/src/ownership.ts +122 -0
  436. package/src/panel-options.ts +66 -0
  437. package/src/panels.test.ts +53 -0
  438. package/src/panels.ts +148 -10
  439. package/src/pin.ts +69 -6
  440. package/src/plugin.test.ts +12 -2
  441. package/src/plugin.ts +75 -0
  442. package/src/promql-check.test.ts +108 -0
  443. package/src/promql-check.ts +193 -0
  444. package/src/query-models.ts +94 -0
  445. package/src/query.test.ts +166 -0
  446. package/src/query.ts +190 -20
  447. package/src/schema/azuremonitor.gen.ts +381 -0
  448. package/src/schema/barchart.gen.ts +221 -0
  449. package/src/schema/bargauge.gen.ts +111 -0
  450. package/src/schema/bigquery.gen.ts +110 -0
  451. package/src/schema/canvas.gen.ts +176 -0
  452. package/src/schema/cloudwatch.gen.ts +423 -0
  453. package/src/schema/dashboard.gen.ts +206 -22
  454. package/src/schema/elasticsearch.gen.ts +508 -0
  455. package/src/schema/expr.gen.ts +618 -0
  456. package/src/schema/gauge.gen.ts +2 -1
  457. package/src/schema/geomap.gen.ts +112 -0
  458. package/src/schema/googlecloudmonitoring.gen.ts +330 -0
  459. package/src/schema/grafanapyroscope.gen.ts +66 -0
  460. package/src/schema/heatmap.gen.ts +2 -1
  461. package/src/schema/histogram.gen.ts +151 -0
  462. package/src/schema/index.ts +18 -0
  463. package/src/schema/logs.gen.ts +3 -1
  464. package/src/schema/loki.gen.ts +2 -1
  465. package/src/schema/nodegraph.gen.ts +58 -0
  466. package/src/schema/piechart.gen.ts +146 -0
  467. package/src/schema/prometheus.gen.ts +2 -1
  468. package/src/schema/stat.gen.ts +2 -1
  469. package/src/schema/statetimeline.gen.ts +143 -0
  470. package/src/schema/statushistory.gen.ts +132 -0
  471. package/src/schema/table.gen.ts +41 -4
  472. package/src/schema/tempo.gen.ts +2 -1
  473. package/src/schema/text.gen.ts +2 -1
  474. package/src/schema/timeseries.gen.ts +7 -2
  475. package/src/schema/trend.gen.ts +208 -0
  476. package/src/schema/xychart.gen.ts +167 -0
  477. package/src/schema-overlay.test.ts +282 -0
  478. package/src/schema-validate.ts +199 -57
  479. package/src/schema.test.ts +4 -2
  480. package/src/serializer.test.ts +13 -1
  481. package/src/serializer.ts +11 -3
  482. package/src/skill-defs.ts +34 -0
  483. package/src/skills/chant-grafana-alerting.md +49 -0
  484. package/src/skills/chant-grafana-operations.md +108 -0
  485. package/src/skills/chant-grafana-provisioning.md +3 -3
  486. package/src/skills/chant-grafana.md +15 -8
  487. package/src/spec/fetch-cli.ts +3 -3
  488. package/src/spec/fetch-units-cli.ts +37 -0
  489. package/src/spec/overlay/canvas.overlay.json +142 -0
  490. package/src/spec/overlay/dashboard.overlay.json +378 -0
  491. package/src/spec/overlay/expr.overlay.json +14 -0
  492. package/src/spec/overlay/geomap.overlay.json +35 -0
  493. package/src/spec/overlay/logs.overlay.json +14 -0
  494. package/src/spec/overlay/nodegraph.overlay.json +31 -0
  495. package/src/spec/overlay/piechart.overlay.json +17 -0
  496. package/src/spec/overlay/table.overlay.json +103 -0
  497. package/src/spec/overlay/timeseries.overlay.json +21 -0
  498. package/src/spec/overlay/trend.overlay.json +17 -0
  499. package/src/spec/overlay/xychart.overlay.json +16 -0
  500. package/src/spec/overlay.ts +105 -0
  501. package/src/spec/schemas/azuremonitor.jsonschema.json +679 -0
  502. package/src/spec/schemas/barchart.jsonschema.json +419 -0
  503. package/src/spec/schemas/bargauge.jsonschema.json +224 -0
  504. package/src/spec/schemas/bigquery.jsonschema.json +290 -0
  505. package/src/spec/schemas/canvas.jsonschema.json +356 -0
  506. package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
  507. package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
  508. package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
  509. package/src/spec/schemas/expr.jsonschema.json +987 -0
  510. package/src/spec/schemas/geomap.jsonschema.json +220 -0
  511. package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
  512. package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -0
  513. package/src/spec/schemas/histogram.jsonschema.json +308 -0
  514. package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
  515. package/src/spec/schemas/piechart.jsonschema.json +275 -0
  516. package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
  517. package/src/spec/schemas/statushistory.jsonschema.json +292 -0
  518. package/src/spec/schemas/trend.jsonschema.json +449 -0
  519. package/src/spec/schemas/xychart.jsonschema.json +428 -0
  520. package/src/spec/schemas.gen.ts +40 -0
  521. package/src/spec/schemas.ts +30 -16
  522. package/src/spec/units.gen.ts +281 -0
  523. package/src/spec/units.ts +77 -0
  524. package/src/transformations.test.ts +65 -0
  525. package/src/transformations.ts +574 -0
  526. package/src/util.ts +8 -0
  527. package/src/validate-alerting.test.ts +255 -0
  528. package/src/validate-alerting.ts +430 -0
  529. package/src/validate-output.ts +427 -104
  530. package/src/validate.ts +28 -0
  531. package/src/validation.ts +27 -0
  532. package/src/variables.ts +120 -8
@@ -7,6 +7,14 @@ import { graf104 } from "./graf104";
7
7
  import { graf105 } from "./graf105";
8
8
  import { graf106 } from "./graf106";
9
9
  import { graf107 } from "./graf107";
10
+ import { graf108 } from "./graf108";
11
+ import { graf109 } from "./graf109";
12
+ import { graf110 } from "./graf110";
13
+ import { graf111 } from "./graf111";
14
+ import { graf112 } from "./graf112";
15
+ import { graf113 } from "./graf113";
16
+ import { graf114 } from "./graf114";
17
+ import { graf115 } from "./graf115";
10
18
 
11
19
  export const postSynthChecks: PostSynthCheck[] = [
12
20
  graf101,
@@ -16,4 +24,12 @@ export const postSynthChecks: PostSynthCheck[] = [
16
24
  graf105,
17
25
  graf106,
18
26
  graf107,
27
+ graf108,
28
+ graf109,
29
+ graf110,
30
+ graf111,
31
+ graf112,
32
+ graf113,
33
+ graf114,
34
+ graf115,
19
35
  ];
@@ -1,13 +1,17 @@
1
1
  import { describe, expect, test } from "vitest";
2
+ import { readdirSync, readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { fileURLToPath } from "url";
2
5
  import { makePostSynthCtxFromFiles, makePostSynthCtx } from "@intentius/chant-test-utils";
3
6
  import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
4
7
  import type { Declarable } from "@intentius/chant/declarable";
5
8
  import { grafanaSerializer } from "../../serializer";
6
- import { Datasource } from "../../datasource";
7
- import { Dashboard } from "../../dashboard";
8
- import { Row, StatPanel, TimeSeriesPanel, TablePanel } from "../../panels";
9
- import { PromQuery, TempoQuery } from "../../query";
10
- import { DatasourceVariable, QueryVariable } from "../../variables";
9
+ import { Datasource, DatasourceProvisioning, ExternalDatasource } from "../../datasource";
10
+ import { Folder } from "../../folder";
11
+ import { Dashboard, DashboardProvider } from "../../dashboard";
12
+ import { HeatmapPanel, Row, StatPanel, TimeSeriesPanel, TablePanel, TextPanel } from "../../panels";
13
+ import { LokiQuery, PromQuery, TempoQuery } from "../../query";
14
+ import { AdhocVariable, CustomVariable, DatasourceVariable, GroupByVariable, IntervalVariable, QueryVariable, SwitchVariable } from "../../variables";
11
15
  import { graf101 } from "./graf101";
12
16
  import { graf102 } from "./graf102";
13
17
  import { graf103 } from "./graf103";
@@ -15,6 +19,15 @@ import { graf104 } from "./graf104";
15
19
  import { graf105 } from "./graf105";
16
20
  import { graf106 } from "./graf106";
17
21
  import { graf107 } from "./graf107";
22
+ import { graf108 } from "./graf108";
23
+ import { graf109 } from "./graf109";
24
+ import { graf110 } from "./graf110";
25
+ import { graf115 } from "./graf115";
26
+ import { panelUnits } from "../../validate-output";
27
+ import { panelsOf } from "../../datasource-refs";
28
+ import { GRAFANA_UNIT_IDS } from "../../spec/units";
29
+ import { prometheusQueries } from "../../promql-check";
30
+ import { knownDatasources } from "../../datasource-refs";
18
31
 
19
32
  /** Serialize entities the way a build does and hand the files to the harness. */
20
33
  function ctxOf(entities: Record<string, Declarable>) {
@@ -72,10 +85,65 @@ describe("GRAF101: undeclared datasource", () => {
72
85
  expect(graf101.check(ctxOf({ prometheus, tempo, d }))).toEqual([]);
73
86
  });
74
87
 
75
- test("is silent when the build declares no datasource (another build root, chant #1939)", () => {
88
+ test("warns once per dashboard when the build declares no datasource, so nothing can be checked", () => {
76
89
  const panel = new StatPanel({ datasource: { type: "prometheus", uid: "elsewhere" }, targets: [up] });
90
+ const other = new StatPanel({ datasource: { type: "prometheus", uid: "elsewhere" }, targets: [up] });
91
+ const diags = graf101.check(ctxOf({ d: new Dashboard({ title: "D", panels: [panel, other] }) }));
92
+ expect(diags.map((d) => [d.checkId, d.severity])).toEqual([["GRAF101", "warning"]]);
93
+ expect(diags[0].message).toContain('datasource uid "elsewhere"');
94
+ expect(diags[0].message).toContain("ExternalDatasource");
95
+ });
96
+
97
+ test("says nothing when the build declares no datasource and the dashboard names none", () => {
98
+ const panel = new TextPanel({ options: { content: "hi", mode: "markdown" } });
77
99
  expect(graf101.check(ctxOf({ d: new Dashboard({ title: "D", panels: [panel] }) }))).toEqual([]);
78
100
  });
101
+
102
+ test("resolves entity and { type, uid } refs to an ExternalDatasource, and never provisions it", () => {
103
+ const mimir = new ExternalDatasource({ type: "prometheus", uid: "mimir", name: "Mimir" });
104
+ const d = new Dashboard({
105
+ title: "D",
106
+ panels: [new StatPanel({ datasource: mimir, targets: [up] }), new StatPanel({ datasource: { type: "prometheus", uid: "mimir" }, targets: [up] })],
107
+ variables: [new QueryVariable({ name: "job", datasource: mimir, query: "label_values(job)" })],
108
+ });
109
+ const entities = { mimir, prometheus, d };
110
+ expect(graf101.check(ctxOf(entities))).toEqual([]);
111
+ expect(graf102.check(ctxOf(entities))).toEqual([]);
112
+ const out = grafanaSerializer.serialize(new Map(Object.entries(entities))) as { primary: string; files: Record<string, string> };
113
+ expect(out.files["provisioning/datasources/chant.yaml"]).not.toContain("mimir");
114
+ expect(JSON.parse(out.primary).externalDatasources).toEqual([{ type: "prometheus", uid: "mimir", name: "Mimir" }]);
115
+ });
116
+
117
+ test("an ExternalDatasource alone is enough to check references", () => {
118
+ const mimir = new ExternalDatasource({ type: "prometheus", uid: "mimir" });
119
+ const d = new Dashboard({ title: "D", panels: [new StatPanel({ datasource: { type: "prometheus", uid: "typo" }, targets: [up] })] });
120
+ const diags = graf101.check(ctxOf({ mimir, d }));
121
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual([
122
+ ["GRAF101", "error"],
123
+ ["GRAF101", "error"],
124
+ ]);
125
+ expect(diags[0].message).toContain('Declared: "mimir"');
126
+ });
127
+
128
+ test("flags a DatasourceVariable whose plugin type no declared datasource has", () => {
129
+ const ds = new DatasourceVariable({ name: "logs", pluginType: "loki" });
130
+ const diags = graf101.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds] }) }));
131
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual([["GRAF101", "error"]]);
132
+ expect(diags[0].message).toContain('variable "logs" chooses among loki datasources');
133
+ });
134
+
135
+ test("passes a DatasourceVariable whose plugin type only an ExternalDatasource has", () => {
136
+ const ds = new DatasourceVariable({ name: "logs", pluginType: "loki" });
137
+ const loki = new ExternalDatasource({ type: "loki", uid: "loki-prod" });
138
+ expect(graf101.check(ctxOf({ prometheus, loki, d: new Dashboard({ title: "D", variables: [ds] }) }))).toEqual([]);
139
+ });
140
+
141
+ test("counts a DatasourceVariable in the no-datasource warning", () => {
142
+ const ds = new DatasourceVariable({ name: "logs", pluginType: "loki" });
143
+ const diags = graf101.check(ctxOf({ d: new Dashboard({ title: "D", variables: [ds] }) }));
144
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual([["GRAF101", "warning"]]);
145
+ expect(diags[0].message).toContain("datasource variables of type loki");
146
+ });
79
147
  });
80
148
 
81
149
  describe("GRAF102: datasource type mismatch", () => {
@@ -95,6 +163,13 @@ describe("GRAF102: datasource type mismatch", () => {
95
163
  expect(diags[0].message).toContain('variable "ds" chooses among loki');
96
164
  });
97
165
 
166
+ test("checks a ref's type against an ExternalDatasource", () => {
167
+ const traces = new ExternalDatasource({ type: "tempo", uid: "traces", name: "Traces" });
168
+ const panel = new StatPanel({ datasource: { type: "prometheus", uid: "traces" }, targets: [up] });
169
+ const diags = graf102.check(ctxOf({ traces, d: new Dashboard({ title: "D", panels: [panel] }) }));
170
+ expect(diags[0].message).toContain('external datasource "Traces", which is tempo');
171
+ });
172
+
98
173
  test("passes matching types", () => {
99
174
  expect(graf102.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels: [new StatPanel({ datasource: prometheus, targets: [up] })] }) }))).toEqual([]);
100
175
  });
@@ -121,6 +196,18 @@ describe("GRAF103: undeclared variables", () => {
121
196
  });
122
197
  });
123
198
 
199
+ describe("GRAF101, GRAF103 on ad hoc and group by variables", () => {
200
+ test("checks an ad hoc or group by variable's datasource like a query variable's", () => {
201
+ const d = dashboardJson([], [
202
+ { type: "adhoc", name: "f", datasource: { type: "prometheus", uid: "gone" }, filters: [] },
203
+ { type: "groupby", name: "by", datasource: { type: "prometheus", uid: "${nope}" } },
204
+ ]);
205
+ const ctx = ctxOfJson(d, [{ name: "Prometheus", type: "prometheus", uid: "prom" }]);
206
+ expect(graf101.check(ctx).map((x) => x.message)).toEqual([expect.stringContaining('variable "f" uses datasource uid "gone"')]);
207
+ expect(graf103.check(ctx).map((x) => x.message)).toEqual([expect.stringContaining('variable "by" datasource uses $nope')]);
208
+ });
209
+ });
210
+
124
211
  describe("GRAF104: duplicates", () => {
125
212
  test("flags duplicate dashboard uids, datasource names and panel ids", () => {
126
213
  const again = new Datasource({ name: "Prometheus", type: "prometheus", uid: "prom-2" });
@@ -180,6 +267,18 @@ describe("GRAF105: grid", () => {
180
267
  });
181
268
  });
182
269
 
270
+ describe("GRAF104 and GRAF106 on external datasources", () => {
271
+ test("flags an ExternalDatasource sharing a uid with a provisioned one", () => {
272
+ const clash = new ExternalDatasource({ type: "prometheus", uid: "prometheus" });
273
+ expect(graf104.check(ctxOf({ prometheus, clash })).map((d) => d.message)).toEqual([expect.stringContaining('2 datasources share the uid "prometheus"')]);
274
+ });
275
+
276
+ test("flags an ExternalDatasource uid Grafana rejects", () => {
277
+ const bad = new ExternalDatasource({ type: "prometheus", uid: "has space" });
278
+ expect(graf106.check(ctxOf({ bad })).map((d) => d.message)).toEqual([expect.stringContaining('ExternalDatasource "has space"')]);
279
+ });
280
+ });
281
+
183
282
  describe("GRAF106: uids and titles", () => {
184
283
  test("flags an over-long uid, a bad datasource uid and an empty title", () => {
185
284
  const bad = new Datasource({ name: "Bad", type: "prometheus", uid: "has space" });
@@ -197,17 +296,17 @@ describe("GRAF106: uids and titles", () => {
197
296
  });
198
297
 
199
298
  describe("GRAF107: the pinned schema", () => {
200
- test("flags an unknown dashboard key, a bad panel option and an unknown query field", () => {
299
+ test("warns about an unknown dashboard key and query field, and fails a bad panel option", () => {
201
300
  const dash = dashboardJson(
202
301
  [panelJson({ options: { graphMode: "sparkline" }, datasource: { type: "prometheus", uid: "p" }, targets: [{ refId: "A", expr: "up", exprr: "typo" }] })],
203
302
  [],
204
303
  { owner: "team-a" },
205
304
  );
206
- const messages = graf107.check(ctxOfJson(dash)).map((d) => d.message);
207
- expect(messages).toEqual([
208
- expect.stringContaining('must NOT have additional properties ("owner")'),
209
- expect.stringContaining("/panels/0/options/graphMode: must be equal to one of the allowed values"),
210
- expect.stringContaining('/panels/0/targets/0: must NOT have additional properties ("exprr")'),
305
+ const diags = graf107.check(ctxOfJson(dash)).map((d) => [d.severity, d.message]);
306
+ expect(diags).toEqual([
307
+ ["warning", expect.stringContaining('/: unknown key "owner" (not in the pinned schema)')],
308
+ ["error", expect.stringContaining("/panels/0/options/graphMode: must be equal to one of the allowed values")],
309
+ ["warning", expect.stringContaining('/panels/0/targets/0: unknown key "exprr" (not in the pinned schema)')],
211
310
  ]);
212
311
  });
213
312
 
@@ -229,3 +328,225 @@ describe("GRAF107: the pinned schema", () => {
229
328
  expect(graf107.check(ctx).map((d) => d.checkId)).toEqual(["GRAF107"]);
230
329
  });
231
330
  });
331
+
332
+ describe("GRAF108: PromQL syntax", () => {
333
+ const loki = new Datasource({ name: "Loki", type: "loki" });
334
+
335
+ test("flags the unbalanced expression from the issue (#2955)", () => {
336
+ const cluster = new QueryVariable({ name: "cluster", datasource: prometheus, query: "label_values(up, cluster)" });
337
+ const panel = new StatPanel({ title: "Up", datasource: prometheus, targets: [new PromQuery({ expr: 'sum(x{cluster="$cluster"}' })] });
338
+ const diags = graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [cluster], panels: [panel] }) }));
339
+ expect(diags.map((d) => [d.checkId, d.severity])).toEqual([["GRAF108", "error"]]);
340
+ expect(diags[0].message).toContain('panel "Up"');
341
+ expect(diags[0].message).toContain("query A is not valid PromQL: the expression ends early");
342
+ });
343
+
344
+ test("flags a query variable and a panel query inheriting a Prometheus datasource variable", () => {
345
+ const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
346
+ const job = new QueryVariable({ name: "job", datasource: prometheus, query: 'label_values(up{env="prod", job, instance)' });
347
+ const panel = new TimeSeriesPanel({ title: "Rate", datasource: ds, targets: [new PromQuery({ expr: "rate(http_requests_total[5 m])" })] });
348
+ const messages = graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds, job], panels: [panel] }) })).map((d) => d.message);
349
+ expect(messages).toEqual([expect.stringContaining('panel "Rate" (id 1) query A is not valid PromQL'), expect.stringContaining('variable "job" query is not valid PromQL')]);
350
+ });
351
+
352
+ test("passes template variables and Grafana's macros where Prometheus would see a value", () => {
353
+ const route = new QueryVariable({ name: "route", datasource: prometheus, query: 'label_values(http_requests_total{job="$job"}, route)' });
354
+ const job = new QueryVariable({ name: "job", datasource: prometheus, query: "label_values(up, job)" });
355
+ const exprs = [
356
+ 'sum by (le) (rate(http_request_duration_seconds_bucket{route=~"$route"}[$__rate_interval]))',
357
+ 'sum(increase(http_requests_total{job="${job}"}[$__range])) / $__range_s',
358
+ 'rate(http_requests_total{job=~"[[job]]"}[$__interval]) offset $__interval',
359
+ ];
360
+ const panel = new TimeSeriesPanel({ datasource: prometheus, targets: exprs.map((expr) => new PromQuery({ expr })) });
361
+ expect(graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [job, route], panels: [panel] }) }))).toEqual([]);
362
+ });
363
+
364
+ test("leaves LogQL, TraceQL and queries of unknown destination alone", () => {
365
+ const logs = new StatPanel({ datasource: loki, targets: [new LokiQuery({ expr: '{app="x"} |= "error" | json' })] });
366
+ const traces = new TablePanel({ datasource: tempo, targets: [new TempoQuery({ query: "{ .status = error }" })] });
367
+ const d = new Dashboard({ title: "D", panels: [logs, traces] });
368
+ expect(graf108.check(ctxOf({ loki, tempo, d }))).toEqual([]);
369
+ // No datasource anywhere: Grafana sends it to its default, whatever that is.
370
+ const unknown = dashboardJson([panelJson({ targets: [{ refId: "A", expr: "sum(" }] })]);
371
+ expect(graf108.check(ctxOfJson(unknown))).toEqual([]);
372
+ });
373
+
374
+ test("checks refs to a Prometheus the build does not declare, by the ref's own type", () => {
375
+ const dash = dashboardJson([panelJson({ datasource: { type: "prometheus", uid: "elsewhere" }, targets: [{ refId: "A", expr: "sum(up" }] })]);
376
+ expect(ids(graf108, ctxOfJson(dash))).toEqual([["GRAF108", "error"]]);
377
+ });
378
+ });
379
+
380
+ describe("GRAF110: repeats", () => {
381
+ test("flags a repeat over a variable Grafana cannot repeat over, and over one that holds one value", () => {
382
+ const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
383
+ const w = new IntervalVariable({ name: "w", values: ["1m", "5m"] });
384
+ const on = new SwitchVariable({ name: "on" });
385
+ const f = new AdhocVariable({ name: "f", datasource: ds });
386
+ const env = new CustomVariable({ name: "env", values: ["a", "b"] });
387
+ const panels = [new StatPanel({ title: "by w", repeat: w }), new StatPanel({ title: "by on", repeat: on }), new StatPanel({ title: "by f", repeat: f }), new StatPanel({ title: "by env", repeat: env })];
388
+ const diags = graf110.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds, w, on, f, env], panels }) }));
389
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual(Array(4).fill(["GRAF110", "warning"]));
390
+ expect(diags.map((x) => x.message)).toEqual([
391
+ expect.stringContaining('panel "by w" (id 1) repeats over $w, an interval variable, which Grafana cannot repeat over'),
392
+ expect.stringContaining("repeats over $on, a switch variable"),
393
+ expect.stringContaining("repeats over $f, an ad hoc variable"),
394
+ expect.stringContaining("repeats over $env, a custom variable with neither multi nor includeAll"),
395
+ ]);
396
+ });
397
+
398
+ test("checks a row's repeat too", () => {
399
+ const env = new CustomVariable({ name: "env", values: ["a", "b"] });
400
+ const row = new Row({ title: "Per env", repeat: env, panels: [new StatPanel({ title: "s" })] });
401
+ expect(graf110.check(ctxOf({ d: new Dashboard({ title: "D", variables: [env], panels: [row] }) })).map((x) => x.message)).toEqual([
402
+ expect.stringContaining('row "Per env" repeats over $env'),
403
+ ]);
404
+ });
405
+
406
+ test("passes a multi-value or include-all variable, a group by variable, and leaves an undeclared one to GRAF103", () => {
407
+ const env = new CustomVariable({ name: "env", values: ["a", "b"], multi: true });
408
+ const job = new QueryVariable({ name: "job", datasource: prometheus, query: { query: "label_values(up, job)", qryType: 1 }, includeAll: true });
409
+ const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus", multi: true });
410
+ const by = new GroupByVariable({ name: "by", datasource: prometheus });
411
+ const panels = [env, job, ds, by].map((v) => new StatPanel({ title: v.variableName, repeat: v }));
412
+ panels.push(new StatPanel({ title: "gone", repeat: "gone" }));
413
+ expect(graf110.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [env, job, ds, by], panels }) }))).toEqual([]);
414
+ });
415
+ });
416
+
417
+ describe("GRAF108 on object-form variable queries", () => {
418
+ test("parses the query text of an object query sent to Prometheus", () => {
419
+ const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
420
+ const bad = new QueryVariable({ name: "bad", datasource: ds, query: { qryType: 1, query: 'label_values(up{job="x", instance)', refId: "PrometheusVariableQueryEditor-VariableQuery" } });
421
+ const good = new QueryVariable({ name: "good", datasource: ds, query: { qryType: 4, query: 'kube_pod_info{namespace=~"$ns"}' } });
422
+ const ns = new CustomVariable({ name: "ns", values: ["a"] });
423
+ const diags = graf108.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", variables: [ds, ns, bad, good] }) }));
424
+ expect(diags.map((x) => x.message)).toEqual([expect.stringContaining('variable "bad" query is not valid PromQL')]);
425
+ });
426
+ });
427
+
428
+ describe("GRAF108 over real Grafana exports", () => {
429
+ const exportsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "test", "fixtures", "exports");
430
+ const files = readdirSync(exportsDir)
431
+ .filter((d) => d.startsWith("grafana-"))
432
+ .flatMap((v) => readdirSync(join(exportsDir, v)).filter((f) => f.endsWith(".json") && !f.includes("-resource")).map((f) => `${v}/${f}`));
433
+
434
+ test.each(files)("%s has no GRAF108 errors", (name) => {
435
+ const text = readFileSync(join(exportsDir, name), "utf-8");
436
+ // Non-vacuous: every export in the corpus sends PromQL to a Prometheus.
437
+ expect(prometheusQueries(JSON.parse(text), knownDatasources([])).length).toBeGreaterThan(0);
438
+ expect(graf108.check(makePostSynthCtxFromFiles("grafana", { [name]: text }, "{}"))).toEqual([]);
439
+ });
440
+ });
441
+
442
+ describe("GRAF115: units", () => {
443
+ test("flags the units from the issue (#2954) and suggests the id meant", () => {
444
+ const bytes = new StatPanel({ title: "Memory", datasource: prometheus, targets: [up], fieldConfig: { defaults: { unit: "byte" } } });
445
+ const cores = new TimeSeriesPanel({ title: "CPU", datasource: prometheus, targets: [up], fieldConfig: { defaults: { unit: "cores" } } });
446
+ const diags = graf115.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels: [bytes, cores] }) }));
447
+ expect(diags.map((d) => [d.checkId, d.severity])).toEqual([
448
+ ["GRAF115", "warning"],
449
+ ["GRAF115", "warning"],
450
+ ]);
451
+ expect(diags[0].message).toContain('panel "Memory" (id 1) fieldConfig.defaults.unit: unit "byte" is not one Grafana knows');
452
+ expect(diags[0].message).toContain('Did you mean "bytes"?');
453
+ expect(diags[1].message).not.toContain("Did you mean");
454
+ expect(diags[1].message).toContain('"suffix:cores"');
455
+ });
456
+
457
+ test("passes registered ids, the legacy alias, an empty unit and every custom-unit syntax", () => {
458
+ const units = ["bytes", "s", "percentunit", "reqps", "currencyEUR", "farenheit", "", "suffix: cores", "prefix:$", "time:YYYY-MM-DD", "si:mF", "count:reqs", "currency:financial:€:suffix", "bool:up/down"];
459
+ const panels = units.map((unit) => new StatPanel({ datasource: prometheus, targets: [up], fieldConfig: { defaults: { unit } } }));
460
+ expect(graf115.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels }) }))).toEqual([]);
461
+ });
462
+
463
+ test("checks overrides, heatmap axes and cells, legacy y-axes and library panels", () => {
464
+ const heat = new HeatmapPanel({ title: "Heat", datasource: prometheus, targets: [up], options: { yAxis: { unit: "sec" }, cellValues: { unit: "Short" } } });
465
+ const table = new TablePanel({
466
+ title: "T",
467
+ datasource: prometheus,
468
+ targets: [up],
469
+ fieldConfig: { overrides: [{ matcher: { id: "byName", options: "x" }, properties: [{ id: "decimals", value: 2 }, { id: "unit", value: "celcius" }] }] },
470
+ });
471
+ const messages = graf115.check(ctxOf({ prometheus, d: new Dashboard({ title: "D", panels: [heat, table] }) })).map((d) => d.message);
472
+ expect(messages).toEqual([
473
+ expect.stringContaining('options.yAxis.unit: unit "sec"'),
474
+ expect.stringContaining('options.cellValues.unit: unit "Short" is not one Grafana knows, so it is shown as a literal suffix. Did you mean "short"?'),
475
+ expect.stringContaining('fieldConfig.overrides[0].properties[1]: unit "celcius" is not one Grafana knows, so it is shown as a literal suffix. Did you mean "celsius"?'),
476
+ ]);
477
+
478
+ const graph = panelJson({ type: "graph", yaxes: [{ format: "bytes" }, { format: "ops/s" }] });
479
+ const lib = panelJson({ fieldConfig: { defaults: { unit: "req/s" }, overrides: [] } });
480
+ const dash = dashboardJson([graph], [], { __elements: { lib: { kind: 1, model: lib } } });
481
+ expect(graf115.check(ctxOfJson(dash)).map((d) => d.message)).toEqual([
482
+ expect.stringContaining('panel "p" (id 1) yaxes[1].format: unit "ops/s"'),
483
+ expect.stringContaining('library panel "lib" fieldConfig.defaults.unit: unit "req/s"'),
484
+ ]);
485
+ });
486
+
487
+ test("the vendored registry is Grafana v13.2.2's", () => {
488
+ expect(GRAFANA_UNIT_IDS.length).toBe(274);
489
+ expect(GRAFANA_UNIT_IDS).toEqual(expect.arrayContaining(["none", "bytes", "decbytes", "reqps", "ms", "percentunit", "celsius", "currencyUSD", "dateTimeAsIso", "h"]));
490
+ expect(GRAFANA_UNIT_IDS).not.toContain("byte");
491
+ });
492
+ });
493
+
494
+ describe("GRAF115 over real Grafana exports and community dashboards", () => {
495
+ const fixtures = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "test", "fixtures");
496
+ const exportsDir = join(fixtures, "exports");
497
+ const files = [
498
+ ...readdirSync(exportsDir)
499
+ .filter((d) => d.startsWith("grafana-"))
500
+ .flatMap((v) => readdirSync(join(exportsDir, v)).filter((f) => f.endsWith(".json") && !f.includes("-resource")).map((f) => `exports/${v}/${f}`)),
501
+ ...readdirSync(join(fixtures, "community"))
502
+ .filter((f) => f.endsWith(".json"))
503
+ .map((f) => `community/${f}`),
504
+ ];
505
+ let unitsSeen = 0;
506
+
507
+ test.each(files)("%s has no GRAF115 warnings", (name) => {
508
+ const text = readFileSync(join(fixtures, name), "utf-8");
509
+ unitsSeen += panelsOf(JSON.parse(text)).flatMap(({ panel }) => panelUnits(panel)).length;
510
+ expect(graf115.check(makePostSynthCtxFromFiles("grafana", { [name]: text }, "{}"))).toEqual([]);
511
+ });
512
+
513
+ // Non-vacuous: the corpus sets hundreds of units between them.
514
+ test("the corpus sets units", () => {
515
+ expect(unitsSeen).toBeGreaterThan(200);
516
+ });
517
+ });
518
+
519
+ describe("annotations, folders and datasource deletes (#2953)", () => {
520
+ const deploys = (datasource: unknown, extra: Record<string, unknown> = {}) => ({ name: "Deploys", datasource, ...extra }) as never;
521
+
522
+ test("GRAF101 flags an annotation on an undeclared datasource, GRAF102 one of another type", () => {
523
+ const d = new Dashboard({ title: "D", annotations: [deploys({ type: "prometheus", uid: "gone" })] });
524
+ expect(ids(graf101, ctxOf({ prometheus, d }))).toEqual([["GRAF101", "error"]]);
525
+ const t = new Dashboard({ title: "T", annotations: [deploys({ type: "loki", uid: "prometheus" })] });
526
+ expect(ids(graf102, ctxOf({ prometheus, t }))).toEqual([["GRAF102", "error"]]);
527
+ expect(ids(graf101, ctxOf({ prometheus, ok: new Dashboard({ title: "O", annotations: [deploys(prometheus)] }) }))).toEqual([]);
528
+ });
529
+
530
+ test("GRAF101 flags an ExternalDatasource the provisioning file deletes", () => {
531
+ const old = new ExternalDatasource({ type: "prometheus", uid: "old", name: "Old" });
532
+ const d = new Dashboard({ title: "D", panels: [new StatPanel({ datasource: old, targets: [up] })] });
533
+ const diags = graf101.check(ctxOf({ prometheus, old, d, settings: new DatasourceProvisioning({ deleteDatasources: [{ name: "Old" }] }) }));
534
+ // The panel's own ref and its query's, as for any undeclared datasource.
535
+ expect(diags.map((x) => [x.checkId, x.severity])).toEqual([["GRAF101", "error"], ["GRAF101", "error"]]);
536
+ expect(diags[0].message).toContain("deleteDatasources");
537
+ });
538
+
539
+ test("GRAF103 reads an annotation's query and datasource; GRAF108 parses its PromQL", () => {
540
+ const d = new Dashboard({ title: "D", annotations: [deploys(prometheus, { expr: "changes(up{job=\"$job\"}[5m] > 0" })] });
541
+ expect(ids(graf103, ctxOf({ prometheus, d }))).toEqual([["GRAF103", "error"]]);
542
+ expect(ids(graf108, ctxOf({ prometheus, d }))).toEqual([["GRAF108", "error"]]);
543
+ });
544
+
545
+ test("GRAF104 flags two folders with one uid; GRAF106 a folder uid Grafana refuses", () => {
546
+ const two = ctxOf({ a: new Dashboard({ title: "A", folder: "Team A" }), b: new Dashboard({ title: "B", folder: "team-a" }) });
547
+ expect(ids(graf104, two)).toEqual([["GRAF104", "error"]]);
548
+ expect(ids(graf106, ctxOf({ g: new Dashboard({ title: "G", folder: "General" }) }))).toEqual([["GRAF106", "error"]]);
549
+ expect(ids(graf106, ctxOf({ f: new Folder({ title: "F", uid: "not a uid" }) }))).toEqual([["GRAF106", "error"]]);
550
+ expect(ids(graf106, ctxOf({ f: new Folder({ title: "F" }), d: new Dashboard({ title: "D", folder: "F/G" }) }))).toEqual([]);
551
+ });
552
+ });
@@ -1,15 +1,38 @@
1
1
  import * as ts from "typescript";
2
2
  import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
3
3
  import { constInitializers, constructorArgs, literalText, position, propertyName, resolveObject } from "./grafana-ast";
4
+ import { CONTACT_POINT_SECRET_SETTINGS, EXPANDED_VALUE as EXPANDED } from "../../contact-point-secrets";
4
5
 
5
- /** How Grafana provisioning reads a value from outside the file: `$VAR`, `${VAR}`, `$__env{…}`, `$__file{…}`, `$__vault{…}`. */
6
- const EXPANDED = /\$(\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*|__(env|file|vault)\{[^}]+\})/;
6
+ /** Follow an identifier to the array literal it was declared as, when it is one in this file. */
7
+ function resolveArray(node: ts.Expression, consts: Map<string, ts.Expression>, depth = 0): ts.ArrayLiteralExpression | undefined {
8
+ let n: ts.Expression = node;
9
+ while (ts.isAsExpression(n) || ts.isSatisfiesExpression(n) || ts.isParenthesizedExpression(n)) n = n.expression;
10
+ if (ts.isArrayLiteralExpression(n)) return n;
11
+ if (ts.isIdentifier(n) && depth < 5) {
12
+ const init = consts.get(n.text);
13
+ if (init) return resolveArray(init, consts, depth + 1);
14
+ }
15
+ return undefined;
16
+ }
17
+
18
+ /** The value of a property, `name: value` or the shorthand `name` (followed as an identifier). */
19
+ function prop(obj: ts.ObjectLiteralExpression, name: string): ts.Expression | undefined {
20
+ for (const p of obj.properties) {
21
+ if (propertyName(p) === name) return (p as ts.PropertyAssignment).initializer;
22
+ if (ts.isShorthandPropertyAssignment(p) && p.name.text === name) return p.name;
23
+ }
24
+ return undefined;
25
+ }
7
26
 
8
27
  /**
9
- * GRAF002: a datasource secret written as a literal.
28
+ * GRAF002: a datasource or contact point secret written as a literal.
10
29
  *
11
- * `secureJsonData` holds passwords, tokens and TLS keys. A literal there is a
12
- * secret in the repository and in the emitted provisioning file. Grafana
30
+ * A datasource's `secureJsonData` holds passwords, tokens and TLS keys, and
31
+ * so do the settings Grafana stores encrypted for each contact point
32
+ * integration (a Slack webhook `url`, a PagerDuty `integrationKey`, a
33
+ * webhook's `authorization_credentials`; see `CONTACT_POINT_SECRET_SETTINGS`).
34
+ * A literal there is a secret in the repository and in the emitted
35
+ * provisioning file. Grafana
13
36
  * expands `$__env{NAME}`, `$__file{/path}` and `${NAME}` when it reads the
14
37
  * file, so the declaration never needs the value. A value written as a
15
38
  * named `const` in the same file is followed.
@@ -18,7 +41,7 @@ export const literalSecretRule: LintRule = {
18
41
  id: "GRAF002",
19
42
  severity: "error",
20
43
  category: "security",
21
- description: "Datasource secureJsonData value declared as a literal instead of $__env{...} or $__file{...}",
44
+ description: "Datasource secureJsonData or contact point secret setting declared as a literal instead of $__env{...} or $__file{...}",
22
45
 
23
46
  check(context: LintContext): LintDiagnostic[] {
24
47
  const diagnostics: LintDiagnostic[] = [];
@@ -43,6 +66,37 @@ export const literalSecretRule: LintRule = {
43
66
  }
44
67
  }
45
68
  }
69
+ for (const { arg } of constructorArgs(source, (n) => n === "ContactPoint")) {
70
+ const receivers = prop(arg, "receivers");
71
+ const list = receivers ? resolveArray(receivers, consts) : undefined;
72
+ for (const el of list?.elements ?? []) {
73
+ const receiver = resolveObject(el as ts.Expression, consts);
74
+ if (!receiver) continue;
75
+ const typeNode = prop(receiver, "type");
76
+ const type = typeNode ? literalText(typeNode) : undefined;
77
+ const settingsNode = prop(receiver, "settings");
78
+ const settings = settingsNode ? resolveObject(settingsNode, consts) : undefined;
79
+ if (!type || !settings) continue;
80
+ for (const path of CONTACT_POINT_SECRET_SETTINGS[type] ?? []) {
81
+ let node: ts.ObjectLiteralExpression | undefined = settings;
82
+ const segments = path.split(".");
83
+ for (const seg of segments.slice(0, -1)) {
84
+ const next: ts.Expression | undefined = node ? prop(node, seg) : undefined;
85
+ node = next ? resolveObject(next, consts) : undefined;
86
+ }
87
+ const valueNode = node ? prop(node, segments[segments.length - 1]) : undefined;
88
+ const text = valueNode ? literalText(valueNode) : undefined;
89
+ if (!valueNode || text === undefined || text === "" || EXPANDED.test(text)) continue;
90
+ diagnostics.push({
91
+ ruleId: "GRAF002",
92
+ severity: "error",
93
+ message: `${type} contact point setting ${path} is a literal secret; write "$__env{NAME}" or "$__file{/path}" and let Grafana read it`,
94
+ file: context.filePath,
95
+ ...position(source, valueNode),
96
+ });
97
+ }
98
+ }
99
+ }
46
100
  return diagnostics;
47
101
  },
48
102
  };
@@ -67,4 +67,31 @@ describe("GRAF002 literal secret", () => {
67
67
  );
68
68
  expect(diags).toEqual([]);
69
69
  });
70
+
71
+ test("flags a contact point's secret setting written as a literal, inline or through consts", () => {
72
+ const diags = literalSecretRule.check(
73
+ ctx(`
74
+ new ContactPoint({ name: "oncall", receivers: [{ type: "slack", settings: { url: "https://hooks.slack.com/services/T/B/X", recipient: "#c" } }] });
75
+ const pd = { integrationKey: "abc123" };
76
+ const receivers = [{ type: "pagerduty", settings: pd }, { type: "webhook", settings: { url: "https://x", tlsConfig: { clientKey: "-----BEGIN" } } }];
77
+ new ContactPoint({ name: "pager", receivers });
78
+ `),
79
+ );
80
+ expect(diags.map((d) => d.message)).toEqual([
81
+ expect.stringContaining("slack contact point setting url"),
82
+ expect.stringContaining("pagerduty contact point setting integrationKey"),
83
+ expect.stringContaining("webhook contact point setting tlsConfig.clientKey"),
84
+ ]);
85
+ });
86
+
87
+ test("accepts expanded contact point secrets, and settings that are not secret", () => {
88
+ const diags = literalSecretRule.check(
89
+ ctx(`
90
+ new ContactPoint({ name: "oncall", receivers: [{ type: "slack", settings: { url: "$__env{SLACK_URL}", recipient: "#c" } }] });
91
+ new ContactPoint({ name: "hook", receivers: [{ type: "webhook", settings: { url: "https://tickets.example.com", authorization_credentials: "\${TOKEN}" } }] });
92
+ new ContactPoint({ name: "mail", receivers: [{ type: "email", settings: { addresses: "a@example.com" } }] });
93
+ `),
94
+ );
95
+ expect(diags).toEqual([]);
96
+ });
70
97
  });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Importing the lexicon must not pay for schema validation (#2958): ajv
3
+ * loads the first time a schema is checked, and the schemas come from the
4
+ * generated `spec/schemas.gen.ts`, not from files beside the source.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import { execFileSync } from "child_process";
9
+ import { dirname, join } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { SCHEMA_NAMES } from "./pin";
12
+ import { loadSchema } from "./spec/schemas";
13
+ import { bundledSchema, schemaValidationUnavailable } from "./schema-validate";
14
+
15
+ const srcDir = dirname(fileURLToPath(import.meta.url));
16
+ const tsx = join(srcDir, "..", "..", "..", "node_modules", ".bin", "tsx");
17
+
18
+ /** How many ajv modules a fresh process has loaded after importing `entries`. */
19
+ function ajvModulesAfterImporting(...entries: string[]): number {
20
+ const imports = entries.map((e) => `import(${JSON.stringify(join(srcDir, e))})`).join(", ");
21
+ const script = `Promise.all([${imports}]).then(() => console.log(Object.keys(require.cache).filter((k) => k.includes("/ajv/")).length))`;
22
+ return Number(execFileSync(tsx, ["-e", script], { encoding: "utf-8" }).trim());
23
+ }
24
+
25
+ describe("load cost", () => {
26
+ test("importing the package root, its plugin or the validation subpath does not load ajv", () => {
27
+ // index.ts imports plugin.ts, whose post-synth checks reach schema-validate.ts.
28
+ expect(ajvModulesAfterImporting("index.ts", "validation.ts")).toBe(0);
29
+ });
30
+
31
+ test("validation reads the overlaid schemas from the generated module", () => {
32
+ expect(schemaValidationUnavailable()).toBeUndefined();
33
+ for (const name of SCHEMA_NAMES) expect(bundledSchema(name)).toEqual(loadSchema(name));
34
+ });
35
+ });
@@ -14,6 +14,11 @@ describe("grafana LSP completions", () => {
14
14
  expect(items.map((i) => i.label)).toContain("TimeSeriesPanel");
15
15
  });
16
16
 
17
+ it("offers every panel class sharing a prefix", () => {
18
+ const items = completions({ uri: "file:///d.ts", content: "new Sta", position: { line: 0, character: 7 }, wordAtCursor: "Sta", linePrefix: "new Sta" });
19
+ expect(items.map((i) => i.label)).toEqual(expect.arrayContaining(["StatPanel", "StateTimelinePanel", "StatusHistoryPanel"]));
20
+ });
21
+
17
22
  it("offers the query classes by prefix", () => {
18
23
  const items = completions({
19
24
  uri: "file:///q.ts",
@@ -14,6 +14,13 @@ describe("grafana LSP hover", () => {
14
14
  expect(info?.contents).toContain("Panel plugin `stat`");
15
15
  });
16
16
 
17
+ it("names the schema a panel's options come from, or that there is none", () => {
18
+ const at = (word: string) => hover({ uri: "file:///d.ts", content: `new ${word}({})`, position: { line: 0, character: 6 }, word, lineText: `new ${word}({})` })?.contents;
19
+ expect(at("NodeGraphPanel")).toContain("Panel plugin `nodeGraph`. Options typed from");
20
+ expect(at("NodeGraphPanel")).toContain("(`nodegraph`)");
21
+ expect(at("AlertListPanel")).toContain("Grafana publishes no options schema for it");
22
+ });
23
+
17
24
  it("describes a query and its datasource type", () => {
18
25
  const info = hover({ uri: "file:///q.ts", content: "new TempoQuery({})", position: { line: 0, character: 6 }, word: "TempoQuery", lineText: "new TempoQuery({})" });
19
26
  expect(info?.contents).toContain("`tempo` datasources");