@intentius/chant-lexicon-grafana 0.99.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 (224) hide show
  1. package/README.md +12 -6
  2. package/dist/annotations.d.ts +61 -0
  3. package/dist/annotations.d.ts.map +1 -0
  4. package/dist/api/apply.d.ts +8 -4
  5. package/dist/api/apply.d.ts.map +1 -1
  6. package/dist/api/fake-grafana.d.ts +2 -0
  7. package/dist/api/fake-grafana.d.ts.map +1 -1
  8. package/dist/api/folders.d.ts +53 -17
  9. package/dist/api/folders.d.ts.map +1 -1
  10. package/dist/build.d.ts +26 -5
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/catalog.d.ts +1 -1
  13. package/dist/catalog.d.ts.map +1 -1
  14. package/dist/codegen/docs.d.ts.map +1 -1
  15. package/dist/codegen/generate.d.ts +7 -1
  16. package/dist/codegen/generate.d.ts.map +1 -1
  17. package/dist/composites/shared.d.ts +4 -3
  18. package/dist/composites/shared.d.ts.map +1 -1
  19. package/dist/dashboard.d.ts +42 -14
  20. package/dist/dashboard.d.ts.map +1 -1
  21. package/dist/datasource-refs.d.ts +9 -5
  22. package/dist/datasource-refs.d.ts.map +1 -1
  23. package/dist/datasource-settings.d.ts +834 -0
  24. package/dist/datasource-settings.d.ts.map +1 -0
  25. package/dist/datasource.d.ts +53 -11
  26. package/dist/datasource.d.ts.map +1 -1
  27. package/dist/deep-observe-hooks.d.ts +1 -1
  28. package/dist/deep-observe-hooks.d.ts.map +1 -1
  29. package/dist/deep-observe.d.ts +9 -4
  30. package/dist/deep-observe.d.ts.map +1 -1
  31. package/dist/describe-resources.d.ts +1 -0
  32. package/dist/describe-resources.d.ts.map +1 -1
  33. package/dist/detect.d.ts +4 -2
  34. package/dist/detect.d.ts.map +1 -1
  35. package/dist/folder.d.ts +68 -0
  36. package/dist/folder.d.ts.map +1 -0
  37. package/dist/import/generator.d.ts.map +1 -1
  38. package/dist/import/mappings.d.ts +12 -6
  39. package/dist/import/mappings.d.ts.map +1 -1
  40. package/dist/import/model.d.ts +7 -0
  41. package/dist/import/model.d.ts.map +1 -1
  42. package/dist/import/normalize.d.ts.map +1 -1
  43. package/dist/import/parser.d.ts +16 -1
  44. package/dist/import/parser.d.ts.map +1 -1
  45. package/dist/import/testdata/fixtures.d.ts +6 -0
  46. package/dist/import/testdata/fixtures.d.ts.map +1 -1
  47. package/dist/import/v2.d.ts +4 -3
  48. package/dist/import/v2.d.ts.map +1 -1
  49. package/dist/index.d.ts +11 -9
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/init-templates.d.ts +27 -0
  52. package/dist/init-templates.d.ts.map +1 -0
  53. package/dist/integrity.json +11 -9
  54. package/dist/lint/audit-catalog.d.ts +1 -1
  55. package/dist/lint/audit-catalog.d.ts.map +1 -1
  56. package/dist/lint/post-synth/graf101.d.ts +1 -1
  57. package/dist/lint/post-synth/graf104.d.ts +2 -2
  58. package/dist/lint/post-synth/graf106.d.ts +2 -2
  59. package/dist/lint/post-synth/graf110.d.ts +8 -0
  60. package/dist/lint/post-synth/graf110.d.ts.map +1 -0
  61. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  62. package/dist/lint/post-synth/index.d.ts.map +1 -1
  63. package/dist/manifest.json +1 -1
  64. package/dist/meta.json +70 -0
  65. package/dist/okf/index.md +18 -3
  66. package/dist/okf/rules/GRAF101.md +2 -2
  67. package/dist/okf/rules/GRAF104.md +2 -2
  68. package/dist/okf/rules/GRAF106.md +2 -2
  69. package/dist/okf/rules/GRAF110.md +11 -0
  70. package/dist/okf/types/AdhocVariable.md +9 -0
  71. package/dist/okf/types/AzureMonitorQuery.md +9 -0
  72. package/dist/okf/types/BigQueryQuery.md +9 -0
  73. package/dist/okf/types/CloudMonitoringQuery.md +9 -0
  74. package/dist/okf/types/CloudWatchQuery.md +9 -0
  75. package/dist/okf/types/Dashboard.md +1 -1
  76. package/dist/okf/types/Datasource.md +1 -1
  77. package/dist/okf/types/DatasourceProvisioning.md +9 -0
  78. package/dist/okf/types/ElasticsearchQuery.md +9 -0
  79. package/dist/okf/types/ExternalDatasource.md +1 -1
  80. package/dist/okf/types/Folder.md +9 -0
  81. package/dist/okf/types/GroupByVariable.md +9 -0
  82. package/dist/okf/types/MSSQLQuery.md +9 -0
  83. package/dist/okf/types/MySQLQuery.md +9 -0
  84. package/dist/okf/types/PostgresQuery.md +9 -0
  85. package/dist/okf/types/PyroscopeQuery.md +9 -0
  86. package/dist/okf/types/SwitchVariable.md +9 -0
  87. package/dist/op/activities/grafana-apply.d.ts +11 -0
  88. package/dist/op/activities/grafana-apply.d.ts.map +1 -1
  89. package/dist/op/activities/index.d.ts +1 -1
  90. package/dist/op/activities/index.d.ts.map +1 -1
  91. package/dist/ownership.d.ts +11 -0
  92. package/dist/ownership.d.ts.map +1 -1
  93. package/dist/pin.d.ts +1 -1
  94. package/dist/pin.d.ts.map +1 -1
  95. package/dist/plugin.d.ts.map +1 -1
  96. package/dist/promql-check.d.ts.map +1 -1
  97. package/dist/query-models.d.ts +95 -0
  98. package/dist/query-models.d.ts.map +1 -0
  99. package/dist/query.d.ts +107 -10
  100. package/dist/query.d.ts.map +1 -1
  101. package/dist/rules/graf101.ts +2 -2
  102. package/dist/rules/graf104.ts +3 -3
  103. package/dist/rules/graf106.ts +3 -3
  104. package/dist/rules/graf110.ts +17 -0
  105. package/dist/rules/grafana-helpers.ts +13 -3
  106. package/dist/schema/azuremonitor.gen.d.ts +354 -0
  107. package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
  108. package/dist/schema/bigquery.gen.d.ts +93 -0
  109. package/dist/schema/bigquery.gen.d.ts.map +1 -0
  110. package/dist/schema/cloudwatch.gen.d.ts +394 -0
  111. package/dist/schema/cloudwatch.gen.d.ts.map +1 -0
  112. package/dist/schema/elasticsearch.gen.d.ts +444 -0
  113. package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
  114. package/dist/schema/googlecloudmonitoring.gen.d.ts +312 -0
  115. package/dist/schema/googlecloudmonitoring.gen.d.ts.map +1 -0
  116. package/dist/schema/grafanapyroscope.gen.d.ts +60 -0
  117. package/dist/schema/grafanapyroscope.gen.d.ts.map +1 -0
  118. package/dist/schema/index.d.ts +6 -0
  119. package/dist/schema/index.d.ts.map +1 -1
  120. package/dist/schema/table.gen.d.ts +13 -0
  121. package/dist/schema/table.gen.d.ts.map +1 -1
  122. package/dist/schema-validate.d.ts +9 -0
  123. package/dist/schema-validate.d.ts.map +1 -1
  124. package/dist/skill-defs.d.ts.map +1 -1
  125. package/dist/skills/chant-grafana-operations.md +108 -0
  126. package/dist/skills/chant-grafana.md +5 -4
  127. package/dist/spec/schemas.d.ts +9 -7
  128. package/dist/spec/schemas.d.ts.map +1 -1
  129. package/dist/spec/schemas.gen.d.ts +3 -0
  130. package/dist/spec/schemas.gen.d.ts.map +1 -0
  131. package/dist/validate-alerting.d.ts.map +1 -1
  132. package/dist/validate-output.d.ts +15 -2
  133. package/dist/validate-output.d.ts.map +1 -1
  134. package/dist/validate.d.ts.map +1 -1
  135. package/dist/validation.d.ts +14 -0
  136. package/dist/validation.d.ts.map +1 -0
  137. package/dist/variables.d.ts +102 -5
  138. package/dist/variables.d.ts.map +1 -1
  139. package/package.json +2 -2
  140. package/src/annotations.ts +84 -0
  141. package/src/api/apply.test.ts +19 -1
  142. package/src/api/apply.ts +18 -8
  143. package/src/api/fake-grafana.ts +6 -2
  144. package/src/api/folders.ts +106 -32
  145. package/src/build.test.ts +142 -6
  146. package/src/build.ts +126 -24
  147. package/src/catalog.ts +20 -2
  148. package/src/codegen/docs.ts +13 -6
  149. package/src/codegen/generate.ts +27 -1
  150. package/src/composites/shared.ts +3 -2
  151. package/src/dashboard.ts +59 -14
  152. package/src/datasource-refs.ts +30 -6
  153. package/src/datasource-settings.test.ts +106 -0
  154. package/src/datasource-settings.ts +941 -0
  155. package/src/datasource.ts +71 -11
  156. package/src/deep-observe-hooks.ts +16 -1
  157. package/src/deep-observe.test.ts +47 -1
  158. package/src/deep-observe.ts +36 -13
  159. package/src/describe-resources.test.ts +16 -0
  160. package/src/describe-resources.ts +36 -1
  161. package/src/detect.ts +3 -3
  162. package/src/folder.ts +118 -0
  163. package/src/generated/lexicon-grafana.json +70 -0
  164. package/src/import/generated-types.e2e.test.ts +15 -3
  165. package/src/import/generator.ts +2 -1
  166. package/src/import/mappings.ts +98 -26
  167. package/src/import/model.ts +7 -0
  168. package/src/import/normalize.ts +32 -3
  169. package/src/import/parser.test.ts +97 -21
  170. package/src/import/parser.ts +169 -18
  171. package/src/import/roundtrip.test.ts +104 -9
  172. package/src/import/testdata/fixtures.ts +8 -0
  173. package/src/import/v2.ts +4 -3
  174. package/src/index.ts +122 -13
  175. package/src/init-templates.test.ts +74 -0
  176. package/src/init-templates.ts +349 -0
  177. package/src/lint/audit-catalog.ts +9 -1
  178. package/src/lint/post-synth/graf101.ts +2 -2
  179. package/src/lint/post-synth/graf104.ts +3 -3
  180. package/src/lint/post-synth/graf106.ts +3 -3
  181. package/src/lint/post-synth/graf110.ts +17 -0
  182. package/src/lint/post-synth/grafana-helpers.ts +13 -3
  183. package/src/lint/post-synth/index.ts +2 -0
  184. package/src/lint/post-synth/post-synth.test.ts +99 -2
  185. package/src/load-cost.test.ts +35 -0
  186. package/src/op/activities/grafana-apply.e2e.test.ts +14 -5
  187. package/src/op/activities/grafana-apply.test.ts +16 -2
  188. package/src/op/activities/grafana-apply.ts +29 -3
  189. package/src/op/activities/index.ts +1 -1
  190. package/src/ownership.ts +19 -0
  191. package/src/panels.test.ts +3 -1
  192. package/src/pin.ts +12 -0
  193. package/src/plugin.test.ts +9 -2
  194. package/src/plugin.ts +9 -0
  195. package/src/promql-check.ts +5 -1
  196. package/src/query-models.ts +94 -0
  197. package/src/query.test.ts +166 -0
  198. package/src/query.ts +177 -12
  199. package/src/schema/azuremonitor.gen.ts +381 -0
  200. package/src/schema/bigquery.gen.ts +110 -0
  201. package/src/schema/cloudwatch.gen.ts +423 -0
  202. package/src/schema/elasticsearch.gen.ts +508 -0
  203. package/src/schema/googlecloudmonitoring.gen.ts +330 -0
  204. package/src/schema/grafanapyroscope.gen.ts +66 -0
  205. package/src/schema/index.ts +6 -0
  206. package/src/schema/table.gen.ts +14 -0
  207. package/src/schema-validate.ts +58 -7
  208. package/src/skill-defs.ts +17 -0
  209. package/src/skills/chant-grafana-operations.md +108 -0
  210. package/src/skills/chant-grafana.md +5 -4
  211. package/src/spec/overlay/table.overlay.json +19 -0
  212. package/src/spec/schemas/azuremonitor.jsonschema.json +679 -0
  213. package/src/spec/schemas/bigquery.jsonschema.json +290 -0
  214. package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
  215. package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
  216. package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
  217. package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -0
  218. package/src/spec/schemas.gen.ts +40 -0
  219. package/src/spec/schemas.ts +9 -7
  220. package/src/validate-alerting.ts +4 -2
  221. package/src/validate-output.ts +96 -4
  222. package/src/validate.ts +14 -0
  223. package/src/validation.ts +27 -0
  224. package/src/variables.ts +113 -6
