@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
@@ -2,6 +2,12 @@
2
2
  * Read the vendored Grafana schemas (`src/spec/schemas/*.jsonschema.json`), check
3
3
  * them against the digests in `GRAFANA_SCHEMA_PIN`, and apply the correction
4
4
  * overlay (`src/spec/overlay/`, see `./overlay.ts`) on top.
5
+ *
6
+ * This is the source side, for `npm run generate`, the lexicon's own
7
+ * validate step, the importer's tests and the fetch scripts: it reads files
8
+ * next to this module. Nothing a build or lint runs imports it. Validation
9
+ * reads the generated `./schemas.gen.ts`, which generate writes from
10
+ * {@link loadSchema}.
5
11
  */
6
12
 
7
13
  import { createHash } from "crypto";
@@ -11,12 +17,7 @@ import { fileURLToPath } from "url";
11
17
  import { GRAFANA_SCHEMA_PIN, VENDORED_SCHEMA_NAMES, type SchemaName, type VendoredSchemaName } from "../pin";
12
18
  import { applyOverlay, loadOverlay } from "./overlay";
13
19
 
14
- /**
15
- * Where the vendored files live: beside this module, under `src/`, so they
16
- * ship with the source the package runs from and GRAF107 can read them.
17
- * A function, not a module-scope constant, so edge bundles that import the
18
- * lexicon never evaluate a filesystem path at load.
19
- */
20
+ /** Where the vendored files live: beside this module, under `src/`. */
20
21
  export function schemasDir(): string {
21
22
  return join(dirname(fileURLToPath(import.meta.url)), "schemas");
22
23
  }
@@ -43,7 +44,8 @@ const parsed = new Map<SchemaName, Record<string, unknown>>();
43
44
 
44
45
  /**
45
46
  * One schema as the lexicon uses it: the vendored file with its overlay
46
- * applied (cached per process). Types, GRAF107 and validation all read this.
47
+ * applied (cached per process). Generate writes the types and
48
+ * `./schemas.gen.ts` from this, so GRAF107 sees the same schema.
47
49
  */