@@ -1,4 +1,9 @@
1
1
  {
2
+ "AdhocVariable": {
3
+ "resourceType": "Grafana::Variable::adhoc",
4
+ "kind": "resource",
5
+ "lexicon": "grafana"
6
+ },
2
7
  "AlertListPanel": {
3
8
  "resourceType": "Grafana::Panel::alertlist",
4
9
  "kind": "resource",
@@ -19,6 +24,11 @@
19
24
  "kind": "resource",
20
25
  "lexicon": "grafana"
21
26
  },
27
+ "AzureMonitorQuery": {
28
+ "resourceType": "Grafana::Query::grafana-azure-monitor-datasource",
29
+ "kind": "resource",
30
+ "lexicon": "grafana"
31
+ },
22
32
  "BarChartPanel": {
23
33
  "resourceType": "Grafana::Panel::barchart",
24
34
  "kind": "resource",
@@ -29,6 +39,11 @@
29
39
  "kind": "resource",
30
40
  "lexicon": "grafana"
31
41
  },
42
+ "BigQueryQuery": {
43
+ "resourceType": "Grafana::Query::grafana-bigquery-datasource",
44
+ "kind": "resource",
45
+ "lexicon": "grafana"
46
+ },
32
47
  "CanvasPanel": {
33
48
  "resourceType": "Grafana::Panel::canvas",
34
49
  "kind": "resource",
@@ -39,6 +54,16 @@
39
54
  "kind": "resource",
40
55
  "lexicon": "grafana"
41
56
  },
57
+ "CloudMonitoringQuery": {
58
+ "resourceType": "Grafana::Query::stackdriver",
59
+ "kind": "resource",
60
+ "lexicon": "grafana"
61
+ },
62
+ "CloudWatchQuery": {
63
+ "resourceType": "Grafana::Query::cloudwatch",
64
+ "kind": "resource",
65
+ "lexicon": "grafana"
66
+ },
42
67
  "ConstantVariable": {
43
68
  "resourceType": "Grafana::Variable::constant",
44
69
  "kind": "resource",
@@ -69,11 +94,21 @@
69
94
  "kind": "resource",
70
95
  "lexicon": "grafana"
71
96
  },
97
+ "DatasourceProvisioning": {
98
+ "resourceType": "Grafana::DatasourceProvisioning",
99
+ "kind": "resource",
100
+ "lexicon": "grafana"
101
+ },
72
102
  "DatasourceVariable": {
73
103
  "resourceType": "Grafana::Variable::datasource",
74
104
  "kind": "resource",
75
105
  "lexicon": "grafana"
76
106
  },
107
+ "ElasticsearchQuery": {
108
+ "resourceType": "Grafana::Query::elasticsearch",
109
+ "kind": "resource",
110
+ "lexicon": "grafana"
111
+ },
77
112
  "ExternalDatasource": {
78
113
  "resourceType": "Grafana::ExternalDatasource",
79
114
  "kind": "resource",
@@ -84,6 +119,11 @@
84
119
  "kind": "resource",
85
120
  "lexicon": "grafana"
86
121
  },
122
+ "Folder": {
123
+ "resourceType": "Grafana::Folder",
124
+ "kind": "resource",
125
+ "lexicon": "grafana"
126
+ },
87
127
  "GaugePanel": {
88
128
  "resourceType": "Grafana::Panel::gauge",
89
129
  "kind": "resource",
@@ -94,6 +134,11 @@
94
134
  "kind": "resource",
95
135
  "lexicon": "grafana"
96
136
  },
137
+ "GroupByVariable": {
138
+ "resourceType": "Grafana::Variable::groupby",
139
+ "kind": "resource",
140
+ "lexicon": "grafana"
141
+ },
97
142
  "HeatmapPanel": {
98
143
  "resourceType": "Grafana::Panel::heatmap",
99
144
  "kind": "resource",
@@ -124,11 +169,21 @@
124
169
  "kind": "resource",
125
170
  "lexicon": "grafana"
126
171
  },
172
+ "MSSQLQuery": {
173
+ "resourceType": "Grafana::Query::mssql",
174
+ "kind": "resource",
175
+ "lexicon": "grafana"
176
+ },
127
177
  "MuteTiming": {
128
178
  "resourceType": "Grafana::MuteTiming",
129
179
  "kind": "resource",
130
180
  "lexicon": "grafana"
131
181
  },
182
+ "MySQLQuery": {
183
+ "resourceType": "Grafana::Query::mysql",
184
+ "kind": "resource",
185
+ "lexicon": "grafana"
186
+ },
132
187
  "NodeGraphPanel": {
133
188
  "resourceType": "Grafana::Panel::nodeGraph",
134
189
  "kind": "resource",
@@ -149,11 +204,21 @@
149
204
  "kind": "resource",
150
205
  "lexicon": "grafana"
151
206
  },
207
+ "PostgresQuery": {
208
+ "resourceType": "Grafana::Query::grafana-postgresql-datasource",
209
+ "kind": "resource",
210
+ "lexicon": "grafana"
211
+ },
152
212
  "PromQuery": {
153
213
  "resourceType": "Grafana::Query::prometheus",
154
214
  "kind": "resource",
155
215
  "lexicon": "grafana"
156
216
  },
217
+ "PyroscopeQuery": {
218
+ "resourceType": "Grafana::Query::grafana-pyroscope-datasource",
219
+ "kind": "resource",
220
+ "lexicon": "grafana"
221
+ },
157
222
  "QueryVariable": {
158
223
  "resourceType": "Grafana::Variable::query",
159
224
  "kind": "resource",
@@ -194,6 +259,11 @@
194
259
  "kind": "resource",
195
260
  "lexicon": "grafana"
196
261
  },
262
+ "SwitchVariable": {
263
+ "resourceType": "Grafana::Variable::switch",
264
+ "kind": "resource",
265
+ "lexicon": "grafana"
266
+ },
197
267
  "TablePanel": {
198
268
  "resourceType": "Grafana::Panel::table",
199
269
  "kind": "resource",
@@ -18,7 +18,7 @@ import { join } from "path";
18
18
  import * as ts from "typescript";
19
19
  import { GrafanaParser } from "./parser";
20
20
  import { GrafanaGenerator } from "./generator";
21
- import { ALERTING, COMMUNITY, KUBE_PROMETHEUS, UI_EXPORTS, V2_EXPORTS, exampleOutputs, pkgDir, read, removeDir, repoRoot, writeFiles } from "./testdata/fixtures";
21
+ import { ALERTING, COMMUNITY, KUBE_PROMETHEUS, PROVISIONING, UI_EXPORTS, V2_EXPORTS, exampleOutputs, pkgDir, read, removeDir, repoRoot, writeFiles } from "./testdata/fixtures";
22
22
 
23
23
  type Files = Array<{ path: string; content: string }>;
24
24
 
@@ -68,13 +68,25 @@ function byProperty(errors: string[]): Record<string, number> {
68
68
 
69
69
  describe("the generated source type-checks against the lexicon's types", () => {
70
70
  test("Grafana's UI exports, the v2 dashboards and the examples' dashboards type-check clean", async () => {
71
- const projects: Record<string, Files> = Object.fromEntries([...UI_EXPORTS, ...V2_EXPORTS].map((f) => [f, generate(read(f))]));
71
+ const projects: Record<string, Files> = Object.fromEntries(
72
+ [...UI_EXPORTS, ...V2_EXPORTS, "provisioning/typed-plugins.datasources.yaml"].map((f) => [f, generate(read(f))]),
73
+ );
72
74
  for (const [name, text] of await exampleOutputs()) {
73
75
  if (/^[^/]+\/dashboards\/.*\.json$/.test(name)) projects[name] = generate(text);
74
76
  }
75
77
  expect(typeErrors(projects)).toEqual([]);
76
78
  }, 180_000);
77
79
 
80
+ test("real-world provisioning files: settings the pinned plugins no longer read are where tsc points", () => {
81
+ const found: Record<string, Record<string, number>> = {};
82
+ for (const file of PROVISIONING) found[file] = byProperty(typeErrors({ [file]: generate(read(file)) }));
83
+ expect(found).toEqual({
84
+ "provisioning/typed-plugins.datasources.yaml": {},
85
+ // Tempo's lokiSearch, which tempo 13.1.5 (bundled with Grafana 13.2.2) no longer reads.
86
+ "provisioning/docker-otel-lgtm.datasources.yaml": { lokiSearch: 1 },
87
+ });
88
+ }, 120_000);
89
+
78
90
  test("alerting provisioning files, and what the alerting example builds, type-check clean", async () => {
79
91
  const projects: Record<string, Files> = Object.fromEntries(ALERTING.map((f) => [f, generate(read(f))]));
80
92
  for (const [name, text] of await exampleOutputs()) {
@@ -90,7 +102,7 @@ describe("the generated source type-checks against the lexicon's types", () => {
90
102
  expect(found).toEqual({
91
103
  "community/node-exporter-full.json": { step: 273, metric: 1 },
92
104
  "community/k8s-views-global.json": {},
93
- "community/k8s-views-pods.json": { footer: 3 },
105
+ "community/k8s-views-pods.json": { wrapText: 2 },
94
106
  "community/traefik.json": {},
95
107
  "community/redis.json": { metric: 11, step: 6, time_options: 1, unitScale: 13 },
96
108
  "community/prometheus-2-stats.json": { metric: 3, now: 1, step: 18 },
@@ -405,6 +405,7 @@ export function generatePlanModules(plan: Plan): {
405
405
  classRef = className;
406
406
  }
407
407
  mod.declarables++;
408
+ const typed = d.typeArguments && d.typeArguments.length > 0 ? `${classRef}<${d.typeArguments.join(", ")}>` : classRef;
408
409
  const entries: Array<[string, string]> = [];
409
410
  for (const [key, raw] of Object.entries(d.props ?? {})) {
410
411
  if (raw === undefined) continue;
@@ -415,7 +416,7 @@ export function generatePlanModules(plan: Plan): {
415
416
  }
416
417
  mod.types.add("PropsOf");
417
418
  const cn = constName(`${name}${pascal(key) || "Value"}`);
418
- const head = `const ${cn}: PropsOf<typeof ${classRef}>[${JSON.stringify(key)}] = `;
419
+ const head = `const ${cn}: PropsOf<typeof ${typed}>[${JSON.stringify(key)}] = `;
419
420
  lines.push(`${head}${tsLiteral(value, 0, head.length)};`);
420
421
  entries.push([key, cn]);
421
422
  }
@@ -8,13 +8,19 @@
8
8
  * in `panels.ts` is used by the importer with no change here. Any other
9
9
  * type is declared in the imported source with `definePanel`.
10
10
  * - Query types (#2951): the same, through `registeredQueries()`, keyed by
11
- * datasource plugin type; any other type gets a `defineQuery`.
11
+ * datasource plugin type or one of its `aliases` (`postgres`); any other
12
+ * type gets a `defineQuery`.
13
+ * - Datasource links in a provisioning file (#2951): `JSONDATA_LINKS` in
14
+ * `../datasource-settings.ts` lists the `jsonData` fields that hold another
15
+ * datasource's uid and the plugin types each takes; the parser turns such a
16
+ * uid into a reference. Add an entry with the `LinkedDatasource` field.
12
17
  * - Variable types (#2952): `VARIABLE_MAPPINGS` has one entry per type the
13
- * lexicon has a class for. A type with no entry (adhoc, groupby, switch)
14
- * is reported and left out. Add an entry when the class lands.
15
- * - Dashboard and panel fields (#2953 annotations, and any new prop):
16
- * `DASHBOARD_FIELDS`, `PANEL_FIELDS` and `ROW_FIELDS` list the JSON keys
17
- * copied onto a prop as they are. A key in none of the tables, and not
18
+ * lexicon has a class for. A type with no entry (`system`, `snapshot`,
19
+ * which Grafana does not save) is reported and left out.
20
+ * - Dashboard and panel fields (any new prop): `DASHBOARD_FIELDS`,
21
+ * `PANEL_FIELDS` and `ROW_FIELDS` list the JSON keys copied onto a prop as
22
+ * they are. Annotations (#2953) are read by the parser's `annotations()`,
23
+ * which resolves their datasources. A key in none of the tables, and not
18
24
  * handled by the parser, is reported and left out, unless it is at the
19
25
  * value Grafana assumes when it is missing (see `./normalize.ts`).
20
26
  */
@@ -22,7 +28,7 @@
22
28
  import { registeredPanels, type PanelDefinition } from "../panels";
23
29
  import { registeredQueries, type QueryDefinition } from "../query";
24
30
  import type { VariableHide } from "../variables";
25
- import { splitValues } from "./normalize";
31
+ import { deepEqual, splitValues } from "./normalize";
26
32
 
27
33
  type Json = Record<string, unknown>;
28
34
 
@@ -96,7 +102,7 @@ export function builtinPanelFor(type: string): PanelDefinition | undefined {
96
102
 
97
103
  /** The class a query to a datasource of this plugin type is declared with, when chant ships one. */
98
104
  export function builtinQueryFor(datasourceType: string): QueryDefinition | undefined {
99
- return registeredQueries().find((d) => d.builtin && d.datasourceType === datasourceType);
105
+ return registeredQueries().find((d) => d.builtin && (d.datasourceType === datasourceType || d.aliases?.includes(datasourceType)));
100
106
  }
101
107
 
102
108
  /**
@@ -166,29 +172,15 @@ export const VARIABLE_MAPPINGS: Readonly<Record<string, VariableMapping>> = {
166
172
  keys: ["datasource", "query", "definition", "regex", "refresh", "sort", "options", "current", ...MULTI_KEYS],
167
173
  convert(ctx) {
168
174
  const { json } = ctx;
169
- let query = str(json.query);
170
- if (query === undefined && json.query !== null && typeof json.query === "object") {
171
- const obj = json.query as Json;
172
- query = str(obj.query);
173
- if (query === undefined) {
174
- ctx.dropVariable("its query is an object with no query string, which QueryVariable cannot hold yet (#2952)");
175
- return undefined;
176
- }
177
- // refId is the editor's own id for the query, which Grafana gives the string form back.
178
- const rest = Object.keys(obj).filter((k) => k !== "query" && k !== "refId");
179
- ctx.replace(
180
- "query",
181
- query,
182
- rest.length === 0 ? undefined : `is an object; it is written as its query string, and its ${rest.join(" and ")} ${rest.length === 1 ? "is" : "are"} not carried (#2952)`,
183
- );
184
- }
185
- query ??= "";
186
- if (nonEmpty(json.definition) && json.definition !== query) ctx.drop("definition", `(it differs from the query, which is kept)`);
175
+ // The object form (Prometheus writes `{ qryType, query, refId }`) is carried as it is: the datasource reads all of it.
176
+ const query: string | Json = json.query !== null && typeof json.query === "object" && !Array.isArray(json.query) ? (json.query as Json) : (str(json.query) ?? "");
177
+ const text = typeof query === "string" ? query : str(query.query);
187
178
  const refresh = typeof json.refresh === "number" ? json.refresh : 1;
188
179
  if (refresh === 0 && nonEmpty(json.options)) {
189
180
  ctx.drop("options", "(QueryVariable does not store options; with refresh never, Grafana shows none until it is refreshed)");
190
181
  }
191
182
  const out: Json = { datasource: ctx.datasource(json.datasource, `${ctx.path}/datasource`), query };
183
+ if (nonEmpty(json.definition) && json.definition !== text) out.definition = json.definition;
192
184
  if (out.datasource === undefined) {
193
185
  ctx.dropVariable("it has no datasource chant can refer to, and QueryVariable needs one");
194
186
  return undefined;
@@ -258,7 +250,87 @@ export const VARIABLE_MAPPINGS: Readonly<Record<string, VariableMapping>> = {
258
250
  return value === "" ? {} : { value };
259
251
  },
260
252
  },
253
+ adhoc: {
254
+ className: "AdhocVariable",
255
+ keys: ["datasource", "filters", "baseFilters", "defaultKeys", "allowCustomValue", "enableGroupBy", "current", "options"],
256
+ convert(ctx) {
257
+ const { json } = ctx;
258
+ const out: Json = { datasource: ctx.datasource(json.datasource, `${ctx.path}/datasource`) };
259
+ if (out.datasource === undefined) {
260
+ ctx.dropVariable("it has no datasource chant can refer to, and AdhocVariable needs one");
261
+ return undefined;
262
+ }
263
+ if (filters(json.filters)) out.filters = json.filters;
264
+ if (filters(json.baseFilters)) out.baseFilters = json.baseFilters;
265
+ if (Array.isArray(json.defaultKeys)) out.defaultKeys = json.defaultKeys;
266
+ if (json.allowCustomValue === false) out.allowCustomValue = false;
267
+ if (json.enableGroupBy === true) out.enableGroupBy = true;
268
+ return out;
269
+ },
270
+ },
271
+ groupby: {
272
+ className: "GroupByVariable",
273
+ // A group by variable is always multi-value; Grafana reads no `multi` on it.
274
+ keys: ["datasource", "options", "current", "defaultValue", "allowCustomValue", "multi"],
275
+ convert(ctx) {
276
+ const { json } = ctx;
277
+ const out: Json = { datasource: ctx.datasource(json.datasource, `${ctx.path}/datasource`) };
278
+ if (out.datasource === undefined) {
279
+ ctx.dropVariable("it has no datasource chant can refer to, and GroupByVariable needs one");
280
+ return undefined;
281
+ }
282
+ if (Array.isArray(json.options) && json.options.length > 0) out.options = json.options.map(keyOption);
283
+ const def = json.defaultValue as Json | undefined;
284
+ if (nonEmpty(def)) out.defaultValue = Array.isArray(def!.value) && deepEqual(def!.text, def!.value) ? def!.value : def;
285
+ // The build writes the default as the current selection when there is no other.
286
+ if (nonEmpty(json.current) && !deepEqual(json.current, def)) out.current = json.current;
287
+ if (json.allowCustomValue === false) out.allowCustomValue = false;
288
+ return out;
289
+ },
290
+ },
291
+ switch: {
292
+ className: "SwitchVariable",
293
+ // Grafana reads no `query` on a switch (the v2 down-conversion writes an empty one).
294
+ keys: ["current", "options", "query"],
295
+ convert(ctx) {
296
+ const { json } = ctx;
297
+ const options = Array.isArray(json.options) ? json.options : [];
298
+ // Grafana keeps the enabled value first and the disabled value second.
299
+ const on = firstValue(options[0]) ?? "true";
300
+ const off = firstValue(options[1]) ?? "false";
301
+ const out: Json = {};
302
+ if (on !== "true") out.enabledValue = on;
303
+ if (off !== "false") out.disabledValue = off;
304
+ const value = firstValue(json.current);
305
+ if (value === on) out.enabled = true;
306
+ else if (value !== off) {
307
+ ctx.replace("current", { text: off, value: off }, `is ${value === undefined ? "missing" : `"${value}", neither the enabled nor the disabled value`}; the switch starts off`);
308
+ }
309
+ return out;
310
+ },
311
+ },
261
312
  };
262
313
 
314
+ /** An ad hoc filter list, or undefined when it is empty (the build writes `[]`). */
315
+ function filters(v: unknown): unknown[] | undefined {
316
+ return Array.isArray(v) && v.length > 0 ? v : undefined;
317
+ }
318
+
319
+ /** A `{ text, value }` option as the shortest prop that builds back to it: the value when the text is the same. */
320
+ function keyOption(o: unknown): unknown {
321
+ if (o && typeof o === "object" && !Array.isArray(o)) {
322
+ const { text, value, ...rest } = o as Json;
323
+ if (Object.keys(rest).length === 0 && typeof value === "string" && text === value) return value;
324
+ }
325
+ return o;
326
+ }
327
+
328
+ /** The first value of a `VariableOption`'s value, which may be a list. */
329
+ function firstValue(o: unknown): string | undefined {
330
+ if (!o || typeof o !== "object") return undefined;
331
+ const v = (o as Json).value;
332
+ return typeof v === "string" ? v : Array.isArray(v) && typeof v[0] === "string" ? v[0] : undefined;
333
+ }
334
+
263
335
  /** Keys every variable type reads: the ones `CommonVariableProps` has, and `type`. */
264
336
  export const COMMON_VARIABLE_KEYS: readonly string[] = ["type", "name", "label", "description", "hide", "skipUrlSync"];
@@ -59,6 +59,13 @@ export interface Declaration {
59
59
  readonly className?: string;
60
60
  /** For `new`: the id of the `CustomClass` declaring `className`, when it is not a package export. */
61
61
  readonly customClass?: string;
62
+ /**
63
+ * For `new`: type arguments for the class, as TypeScript source. The
64
+ * consts lifted out of the props are typed `PropsOf<typeof Class<...>>`,
65
+ * so a `Datasource<"prometheus">`'s `jsonData` const is typed as
66
+ * Prometheus settings rather than any plugin's.
67
+ */
68
+ readonly typeArguments?: readonly string[];
62
69
  /** For `new`: the constructor's props, in source order. */
63
70
  readonly props?: Record<string, unknown>;
64
71
  /** For `value`: the value, and its type annotation with the package types it names. */
@@ -89,6 +89,7 @@ export const VARIABLE_DEFAULTS: Readonly<Json> = {
89
89
  /** Per variable type, Grafana's value for a key the JSON leaves out, over `VARIABLE_DEFAULTS`. */
90
90
  export const VARIABLE_TYPE_DEFAULTS: Readonly<Record<string, Json>> = {
91
91
  query: { refresh: 1 },
92
+ adhoc: { filters: [], baseFilters: [], enableGroupBy: false },
92
93
  datasource: { refresh: 1 },
93
94
  interval: { refresh: 2, auto: false, auto_count: 30, auto_min: "10s" },
94
95
  };
@@ -264,12 +265,40 @@ function normalizeVariable(variable: unknown): unknown {
264
265
  else if (out.datasource !== undefined) out.datasource = canonicalRef(out.datasource);
265
266
  const query = typeof out.query === "string" ? out.query : undefined;
266
267
  switch (type) {
267
- case "query":
268
- // `definition` is the query as the editor shows it; Grafana reads `query`.
269
- if (out.definition === undefined || out.definition === "" || (query !== undefined && out.definition === query)) delete out.definition;
268
+ case "query": {
269
+ // `definition` is the query as the editor shows it; Grafana reads `query` (an object query's text is its `query`).
270
+ const text = query ?? (isObject(out.query) && typeof out.query.query === "string" ? out.query.query : undefined);
271
+ if (out.definition === undefined || out.definition === "" || (text !== undefined && out.definition === text)) delete out.definition;
270
272
  // Grafana runs the query again when the dashboard loads (or the time range changes).
271
273
  if (out.refresh !== 0) delete out.options;
272
274
  break;
275
+ }
276
+ case "adhoc":
277
+ // The filters are the selection; Grafana reads neither of these for an ad hoc variable.
278
+ delete out.current;
279
+ delete out.options;
280
+ break;
281
+ case "groupby": {
282
+ // With nothing selected, Grafana starts from the default.
283
+ const cur = isObject(out.current) ? out.current : undefined;
284
+ const empty = !cur || Object.keys(cur).length === 0 || (Array.isArray(cur.value) && cur.value.length === 0);
285
+ if (empty || deepEqual(cur, out.defaultValue)) delete out.current;
286
+ if (Array.isArray(out.options) && out.options.length === 0) delete out.options;
287
+ // Always multi-value.
288
+ delete out.multi;
289
+ break;
290
+ }
291
+ case "switch": {
292
+ // Grafana reads only the values: the enabled one first, the disabled one second, and the current one.
293
+ const value = (o: unknown): unknown => (isObject(o) ? (Array.isArray(o.value) ? o.value[0] : o.value) : undefined);
294
+ const opts = Array.isArray(out.options) ? out.options : [];
295
+ const on = value(opts[0]) ?? "true";
296
+ const off = value(opts[1]) ?? "false";
297
+ out.options = [on, off];
298
+ out.current = value(out.current) ?? off;
299
+ delete out.query;
300
+ break;
301
+ }
273
302
  case "custom":
274
303
  case "interval": {
275
304
  const values = splitValues(query ?? "");
@@ -233,26 +233,84 @@ describe("variables", () => {
233
233
  expect(p.declarations.filter((d) => d.module === "variables").map((d) => d.id)[0]).toBe("variable:ds");
234
234
  });
235
235
 
236
- test("a type the lexicon has no class for is reported and left out (#2952)", () => {
237
- const { plan: p, warnings, edits } = plan([{ type: "adhoc", name: "Filters", datasource: prom, filters: [] }]);
236
+ test("a type the lexicon has no class for is reported and left out", () => {
237
+ const { plan: p, warnings, edits } = plan([{ type: "system", name: "__org", query: "" }]);
238
238
  expect(p.declarations.filter((d) => d.module === "variables")).toEqual([]);
239
- expect(warnings).toEqual(['variable "Filters" (adhoc) is not carried: chant has no adhoc variable yet (#2952), so it is left out']);
239
+ expect(warnings).toEqual(['variable "__org" (system) is not carried: chant has no system variable, so it is left out']);
240
240
  expect(edits).toContainEqual({ op: "remove", path: "/templating/list/0" });
241
241
  });
242
242
 
243
- test("an object query is written as its query string; keys besides refId are named", () => {
243
+ test("ad hoc, group by and switch variables", () => {
244
244
  const { plan: p, warnings, edits } = plan([
245
- { type: "query", name: "job", datasource: prom, query: { qryType: 1, query: "label_values(job)", refId: "PrometheusVariableQueryEditor-VariableQuery" } },
245
+ {
246
+ type: "adhoc",
247
+ name: "Filters",
248
+ datasource: prom,
249
+ filters: [{ key: "namespace", operator: "=", value: "shop" }],
250
+ baseFilters: [],
251
+ defaultKeys: [{ text: "pod", value: "pod" }],
252
+ allowCustomValue: false,
253
+ enableGroupBy: false,
254
+ },
255
+ {
256
+ type: "groupby",
257
+ name: "by",
258
+ datasource: prom,
259
+ options: [{ text: "pod", value: "pod" }, { text: "Node", value: "node" }],
260
+ current: { text: ["node"], value: ["node"] },
261
+ defaultValue: { text: ["pod"], value: ["pod"] },
262
+ },
263
+ { type: "switch", name: "on", current: { text: "true", value: "true" }, options: [{ text: "true", value: "true" }, { text: "false", value: "false" }] },
264
+ { type: "switch", name: "q", current: { text: "0.5", value: "0.5" }, options: [{ text: "0.99", value: "0.99" }, { text: "0.5", value: "0.5" }] },
246
265
  ]);
247
- expect(decl(p.declarations, "variable:job").props!.query).toBe("label_values(job)");
248
- expect(edits).toContainEqual({ op: "replace", path: "/templating/list/0/query", value: "label_values(job)" });
249
- expect(warnings).toEqual(['variable "job": query is an object; it is written as its query string, and its qryType is not carried (#2952)']);
250
- expect(plan([{ type: "query", name: "j", datasource: prom, query: { query: "x", refId: "r" } }]).warnings).toEqual([]);
266
+ expect(warnings).toEqual([]);
267
+ expect(edits.filter((e) => "path" in e && e.path.startsWith("/templating"))).toEqual([]);
268
+ const props = (id: string) => [decl(p.declarations, id).className, decl(p.declarations, id).props];
269
+ expect(props("variable:Filters")).toEqual([
270
+ "AdhocVariable",
271
+ {
272
+ name: "Filters",
273
+ datasource: { $decl: "datasource:prometheus:prom" },
274
+ filters: [{ key: "namespace", operator: "=", value: "shop" }],
275
+ defaultKeys: [{ text: "pod", value: "pod" }],
276
+ allowCustomValue: false,
277
+ },
278
+ ]);
279
+ expect(props("variable:by")).toEqual([
280
+ "GroupByVariable",
281
+ {
282
+ name: "by",
283
+ datasource: { $decl: "datasource:prometheus:prom" },
284
+ options: ["pod", { text: "Node", value: "node" }],
285
+ defaultValue: ["pod"],
286
+ current: { text: ["node"], value: ["node"] },
287
+ },
288
+ ]);
289
+ expect(props("variable:on")).toEqual(["SwitchVariable", { name: "on", enabled: true }]);
290
+ expect(props("variable:q")).toEqual(["SwitchVariable", { name: "q", enabledValue: "0.99", disabledValue: "0.5" }]);
251
291
  });
252
292
 
253
- test("an object query with no query string cannot be carried", () => {
254
- const { warnings } = plan([{ type: "query", name: "ns", datasource: { type: "cloudwatch", uid: "cw" }, query: { namespace: "AWS/EC2" } }]);
255
- expect(warnings).toEqual([expect.stringMatching(/^variable "ns" is not carried: its query is an object with no query string/)]);
293
+ test("a switch whose current value is neither of its values starts off, and says so", () => {
294
+ const { warnings, edits } = plan([{ type: "switch", name: "s", current: { text: "maybe", value: "maybe" }, options: [{ text: "true", value: "true" }, { text: "false", value: "false" }] }]);
295
+ expect(warnings).toEqual(['variable "s": current is "maybe", neither the enabled nor the disabled value; the switch starts off']);
296
+ expect(edits).toContainEqual({ op: "replace", path: "/templating/list/0/current", value: { text: "false", value: "false" } });
297
+ });
298
+
299
+ test("an ad hoc or group by variable with no datasource chant can refer to is left out", () => {
300
+ const { warnings } = plan([{ type: "adhoc", name: "f", datasource: { type: "prometheus" }, filters: [] }]);
301
+ expect(warnings).toContainEqual(expect.stringMatching(/^variable "f" is not carried: it has no datasource chant can refer to, and AdhocVariable needs one/));
302
+ });
303
+
304
+ test("an object query is carried as it is, with a definition that differs from its text", () => {
305
+ const query = { qryType: 1, query: "label_values(job)", refId: "PrometheusVariableQueryEditor-VariableQuery" };
306
+ const { plan: p, warnings, edits } = plan([
307
+ { type: "query", name: "job", datasource: prom, query, definition: "label_values(job)" },
308
+ { type: "query", name: "ns", datasource: { type: "cloudwatch", uid: "cw" }, query: { namespace: "AWS/EC2" }, definition: "Namespaces" },
309
+ ]);
310
+ expect(warnings).toEqual([]);
311
+ expect(edits.filter((e) => "path" in e && e.path.startsWith("/templating"))).toEqual([]);
312
+ expect(decl(p.declarations, "variable:job").props).toEqual({ name: "job", datasource: { $decl: "datasource:prometheus:prom" }, query });
313
+ expect(decl(p.declarations, "variable:ns").props).toMatchObject({ query: { namespace: "AWS/EC2" }, definition: "Namespaces" });
256
314
  });
257
315
 
258
316
  test("keys at Grafana's default are dropped quietly; others are named", () => {
@@ -387,26 +445,44 @@ describe("the dashboard", () => {
387
445
  expect(warnings).toContain('dashboard: uid is missing, so the dashboard is given the uid "my-board", from its title');
388
446
  });
389
447
 
390
- test("the built-in annotation is Grafana's default; any other is named (#2953)", () => {
448
+ test("the built-in annotation is Grafana's default and left out; any other is carried (#2953)", () => {
391
449
  const builtin = { builtIn: 1, datasource: { type: "datasource", uid: "grafana" }, enable: true, hide: true, iconColor: "rgba(0, 211, 255, 1)", name: "Annotations & Alerts", type: "dashboard" };
392
- expect(planDashboard(dashboard({ annotations: { list: [builtin] } })).warnings).toEqual([]);
393
- const other = planDashboard(dashboard({ annotations: { list: [builtin, { name: "Deploys", enable: true }] } }));
394
- expect(other.warnings).toEqual(['dashboard has the annotation "Deploys", which is not carried: Dashboard has no annotations yet (#2953)']);
395
- expect(other.edits).toContainEqual({ op: "remove", path: "/annotations" });
450
+ const only = planDashboard(dashboard({ annotations: { list: [builtin] } }));
451
+ expect(only.warnings).toEqual([]);
452
+ expect(only.plan.declarations.find((d) => d.id === "dashboard")!.props!.annotations).toBeUndefined();
453
+ const deploys = { name: "Deploys", datasource: { type: "prometheus", uid: "prom" }, iconColor: "red", expr: "changes(up[5m]) > 0", step: "60s", target: { refId: "Anno" } };
454
+ const other = planDashboard(dashboard({ annotations: { list: [builtin, deploys, { name: "Off", enable: false, iconColor: "blue", hide: null }] } }));
455
+ expect(other.warnings).toEqual(['annotation "Deploys": enable is missing, so it is written as true, which runs the query']);
456
+ const props = other.plan.declarations.find((d) => d.id === "dashboard")!.props!;
457
+ expect(props.annotations).toEqual([
458
+ { name: "Deploys", datasource: { $decl: "datasource:prometheus:prom" }, iconColor: "red", expr: "changes(up[5m]) > 0", step: "60s", target: { refId: "Anno" } },
459
+ { name: "Off", enable: false, iconColor: "blue" },
460
+ ]);
461
+ expect(other.plan.declarations).toContainEqual(expect.objectContaining({ id: "datasource:prometheus:prom", className: "ExternalDatasource" }));
462
+ expect(other.edits).toContainEqual({ op: "replace", path: "/annotations/list/1/enable", value: true });
463
+ expect(other.edits).toContainEqual({ op: "remove", path: "/annotations/list/2/hide" });
396
464
  });
397
465
  });
398
466
 
399
467
  describe("provisioning files", () => {
400
468
  test("a datasource provisioning file: one Datasource each, unknown keys named", () => {
401
- const ir = parse("apiVersion: 1\nprune: true\ndatasources:\n - name: Prometheus\n type: prometheus\n url: http://prom:9090\n password: x\n");
469
+ const ir = parse("apiVersion: 1\nprune: true\ndatasources:\n - name: Prometheus\n type: prometheus\n url: http://prom:9090\n password: x\nextra: 1\n");
402
470
  const { plan } = ir.resources[0].properties as unknown as PlanResourceProperties;
403
471
  expect(plan.declarations).toEqual([
404
472
  expect.objectContaining({ className: "Datasource", props: { name: "Prometheus", type: "prometheus", url: "http://prom:9090" } }),
405
473
  ]);
406
- expect(ir.warnings).toEqual([
407
- 'datasource "Prometheus": password is not carried (no prop takes it)',
408
- "the provisioning file: prune is not carried (chant writes datasources only; prune and deleteDatasources are #2953)",
474
+ expect(ir.warnings).toEqual(['datasource "Prometheus": password is not carried (no prop takes it)', "the provisioning file: extra is not carried (no prop takes it)"]);
475
+ });
476
+
477
+ test("a datasource provisioning file's prune and deleteDatasources become a DatasourceProvisioning (#2953)", () => {
478
+ const pruned = parse("apiVersion: 1\nprune: true\ndatasources: []\n").resources[0].properties as unknown as PlanResourceProperties;
479
+ expect(pruned.plan.declarations).toEqual([]);
480
+ const kept = parse("apiVersion: 1\ndeleteDatasources:\n - name: Old\n orgId: 2\n - name: Gone\ndatasources: []\n");
481
+ const { plan } = kept.resources[0].properties as unknown as PlanResourceProperties;
482
+ expect(plan.declarations).toEqual([
483
+ expect.objectContaining({ className: "DatasourceProvisioning", props: { prune: false, deleteDatasources: [{ name: "Old", orgId: 2 }, { name: "Gone" }] } }),
409
484
  ]);
485
+ expect(kept.warnings).toEqual([]);
410
486
  });
411
487
 
412
488
  test("a dashboard provisioning file: file providers only", () => {