48
50
  export function loadSchema(name: SchemaName): Record<string, unknown> {
49
51
  let schema = parsed.get(name);
@@ -26,7 +26,7 @@ import { parseMatchers } from "@intentius/chant-lexicon-prometheus/matchers";
26
26
  import { EXPRESSION_DATASOURCE_UID } from "./alerting";
27
27
  import type { KnownDatasource } from "./datasource-refs";
28
28
  import { isValidUid } from "./util";
29
- import { validateExpressionSchema } from "./schema-validate";
29
+ import { schemaValidationUnavailable, validateExpressionSchema } from "./schema-validate";
30
30
  import { checkGrafanaPromql } from "./promql-check";
31
31
 
32
32
  type Json = Record<string, unknown>;
@@ -152,7 +152,9 @@ export function checkRuleQueries(docs: readonly AlertingDoc[]): AlertingIssue[]
152
152
  if (q.datasourceUid !== EXPRESSION_DATASOURCE_UID) continue;
153
153
  const model = modelOf(q);
154
154
  const at = `expression ${String(q.refId ?? "?")}`;
155
- for (const p of validateExpressionSchema(model)) {
155
+ const unavailable = schemaValidationUnavailable();
156
+ if (unavailable) push(`${at} was not checked against the Grafana expression schema: ${unavailable}.`, "warning");
157
+ for (const p of unavailable ? [] : validateExpressionSchema(model)) {
156
158
  if (p.severity === "error") push(`${at} ${p.path === "/" ? "" : `${p.path.slice(1)} `}${p.message} (Grafana expression schema).`);
157
159
  }
158
160
  for (const input of expressionInputs(model)) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The checks behind GRAF101-GRAF109, GRAF111-GRAF114 and GRAF115, as plain
2
+ * The checks behind GRAF101-GRAF110, GRAF111-GRAF114 and GRAF115, as plain
3
3
  * functions over built Grafana output: dashboard JSON documents, provisioned
4
4
  * datasources, dashboard providers and alerting provisioning files (the
5
5
  * alerting checks are in `validate-alerting.ts`). The
@@ -17,6 +17,7 @@
17
17
  import type { ExternalDatasourceRecord, ProvisionedDatasource } from "./build";
18
18
  import { DASHBOARDS_DIR, GRID_COLUMNS } from "./build";
19
19
  import {
20
+ annotationsOf,
20
21
  datasourceUses,
21
22
  describePanel,
22
23
  knownDatasources,
@@ -26,9 +27,9 @@ import {
26
27
  type DatasourceRefJson,
27
28
  type KnownDatasource,
28
29
  } from "./datasource-refs";
29
- import { isBuiltinVariable } from "./variables";
30
+ import { isBuiltinVariable, MULTI_VALUE_KINDS } from "./variables";
30
31
  import { isValidUid } from "./util";
31
- import { validateDashboardSchema } from "./schema-validate";
32
+ import { schemaValidationUnavailable, validateDashboardSchema } from "./schema-validate";
32
33
  import { checkGrafanaPromql, prometheusQueries } from "./promql-check";
33
34
  import { checkAlertingIdentity, checkNotificationRefs, checkRuleDatasources, checkRulePromql, checkRuleQueries, type AlertingDoc } from "./validate-alerting";
34
35
  import { closestGrafanaUnit, isGrafanaUnit } from "./spec/units";
@@ -43,6 +44,7 @@ export type GrafanaIssueCode =
43
44
  | "GRAF107"
44
45
  | "GRAF108"
45
46
  | "GRAF109"
47
+ | "GRAF110"
46
48
  | "GRAF111"
47
49
  | "GRAF112"
48
50
  | "GRAF113"
@@ -73,6 +75,10 @@ export interface GrafanaArtifacts {
73
75
  providers?: Array<Record<string, unknown>>;
74
76
  /** Alerting provisioning files (rule groups, contact points, policies, mute timings, templates). */
75
77
  alerting?: AlertingDoc[];
78
+ /** The folders the build resolved (its index's `folders`): each level of each dashboard's folder path, and every `Folder`. */
79
+ folders?: Array<{ uid: string; title: string; path?: string; parentUid?: string }>;
80
+ /** `deleteDatasources` entries of datasource provisioning files. */
81
+ deleteDatasources?: Array<{ name: string; orgId?: number }>;
76
82
  }
77
83
 
78
84
  export type { AlertingDoc };
@@ -180,6 +186,19 @@ export function checkDatasourceRefs(a: GrafanaArtifacts): GrafanaIssue[] {
180
186
  }
181
187
  }
182
188
 
189
+ // An ExternalDatasource the provisioning file deletes (and does not provision again) is gone once Grafana provisions it.
190
+ const deleted = new Set((a.deleteDatasources ?? []).filter((x) => (x.orgId ?? 1) === 1).map((x) => x.name));
191
+ const provisioned = new Set(a.datasources.map((x) => x.name));
192
+ const gone = new Set([...known.values()].filter((k) => k.external && k.name !== undefined && deleted.has(k.name) && !provisioned.has(k.name)).map((k) => k.uid));
193
+ for (const { where, resolved } of uses) {
194
+ if (resolved.kind !== "declared" || !gone.has(resolved.datasource.uid)) continue;
195
+ push(
196
+ "GRAF101",
197
+ "error",
198
+ `${where} uses external datasource "${resolved.datasource.name}", which the datasource provisioning file deletes (deleteDatasources); Grafana removes it before the dashboard can use it.`,
199
+ );
200
+ }
201
+
183
202
  // A datasource variable offers the datasources of its plugin type; with none declared it has nothing to choose.
184
203
  if (known.size > 0) {
185
204
  for (const v of dsVariables) {
@@ -225,10 +244,22 @@ export function checkVariables(a: GrafanaArtifacts): GrafanaIssue[] {
225
244
  }
226
245
  }
227
246
  for (const v of variablesOf(d)) {
247
+ const ref = v.datasource as DatasourceRefJson | undefined;
248
+ if (ref && typeof ref === "object" && isVariableUid(ref.uid)) {
249
+ for (const n of variableReferences(ref.uid!)) if (n !== v.name) report(n, `variable "${String(v.name)}" datasource`);
250
+ }
228
251
  if (v.type !== "query") continue;
229
- const names = new Set(stringsIn(v.query, new Set()).flatMap(variableReferences));
252
+ const names = new Set(stringsIn(v.query, new Set(["refId"])).flatMap(variableReferences));
230
253
  for (const n of names) if (n !== v.name) report(n, `variable "${String(v.name)}" query`);
231
254
  }
255
+ for (const an of annotationsOf(d)) {
256
+ const where = `annotation "${String(an.name ?? "?")}"`;
257
+ const ref = an.datasource as DatasourceRefJson | undefined;
258
+ if (ref && typeof ref === "object" && isVariableUid(ref.uid)) for (const n of variableReferences(ref.uid!)) report(n, `${where} datasource`);
259
+ // Grafana interpolates the query (`target`, or Prometheus's legacy `expr`), not the name, colour or title formats.
260
+ const names = new Set(stringsIn([an.target, an.expr], TARGET_SKIP).flatMap(variableReferences));
261
+ for (const n of names) report(n, `${where} query`);
262
+ }
232
263
  }
233
264
  return issues;
234
265
  }
@@ -253,6 +284,10 @@ export function checkDuplicates(a: GrafanaArtifacts): GrafanaIssue[] {
253
284
  const allUids = [...a.datasources.map((d) => d.uid), ...(a.externalDatasources ?? []).map((d) => d.uid)];
254
285
  for (const [uid, n] of dupes(allUids, (u) => u)) push(`${n} datasources share the uid "${uid}".`, uid);
255
286
  for (const [name, n] of dupes(a.datasources, (d) => d.name)) push(`${n} datasources share the name "${name}"; Grafana needs names to be unique.`, name);
287
+ for (const [uid, n] of dupes(a.folders ?? [], (f) => f.uid)) {
288
+ const paths = (a.folders ?? []).filter((f) => f.uid === uid).map((f) => `"${f.path ?? f.title}"`);
289
+ push(`${n} folders share the uid "${uid}" (${paths.join(", ")}); Grafana keeps one folder per uid, so the API applier refuses the build. Give one a Folder with its own uid.`, uid);
290
+ }
256
291
  for (const { json: d } of a.dashboards) {
257
292
  const uid = String(d.uid ?? "");
258
293
  const panels = panelsOf(d).map((p) => p.panel);
@@ -355,6 +390,19 @@ export function checkIdentity(a: GrafanaArtifacts): GrafanaIssue[] {
355
390
  });
356
391
  }
357
392
  }
393
+ for (const f of a.folders ?? []) {
394
+ const where = `Folder "${f.path ?? f.title}"`;
395
+ if (!isValidUid(f.uid)) {
396
+ issues.push({ code: "GRAF106", severity: "error", message: `${where} has uid "${f.uid}"; Grafana accepts 1-40 letters, digits, "-" and "_".`, entity: f.uid });
397
+ } else if (f.uid === "general") {
398
+ issues.push({
399
+ code: "GRAF106",
400
+ severity: "error",
401
+ message: `${where} has uid "general", which Grafana keeps for its root (the General folder) and refuses for a folder. Leave folder out to put a dashboard in General, or give the folder another uid.`,
402
+ entity: f.uid,
403
+ });
404
+ }
405
+ }
358
406
  for (const ds of a.externalDatasources ?? []) {
359
407
  if (!isValidUid(ds.uid)) {
360
408
  issues.push({
@@ -377,6 +425,11 @@ export function checkIdentity(a: GrafanaArtifacts): GrafanaIssue[] {
377
425
  */
378
426
  export function checkSchema(a: GrafanaArtifacts): GrafanaIssue[] {
379
427
  const issues: GrafanaIssue[] = [];
428
+ if (a.dashboards.length === 0) return issues;
429
+ const unavailable = schemaValidationUnavailable();
430
+ if (unavailable) {
431
+ return [{ code: "GRAF107", severity: "warning", message: `Dashboards were not checked against the Grafana schema: ${unavailable}.` }];
432
+ }
380
433
  for (const { json: d } of a.dashboards) {
381
434
  for (const p of validateDashboardSchema(d)) {
382
435
  issues.push({
@@ -412,6 +465,44 @@ export function checkPromqlSyntax(a: GrafanaArtifacts): GrafanaIssue[] {
412
465
  return issues;
413
466
  }
414
467
 
468
+ // ── GRAF110: repeats ────────────────────────────────────────────
469
+
470
+ /**
471
+ * A panel or row repeated over a variable Grafana cannot repeat over, or
472
+ * one that can only ever hold one value. Grafana repeats over a query,
473
+ * custom, datasource or group by variable (a `MultiValueVariable` in
474
+ * @grafana/scenes), once per selected value. Over any other kind it logs an
475
+ * error and shows the panel once; over a query, custom or datasource
476
+ * variable with neither `multi` nor `includeAll`, there is only ever one
477
+ * value to repeat for. A repeat naming no variable is GRAF103's.
478
+ */
479
+ const KIND_NAMES: Readonly<Record<string, string>> = { adhoc: "an ad hoc", interval: "an interval", groupby: "a group by" };
480
+
481
+ export function checkRepeats(a: GrafanaArtifacts): GrafanaIssue[] {
482
+ const issues: GrafanaIssue[] = [];
483
+ for (const { json: d } of a.dashboards) {
484
+ const variables = new Map(variablesOf(d).map((v) => [String(v.name), v]));
485
+ for (const { panel } of panelsOf(d)) {
486
+ if (typeof panel.repeat !== "string" || panel.repeat === "") continue;
487
+ const v = variables.get(panel.repeat);
488
+ if (!v) continue;
489
+ const where = panel.type === "row" ? `row "${String(panel.title ?? "")}"` : describePanel(panel);
490
+ const type = String(v.type);
491
+ const kind = `${KIND_NAMES[type] ?? `a ${type}`} variable`;
492
+ let why: string | undefined;
493
+ if (!MULTI_VALUE_KINDS.has(type)) {
494
+ why = `${kind}, which Grafana cannot repeat over: it shows the ${panel.type === "row" ? "row" : "panel"} once. Repeat over a query, custom, datasource or group by variable`;
495
+ } else if (type !== "groupby" && v.multi !== true && v.includeAll !== true) {
496
+ why = `${kind} with neither multi nor includeAll, so it only ever holds one value and the ${panel.type === "row" ? "row" : "panel"} shows once. Set multi or includeAll on it`;
497
+ }
498
+ if (why) {
499
+ issues.push({ code: "GRAF110", severity: "warning", message: `${dashName(d)} ${where} repeats over $${panel.repeat}, ${why}.`, entity: String(d.uid ?? "") });
500
+ }
501
+ }
502
+ }
503
+ return issues;
504
+ }
505
+
415
506
  // ── GRAF115: units ──────────────────────────────────────────────
416
507
 
417
508
  /** Every unit a panel sets, with where: field defaults, `unit` overrides, heatmap axes and cells, and a legacy graph panel's y-axes. */
@@ -593,6 +684,7 @@ const BY_CODE: Record<GrafanaIssueCode, (a: GrafanaArtifacts) => GrafanaIssue[]>
593
684
  GRAF107: checkSchema,
594
685
  GRAF108: checkPromqlSyntax,
595
686
  GRAF109: checkProvisioning,
687
+ GRAF110: checkRepeats,
596
688
  GRAF111: (a) => checkRuleQueries(a.alerting ?? []),
597
689
  GRAF112: (a) => checkRuleDatasources(a.alerting ?? [], knownDatasourcesOf(a)),
598
690
  GRAF113: (a) => checkNotificationRefs(a.alerting ?? []),
package/src/validate.ts CHANGED
@@ -25,6 +25,8 @@ export const REQUIRED_NAMES = [
25
25
  "Datasource",
26
26
  "ExternalDatasource",
27
27
  "DashboardProvider",
28
+ "DatasourceProvisioning",
29
+ "Folder",
28
30
  "Row",
29
31
  "TimeSeriesPanel",
30
32
  "StatPanel",
@@ -50,12 +52,24 @@ export const REQUIRED_NAMES = [
50
52
  "PromQuery",
51
53
  "TempoQuery",
52
54
  "LokiQuery",
55
+ "ElasticsearchQuery",
56
+ "CloudWatchQuery",
57
+ "AzureMonitorQuery",
58
+ "CloudMonitoringQuery",
59
+ "BigQueryQuery",
60
+ "PyroscopeQuery",
61
+ "PostgresQuery",
62
+ "MySQLQuery",
63
+ "MSSQLQuery",
53
64
  "QueryVariable",
54
65
  "CustomVariable",
55
66
  "IntervalVariable",
56
67
  "DatasourceVariable",
57
68
  "ConstantVariable",
58
69
  "TextboxVariable",
70
+ "AdhocVariable",
71
+ "GroupByVariable",
72
+ "SwitchVariable",
59
73
  ];
60
74
 
61
75
  const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `@intentius/chant-lexicon-grafana/validation`: the GRAF101-GRAF115 checks
3
+ * as plain functions over built Grafana output, and the schema validation
4
+ * behind GRAF107, for code that checks dashboards without a build.
5
+ *
6
+ * These are not on the package root, so importing the lexicon to declare
7
+ * dashboards does not load them. `chant build` and `chant lint` run the same
8
+ * checks through the plugin's post-synth checks. ajv is loaded the first time
9
+ * a schema is checked, and the schemas come from a generated module rather
10
+ * than files, so this works bundled.
11
+ */
12
+
13
+ export {
14
+ validateGrafanaOutput,
15
+ issuesFor,
16
+ knownDatasourcesOf,
17
+ type GrafanaIssue,
18
+ type GrafanaIssueCode,
19
+ type GrafanaArtifacts,
20
+ type DashboardDoc,
21
+ } from "./validate-output";
22
+ export {
23
+ validateDashboardSchema,
24
+ validateExpressionSchema,
25
+ schemaValidationUnavailable,
26
+ type SchemaProblem,
27
+ } from "./schema-validate";
package/src/variables.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Dashboard variables (Grafana's "templating"): query, custom, interval,
3
- * datasource, constant and textbox.
3
+ * datasource, constant, textbox, ad hoc filters, group by and switch.
4
4
  *
5
5
  * A variable is declared on its own and listed in a dashboard's
6
6
  * `variables`. Queries reference it as `$name` or `${name}` in their
@@ -12,9 +12,12 @@
12
12
  import { createProperty } from "@intentius/chant/runtime";
13
13
  import type { Declarable } from "@intentius/chant/declarable";
14
14
  import type { DatasourceEntity, DatasourceRef, ExternalDatasourceEntity } from "./datasource";
15
- import type { VariableOption } from "./schema/dashboard.gen";
15
+ import type { AdHocFilter, VariableOption } from "./schema/dashboard.gen";
16
16
 
17
- export type VariableKind = "query" | "custom" | "interval" | "datasource" | "constant" | "textbox";
17
+ export type VariableKind = "query" | "custom" | "interval" | "datasource" | "constant" | "textbox" | "adhoc" | "groupby" | "switch";
18
+
19
+ /** The kinds Grafana can repeat a panel or row over: the ones that hold a list of values (`MultiValueVariable` in @grafana/scenes). */
20
+ export const MULTI_VALUE_KINDS: ReadonlySet<string> = new Set(["query", "custom", "datasource", "groupby"]);
18
21
 
19
22
  /** Where the variable shows: with its label, without it, or not at all. */
20
23
  export type VariableHide = "label" | "valueOnly" | "hidden";
@@ -42,10 +45,45 @@ interface MultiValueProps {
42
45
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
46
  export type VariableDatasource = DatasourceEntity<any> | ExternalDatasourceEntity<any> | DatasourceRef | DatasourceVariableEntity<any>;
44
47
 
48
+ /**
49
+ * A variable query in the object form a datasource's variable editor
50
+ * writes. Grafana passes it to the datasource as it is, so any key the
51
+ * datasource reads may be here.
52
+ */
53
+ export interface VariableQueryObject {
54
+ /** The query text, when the datasource has one (Prometheus, Loki, and most others). */
55
+ query?: string;
56
+ /** The editor's id for the query; Grafana writes e.g. `PrometheusVariableQueryEditor-VariableQuery`. */
57
+ refId?: string;
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ /**
62
+ * A Prometheus variable query as Grafana's Prometheus variable editor
63
+ * writes it. `query` is the text Grafana runs (`label_values(up, job)`);
64
+ * the other fields let the editor show the query in its form again.
65
+ */
66
+ export interface PrometheusVariableQuery extends VariableQueryObject {
67
+ query: string;
68
+ /** The editor's query type: 0 label names, 1 label values, 2 metrics, 3 query result, 4 series query, 5 classic query. */
69
+ qryType?: 0 | 1 | 2 | 3 | 4 | 5;
70
+ label?: string;
71
+ metric?: string;
72
+ seriesQuery?: string;
73
+ varQueryResult?: string;
74
+ labelFilters?: Array<{ label: string; op: string; value: string }>;
75
+ }
76
+
45
77
  export interface QueryVariableProps extends CommonVariableProps, MultiValueProps {
46
78
  datasource: VariableDatasource;
47
- /** The datasource's variable query, e.g. `label_values(up, job)` for Prometheus. */
48
- query: string;
79
+ /**
80
+ * The datasource's variable query: a string (`label_values(up, job)` for
81
+ * Prometheus), or the object the datasource's variable editor writes
82
+ * (`{ query: "label_values(up, job)", qryType: 1 }`).
83
+ */
84
+ query: string | VariableQueryObject;
85
+ /** The text Grafana shows for the query in the variable list. Defaults to the query string. */
86
+ definition?: string;
49
87
  regex?: string;
50
88
  /** When to re-run the query. Defaults to on dashboard load. */
51
89
  refresh?: "never" | "onLoad" | "onTimeRangeChange";
@@ -85,6 +123,51 @@ export interface TextboxVariableProps extends CommonVariableProps {
85
123
  value?: string;
86
124
  }
87
125
 
126
+ /** One ad hoc filter: `key operator value`, e.g. `{ key: "namespace", operator: "=", value: "shop" }`. */
127
+ export type AdhocFilter = AdHocFilter;
128
+
129
+ /** A key an ad hoc or group by variable offers, in place of asking the datasource. */
130
+ export interface VariableKeyOption {
131
+ text: string;
132
+ value?: string | number;
133
+ [key: string]: unknown;
134
+ }
135
+
136
+ export interface AdhocVariableProps extends CommonVariableProps {
137
+ /** The datasource whose queries get the filters, and which is asked for keys and values. */
138
+ datasource: VariableDatasource;
139
+ /** The filters applied when the dashboard loads. */
140
+ filters?: AdhocFilter[];
141
+ /** Filters that narrow the key and value lookups, not shown or applied to queries. */
142
+ baseFilters?: AdhocFilter[];
143
+ /** A fixed set of keys to offer instead of the datasource's. */
144
+ defaultKeys?: VariableKeyOption[];
145
+ /** Whether a key or value not in the list can be typed in. Grafana's default is true. */
146
+ allowCustomValue?: boolean;
147
+ /** Offer the group-by operator in the filter box (Grafana 13, behind the `dashboardUnifiedDrilldownControls` feature toggle). */
148
+ enableGroupBy?: boolean;
149
+ }
150
+
151
+ export interface GroupByVariableProps extends CommonVariableProps {
152
+ /** The datasource whose queries are grouped, and which is asked for the keys. */
153
+ datasource: VariableDatasource;
154
+ /** A fixed set of keys to offer instead of the datasource's: a key, or `{ text, value }`. */
155
+ options?: Array<string | { text: string; value: string }>;
156
+ /** The keys selected when the dashboard loads with none in the URL. */
157
+ defaultValue?: string[] | VariableOption;
158
+ /** Whether a key not in the list can be typed in. Grafana's default is true. */
159
+ allowCustomValue?: boolean;
160
+ }
161
+
162
+ export interface SwitchVariableProps extends Omit<CommonVariableProps, "current"> {
163
+ /** Whether the switch is on when the dashboard loads. Defaults to off. */
164
+ enabled?: boolean;
165
+ /** The value `$name` has when the switch is on. Defaults to `"true"`. */
166
+ enabledValue?: string;
167
+ /** The value `$name` has when the switch is off. Defaults to `"false"`. */
168
+ disabledValue?: string;
169
+ }
170
+
88
171
  export interface VariableEntity<P = CommonVariableProps> extends Declarable {
89
172
  readonly props: P;
90
173
  readonly variableKind: VariableKind;
@@ -99,7 +182,7 @@ export interface DatasourceVariableEntity<T extends string = string> extends Var
99
182
 
100
183
  export const VARIABLE_TYPE_PREFIX = "Grafana::Variable::";
101
184
 
102
- function variableClass<P extends CommonVariableProps | ConstantVariableProps>(kind: VariableKind, className: string) {
185
+ function variableClass<P extends CommonVariableProps | ConstantVariableProps | SwitchVariableProps>(kind: VariableKind, className: string) {
103
186
  const Base = createProperty(`${VARIABLE_TYPE_PREFIX}${kind}`, "grafana") as unknown as (this: object, props: Record<string, unknown>) => void;
104
187
  const Cls = function (this: object, props: P) {
105
188
  Base.call(this, props as unknown as Record<string, unknown>);
@@ -143,6 +226,30 @@ export const TextboxVariable = variableClass<TextboxVariableProps>("textbox", "T
143
226
  props: TextboxVariableProps,
144
227
  ) => VariableEntity<TextboxVariableProps>;
145
228
 
229
+ /**
230
+ * Ad hoc filters: `key operator value` filters Grafana adds to every query
231
+ * sent to the variable's datasource (Prometheus, Loki, Elasticsearch,
232
+ * InfluxDB and others whose plugin supports them). Referenced by no query.
233
+ */
234
+ export const AdhocVariable = variableClass<AdhocVariableProps>("adhoc", "AdhocVariable") as unknown as new (
235
+ props: AdhocVariableProps,
236
+ ) => VariableEntity<AdhocVariableProps>;
237
+
238
+ /**
239
+ * Group by: a choice of label keys Grafana adds as a grouping to every query
240
+ * sent to the variable's datasource. In Grafana 12.4 and 13.x it is
241
+ * experimental: with the `groupByVariable` feature toggle off, Grafana
242
+ * drops it when the dashboard loads.
243
+ */
244
+ export const GroupByVariable = variableClass<GroupByVariableProps>("groupby", "GroupByVariable") as unknown as new (
245
+ props: GroupByVariableProps,
246
+ ) => VariableEntity<GroupByVariableProps>;
247
+
248
+ /** An on/off switch, `$name` being `enabledValue` or `disabledValue`. New in Grafana 12.3. */
249
+ export const SwitchVariable = variableClass<SwitchVariableProps>("switch", "SwitchVariable") as unknown as new (
250
+ props: SwitchVariableProps,
251
+ ) => VariableEntity<SwitchVariableProps>;
252
+
146
253
  export function isVariableEntity(value: unknown): value is VariableEntity {
147
254
  return (
148
255
  typeof value === "object" &&