@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
@@ -47,6 +47,7 @@ import * as jsYaml from "js-yaml";
47
47
  import type { TemplateIR, TemplateParser, ResourceIR } from "@intentius/chant/import/parser";
48
48
  import { DASHBOARD_SCHEMA_VERSION } from "../schema/dashboard.gen";
49
49
  import { BUILTIN_DATASOURCE_UIDS } from "../datasource";
50
+ import { JSONDATA_LINKS } from "../datasource-settings";
50
51
  import {
51
52
  looksLikeDashboard,
52
53
  looksLikeAlertingProvisioning,
@@ -149,9 +150,21 @@ const PACKAGE_CLASS_NAMES = new Set([
149
150
  "DatasourceVariable",
150
151
  "ConstantVariable",
151
152
  "TextboxVariable",
153
+ "AdhocVariable",
154
+ "GroupByVariable",
155
+ "SwitchVariable",
152
156
  "PromQuery",
153
157
  "TempoQuery",
154
158
  "LokiQuery",
159
+ "ElasticsearchQuery",
160
+ "CloudWatchQuery",
161
+ "AzureMonitorQuery",
162
+ "CloudMonitoringQuery",
163
+ "BigQueryQuery",
164
+ "PyroscopeQuery",
165
+ "PostgresQuery",
166
+ "MySQLQuery",
167
+ "MSSQLQuery",
155
168
  "TimeSeriesPanel",
156
169
  "StatPanel",
157
170
  "GaugePanel",
@@ -187,6 +200,8 @@ const PACKAGE_CLASS_NAMES = new Set([
187
200
  "MuteTiming",
188
201
  "NotificationTemplate",
189
202
  "ExternalDatasource",
203
+ "DatasourceProvisioning",
204
+ "Folder",
190
205
  ]);
191
206
 
192
207
  function words(text: string): string[] {
@@ -526,6 +541,52 @@ class DashboardConverter {
526
541
  return ids;
527
542
  }
528
543
 
544
+ // ── annotations ─────────────────────────────────────────────────
545
+
546
+ /**
547
+ * `annotations.list` as `Dashboard.annotations`: every entry but the
548
+ * built-in one Grafana adds anyway, with its datasource resolved like a
549
+ * panel's and every other key as it is. `enable` and `iconColor`, which
550
+ * the schema requires and the build fills in, are recorded when missing.
551
+ */
552
+ annotations(): Json[] {
553
+ const container = this.d.annotations;
554
+ if (container === undefined || container === null) return [];
555
+ if (!isObject(container) || !Array.isArray(container.list)) {
556
+ this.report.drop(pointer("annotations"), "dashboard", "annotations", "(it is not an annotations.list)");
557
+ return [];
558
+ }
559
+ for (const key of Object.keys(container)) if (key !== "list") this.report.drop(pointer("annotations", key), "annotations", key, NO_PROP);
560
+ const out: Json[] = [];
561
+ container.list.forEach((a, i) => {
562
+ if (isBuiltinAnnotation(a)) return;
563
+ const path = (...rest: string[]) => pointer("annotations", "list", i, ...rest);
564
+ if (!isObject(a) || typeof a.name !== "string") {
565
+ this.report.drop(path(), "annotations", `entry ${i}`, "(it is not a named annotation query)");
566
+ return;
567
+ }
568
+ const subject = `annotation "${a.name}"`;
569
+ const props: Json = {};
570
+ for (const [key, value] of Object.entries(a)) {
571
+ if (value === null || value === undefined) {
572
+ this.report.drop(path(key), subject, key);
573
+ continue;
574
+ }
575
+ if (key !== "datasource") {
576
+ props[key] = value;
577
+ continue;
578
+ }
579
+ const ds = this.resolveDatasource(value, path(key), subject);
580
+ if (ds === "mixed") this.report.drop(path(key), subject, key, MIXED_ELSEWHERE);
581
+ else if (ds) props.datasource = ds.value;
582
+ }
583
+ if (typeof a.enable !== "boolean") this.report.replace(path("enable"), true, subject, "enable", "is missing, so it is written as true, which runs the query");
584
+ if (typeof a.iconColor !== "string") this.report.replace(path("iconColor"), "red", subject, "iconColor", "is missing, so it is written as red");
585
+ out.push(props);
586
+ });
587
+ return out;
588
+ }
589
+
529
590
  // ── variables ───────────────────────────────────────────────────
530
591
 
531
592
  variable(json: unknown, index: number): string | undefined {
@@ -539,7 +600,7 @@ class DashboardConverter {
539
600
  const type = String(json.type);
540
601
  const mapping = VARIABLE_MAPPINGS[type];
541
602
  if (!mapping) {
542
- this.report.drop(path, subject, "", `(${type}) is not carried: chant has no ${type} variable yet (#2952), so it is left out`);
603
+ this.report.drop(path, subject, "", `(${type}) is not carried: chant has no ${type} variable, so it is left out`);
543
604
  return undefined;
544
605
  }
545
606
  let dropped = false;
@@ -883,6 +944,7 @@ class DashboardConverter {
883
944
  const variableRefs = [...inputIds, ...templating.map((_, i) => varIds.get(i)).filter((x): x is string => x !== undefined)].map(declRef);
884
945
 
885
946
  const panelRefs = this.items();
947
+ const annotations = this.annotations();
886
948
  this.settleExternals();
887
949
  if (this.legacyPanels > 0) {
888
950
  this.report.warn(
@@ -920,18 +982,7 @@ class DashboardConverter {
920
982
  if (variableRefs.length > 0) props.variables = variableRefs;
921
983
  if (panelRefs.length > 0) props.panels = panelRefs;
922
984
 
923
- // Annotations: the built-in one is what Grafana adds anyway; any other is not carried yet.
924
- const annotations = isObject(d.annotations) && Array.isArray(d.annotations.list) ? d.annotations.list : [];
925
- const others = annotations.filter((a) => !isBuiltinAnnotation(a));
926
- if (others.length > 0) {
927
- const names = others.map((a) => (isObject(a) && typeof a.name === "string" ? `"${a.name}"` : "(unnamed)"));
928
- this.report.drop(
929
- pointer("annotations"),
930
- "dashboard",
931
- "",
932
- `has ${others.length === 1 ? "the annotation" : "the annotations"} ${list(names)}, which ${others.length === 1 ? "is" : "are"} not carried: Dashboard has no annotations yet (#2953)`,
933
- );
934
- }
985
+ if (annotations.length > 0) props.annotations = annotations;
935
986
 
936
987
  const handled = new Set([
937
988
  ...DASHBOARD_FIELDS,
@@ -1032,10 +1083,55 @@ const DATASOURCE_FIELDS = [
1032
1083
  "version",
1033
1084
  ];
1034
1085
 
1035
- /** Plan a datasource provisioning file: one `Datasource` per entry. */
1086
+ /**
1087
+ * The `jsonData` paths of `ds` that hold another datasource's uid (see
1088
+ * `JSONDATA_LINKS`), with the uid found there and the plugin types the
1089
+ * field accepts.
1090
+ */
1091
+ function jsonDataLinks(type: string, jsonData: Json): Array<{ path: Array<string | number>; uid: string; targets: readonly string[] }> {
1092
+ const out: Array<{ path: Array<string | number>; uid: string; targets: readonly string[] }> = [];
1093
+ const walk = (value: unknown, rest: readonly string[], at: Array<string | number>, targets: readonly string[]) => {
1094
+ if (rest.length === 0) {
1095
+ if (typeof value === "string" && value !== "") out.push({ path: at, uid: value, targets });
1096
+ return;
1097
+ }
1098
+ const [head, ...tail] = rest;
1099
+ if (head === "*") {
1100
+ if (Array.isArray(value)) value.forEach((v, i) => walk(v, tail, [...at, i], targets));
1101
+ } else if (isObject(value)) walk(value[head], tail, [...at, head], targets);
1102
+ };
1103
+ for (const link of JSONDATA_LINKS) if (!link.on || link.on.includes(type)) walk(jsonData, link.path, [], link.targets);
1104
+ return out;
1105
+ }
1106
+
1107
+ /** A copy of `value` with `replacement` at `path`. */
1108
+ function replaceAt(value: unknown, path: ReadonlyArray<string | number>, replacement: unknown): unknown {
1109
+ if (path.length === 0) return replacement;
1110
+ const [head, ...tail] = path;
1111
+ if (Array.isArray(value)) return value.map((v, i) => (i === head ? replaceAt(v, tail, replacement) : v));
1112
+ if (isObject(value)) return { ...value, [head]: replaceAt(value[head], tail, replacement) };
1113
+ return value;
1114
+ }
1115
+
1116
+ /**
1117
+ * Plan a datasource provisioning file: one `Datasource` per entry, and a
1118
+ * `DatasourceProvisioning` for the file's own settings when they differ
1119
+ * from what the build writes by default (`prune: true`, no deletes): a file
1120
+ * that does not prune gets `prune: false`, so the rebuilt file still does
1121
+ * not.
1122
+ *
1123
+ * A uid in one entry's `jsonData` that names another entry of the file (a
1124
+ * Tempo's `tracesToLogsV2.datasourceUid`, a Prometheus exemplar's
1125
+ * `datasourceUid`, a Loki derived field's) becomes a reference to that
1126
+ * entry's `Datasource`, so the link is type-checked, when the entry is of a
1127
+ * plugin type the field accepts. Declarations are ordered so each refers
1128
+ * only to ones before it; a link that would close a cycle (Tempo to Loki and
1129
+ * Loki back to Tempo) stays a uid string. The build writes provisioned
1130
+ * datasources sorted by name, so the order does not show in its output.
1131
+ */
1036
1132
  export function planDatasourceProvisioning(doc: Json): { plan: Plan; warnings: string[] } {
1037
1133
  const report = new Report();
1038
- const declarations: Declaration[] = [];
1134
+ const entries: Array<{ id: string; name: string; type: string; props: Json; uid?: string }> = [];
1039
1135
  const list = Array.isArray(doc.datasources) ? doc.datasources : [];
1040
1136
  list.forEach((ds, i) => {
1041
1137
  if (!isObject(ds) || typeof ds.name !== "string" || typeof ds.type !== "string") {
@@ -1047,12 +1143,67 @@ export function planDatasourceProvisioning(doc: Json): { plan: Plan; warnings: s
1047
1143
  for (const key of Object.keys(ds)) {
1048
1144
  if (!DATASOURCE_FIELDS.includes(key)) report.drop(pointer("datasources", i, key), `datasource "${ds.name}"`, key, NO_PROP);
1049
1145
  }
1050
- declarations.push({ id: `datasource:${i}`, kind: "new", className: "Datasource", props, name: ds.name, module: "datasources" });
1146
+ entries.push({ id: `datasource:${i}`, name: ds.name, type: ds.type, props, ...(typeof ds.uid === "string" ? { uid: ds.uid } : {}) });
1051
1147
  });
1148
+ const settings: Json = {};
1149
+ if (doc.prune !== true) settings.prune = false;
1150
+ if (doc.prune !== undefined && doc.prune !== null && typeof doc.prune !== "boolean") {
1151
+ report.drop(pointer("prune"), "the provisioning file", "prune", `(${JSON.stringify(doc.prune)} is not true or false, so the file is written not to prune)`);
1152
+ }
1153
+ const deletes: Json[] = [];
1154
+ (Array.isArray(doc.deleteDatasources) ? doc.deleteDatasources : []).forEach((d, i) => {
1155
+ if (!isObject(d) || typeof d.name !== "string") {
1156
+ report.drop(pointer("deleteDatasources", i), "deleteDatasources", `entry ${i}`, "(it names no datasource)");
1157
+ return;
1158
+ }
1159
+ const orgId = typeof d.orgId === "number" ? d.orgId : undefined;
1160
+ for (const key of Object.keys(d)) if (key !== "name" && key !== "orgId") report.drop(pointer("deleteDatasources", i, key), `deleteDatasources "${d.name}"`, key, NO_PROP);
1161
+ deletes.push({ name: d.name, ...(orgId !== undefined ? { orgId } : {}) });
1162
+ });
1163
+ if (deletes.length > 0) settings.deleteDatasources = deletes;
1164
+ if (doc.deleteDatasources !== undefined && !Array.isArray(doc.deleteDatasources)) report.drop(pointer("deleteDatasources"), "the provisioning file", "deleteDatasources", "(it is not a list)");
1165
+ const settingsDeclarations: Declaration[] = [];
1166
+ if (Object.keys(settings).length > 0) {
1167
+ settingsDeclarations.push({ id: "datasource-provisioning", kind: "new", className: "DatasourceProvisioning", props: settings, name: "datasource provisioning", module: "datasources" });
1168
+ }
1052
1169
  for (const key of Object.keys(doc)) {
1053
- if (key === "apiVersion" || key === "datasources") continue;
1054
- report.drop(pointer(key), "the provisioning file", key, "(chant writes datasources only; prune and deleteDatasources are #2953)");
1170
+ if (key === "apiVersion" || key === "datasources" || key === "prune" || key === "deleteDatasources") continue;
1171
+ report.drop(pointer(key), "the provisioning file", key, NO_PROP);
1172
+ }
1173
+
1174
+ // Each entry's links to other entries of the file, by the uid they name.
1175
+ const byUid = new Map(entries.filter((e) => e.uid !== undefined).map((e) => [e.uid!, e]));
1176
+ const links = new Map(
1177
+ entries.map((e) => {
1178
+ const jsonData = e.props.jsonData;
1179
+ const found = isObject(jsonData) ? jsonDataLinks(e.type, jsonData) : [];
1180
+ return [e.id, found.flatMap((l) => {
1181
+ const target = byUid.get(l.uid);
1182
+ return target && target !== e && l.targets.includes(target.type) ? [{ path: l.path, target: target.id }] : [];
1183
+ })];
1184
+ }),
1185
+ );
1186
+ // File order, except that an entry waits for the entries it links to; a cycle is broken at the first entry left.
1187
+ const declared = new Set<string>();
1188
+ const declarations: Declaration[] = [];
1189
+ while (declared.size < entries.length) {
1190
+ const remaining = entries.filter((e) => !declared.has(e.id));
1191
+ const next = remaining.find((e) => links.get(e.id)!.every((l) => declared.has(l.target))) ?? remaining[0];
1192
+ let jsonData = next.props.jsonData;
1193
+ for (const l of links.get(next.id)!) if (declared.has(l.target)) jsonData = replaceAt(jsonData, l.path, declRef(l.target));
1194
+ const props = jsonData === undefined ? next.props : { ...next.props, jsonData };
1195
+ declarations.push({
1196
+ id: next.id,
1197
+ kind: "new",
1198
+ className: "Datasource",
1199
+ typeArguments: [JSON.stringify(next.type)],
1200
+ props,
1201
+ name: next.name,
1202
+ module: "datasources",
1203
+ });
1204
+ declared.add(next.id);
1055
1205
  }
1206
+ declarations.push(...settingsDeclarations);
1056
1207
  return {
1057
1208
  plan: {
1058
1209
  directory: "",
@@ -14,7 +14,7 @@
14
14
  * the check that nothing was dropped silently.
15
15
  *
16
16
  * The generated source must lint clean (COR001, COR004 and COR009 among
17
- * the rules), and the rebuilt dashboards must pass GRAF101-GRAF107 without
17
+ * the rules), and the rebuilt dashboards must pass GRAF101-GRAF110 without
18
18
  * errors. generated-types.e2e.test.ts type-checks the generated source.
19
19
  */
20
20
 
@@ -34,7 +34,19 @@ import { GrafanaParser, type DashboardResourceMetadata } from "./parser";
34
34
  import { GrafanaGenerator } from "./generator";
35
35
  import { applyEdits } from "./edits";
36
36
  import { normalizeDashboard } from "./normalize";
37
- import { COMMUNITY, KUBE_PROMETHEUS, LOSSY_V1_EXPORT, UI_EXPORTS, V2_EXPORTS, exampleOutputs, projectDir, read, removeDir, writeFiles } from "./testdata/fixtures";
37
+ import {
38
+ COMMUNITY,
39
+ KUBE_PROMETHEUS,
40
+ LOSSY_V1_EXPORT,
41
+ PROVISIONING,
42
+ UI_EXPORTS,
43
+ V2_EXPORTS,
44
+ exampleOutputs,
45
+ projectDir,
46
+ read,
47
+ removeDir,
48
+ writeFiles,
49
+ } from "./testdata/fixtures";
38
50
 
39
51
  type Json = Record<string, unknown>;
40
52
 
@@ -54,7 +66,7 @@ interface Imported {
54
66
  lint: { errorCount: number; warningCount: number; output: string };
55
67
  }
56
68
 
57
- /** GRAF101-GRAF107 over a build's grafana entities, as the post-synth checks run them: declared and external datasources included. */
69
+ /** GRAF101-GRAF110 over a build's grafana entities, as the post-synth checks run them: declared and external datasources included. */
58
70
  function checks(entities: Map<string, Declarable>): GrafanaIssue[] {
59
71
  const built = buildGrafana(new Map([...entities].filter(([, e]) => e.lexicon === "grafana")));
60
72
  return validateGrafanaOutput({
@@ -173,12 +185,32 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
173
185
  // GRAF101 saying it cannot check a dashboard that names its datasources only through variables.
174
186
  expect(out.issues.filter((i) => !(i.code === "GRAF101" && i.severity === "warning" && i.message.includes("cannot check")))).toEqual([]);
175
187
  expect(out.paths.every((p) => p.startsWith("chant-fx-"))).toBe(true);
176
- // The ad hoc filter and the annotation are named, not dropped silently.
188
+ // The ad hoc filter and the deploy annotation are carried (#2952, #2953).
177
189
  if (file.includes("checkout")) {
178
- expect(out.warnings).toContainEqual(expect.stringContaining('variable "Filters" (adhoc) is not carried'));
179
- expect(out.warnings).toContainEqual(expect.stringContaining('the annotation "Deploys"'));
190
+ expect(out.source).toContain("const filters = new AdhocVariable({");
191
+ expect(out.warnings.join("\n")).not.toContain("Deploys");
192
+ expect(out.source).toContain('name: "Deploys"');
193
+ expect((out.rebuilt!.annotations as { list: Json[] }).list.map((a) => a.name)).toEqual(["Deploys"]);
180
194
  expect(out.source).toContain("repeat: env,");
181
195
  expect(out.source).toContain("collapsed: true,");
196
+ } else if (file.includes("queries")) {
197
+ // Every datasource with a query class is imported through it, typed; none needs a defineQuery.
198
+ for (const cls of [
199
+ "ElasticsearchQuery",
200
+ "CloudWatchQuery",
201
+ "AzureMonitorQuery",
202
+ "CloudMonitoringQuery",
203
+ "BigQueryQuery",
204
+ "PyroscopeQuery",
205
+ "PostgresQuery",
206
+ "MySQLQuery",
207
+ "MSSQLQuery",
208
+ "PromQuery",
209
+ "LokiQuery",
210
+ ]) {
211
+ expect(out.source).toContain(`new ${cls}(`);
212
+ }
213
+ expect(out.source).not.toContain("defineQuery");
182
214
  } else if (file.includes("slo")) {
183
215
  expect(out.warnings).toContainEqual(expect.stringMatching(/is a library panel \("Burn rate", uid chant-fx-burn\)/));
184
216
  }
@@ -190,7 +222,7 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
190
222
  expect(out.warnings).toContain("dashboard: __elements is not carried (the library panels exported with it; library panels are not carried yet)");
191
223
  }
192
224
  if (file.includes(".external.")) {
193
- expect(out.warnings).toContainEqual(expect.stringMatching(/^__inputs: DS_PROMETHEUS \(prometheus\)/));
225
+ expect(out.warnings).toContainEqual(expect.stringMatching(/^__inputs: (.*, )?DS_PROMETHEUS \(prometheus\)/));
194
226
  expect(out.source).toContain("const dsPrometheus = new DatasourceVariable({");
195
227
  }
196
228
  });
@@ -205,6 +237,10 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
205
237
  // Tabs become rows, and the auto grid's panels keep the positions Grafana gives them.
206
238
  expect(out.warnings).toContainEqual(expect.stringMatching(/tabs "Overview" and "Details" become expanded rows/));
207
239
  expect((out.rebuilt!.panels as Json[]).filter((p) => p.type === "row").map((p) => p.title)).toEqual(["Overview", "Details", "Latency ($env)", "Logs"]);
240
+ // Its switch and group by variables come through the classic variable mappings.
241
+ expect(out.source).toContain('const detailed = new SwitchVariable({ name: "detailed", label: "Detailed" });');
242
+ expect(out.source).toContain("const groupBy = new GroupByVariable({");
243
+ expect(out.warnings.filter((w) => /^variable "(detailed|groupBy)"/.test(w))).toEqual([]);
208
244
  }
209
245
  });
210
246
  }
@@ -241,6 +277,23 @@ describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
241
277
  expect(out.text).not.toContain("__inputs");
242
278
  });
243
279
 
280
+ test("ad hoc, group by and switch variables, and object-form queries, import with no warnings", async () => {
281
+ const out = await expectRoundTrip(read("exports/grafana-13.2.2/drilldown.json"));
282
+ expect(out.warnings).toEqual([]);
283
+ for (const cls of ["AdhocVariable", "GroupByVariable", "SwitchVariable"]) expect(out.source).toContain(`new ${cls}({`);
284
+ expect(out.source).toContain("qryType: 4,");
285
+ expect(out.source).toContain('enabledValue: "0.99",');
286
+ // The rebuilt JSON keeps the object query, so Grafana's variable editor opens it in the form it was written in.
287
+ const namespace = (out.rebuilt!.templating as { list: Json[] }).list.find((v) => v.name === "namespace")!;
288
+ expect(namespace.query).toEqual({ qryType: 1, query: "label_values(kube_namespace_created, namespace)", refId: "PrometheusVariableQueryEditor-VariableQuery" });
289
+ // Repeated over a multi-value variable: no GRAF110.
290
+ expect(out.issues.filter((i) => i.code === "GRAF110")).toEqual([]);
291
+ for (const file of ["exports/grafana-12.4.11/drilldown.json", "exports/grafana-12.4.11/drilldown.external.json", "exports/grafana-13.2.2/drilldown.external.json"]) {
292
+ const warnings = (await expectRoundTrip(read(file))).warnings;
293
+ expect(warnings.filter((w) => !w.startsWith("__inputs:") && !w.startsWith("dashboard: __requires")), file).toEqual([]);
294
+ }
295
+ });
296
+
244
297
  for (const file of COMMUNITY) {
245
298
  test(`grafana.com dashboard ${file}`, async () => {
246
299
  const out = await expectRoundTrip(read(file));
@@ -343,6 +396,46 @@ describe("provisioning files", () => {
343
396
  expect(out.lint.errorCount + out.lint.warningCount, out.lint.output).toBe(0);
344
397
  });
345
398
 
399
+ for (const file of PROVISIONING) {
400
+ test(`${file}: typed Datasources, links as references, the same datasources back`, async () => {
401
+ const yaml = read(file);
402
+ const out = await importAndBuild(yaml);
403
+ expect(out.warnings).toEqual([]);
404
+ expect(out.buildErrors).toEqual([]);
405
+ // What the build adds: its defaults for access and editable, and a uid made from the name.
406
+ type Entry = Record<string, unknown> & { name: string };
407
+ const withDefaults = (d: Entry) => ({ access: "proxy", editable: false, ...d });
408
+ const sorted = (list: Entry[]) => [...list].sort((a, b) => a.name.localeCompare(b.name));
409
+ const source = sorted((load(yaml) as { datasources: Entry[] }).datasources.map(withDefaults));
410
+ expect(sorted((load(out.files[DATASOURCES_FILE]) as { datasources: Entry[] }).datasources)).toEqual(source);
411
+ // Settings consts are typed for their plugin, and a uid naming an earlier datasource of the file is a reference.
412
+ expect(out.source).toContain('PropsOf<typeof Datasource<"tempo">>["jsonData"]');
413
+ expect(out.source).toMatch(/serviceMap: \{ datasourceUid: (mimir|prometheus) \}/);
414
+ expect(out.lint.errorCount + out.lint.warningCount, out.lint.output).toBe(0);
415
+ });
416
+ }
417
+
418
+ test("a link that would close a cycle stays a uid, and one to a plugin type the field does not take is not linked", () => {
419
+ const ir = new GrafanaParser().parse(
420
+ [
421
+ "apiVersion: 1",
422
+ "datasources:",
423
+ " - { name: Tempo, type: tempo, uid: tempo, jsonData: { tracesToLogsV2: { datasourceUid: loki }, serviceMap: { datasourceUid: splunk } } }",
424
+ " - { name: Loki, type: loki, uid: loki, jsonData: { derivedFields: [{ name: t, matcherRegex: x, datasourceUid: tempo }] } }",
425
+ " - { name: Splunk, type: grafana-splunk-datasource, uid: splunk }",
426
+ ].join("\n"),
427
+ );
428
+ const source = new GrafanaGenerator()
429
+ .generate(ir)
430
+ .map((f) => f.content)
431
+ .join("\n");
432
+ // Neither Tempo nor Loki can come first with both links; Tempo, first in the file, keeps its link as a uid.
433
+ expect(source).toContain('tracesToLogsV2: { datasourceUid: "loki" }');
434
+ expect(source).toContain("datasourceUid: tempo");
435
+ // The service map takes a Prometheus, so a Splunk uid stays a string.
436
+ expect(source).toContain('serviceMap: { datasourceUid: "splunk" }');
437
+ });
438
+
346
439
  test("a dashboard provisioning file becomes DashboardProviders", async () => {
347
440
  const yaml = [
348
441
  "apiVersion: 1",
@@ -382,13 +475,15 @@ describe("chant import dashboard.json", () => {
382
475
  expect(result.error).toBeUndefined();
383
476
  expect(result.success).toBe(true);
384
477
  expect(result.lexicon).toBe("grafana");
478
+ // Nine variables (the two __inputs datasources among them): more than COR009's eight per module.
385
479
  expect(result.generatedFiles).toEqual([
386
- "chant-fx-checkout/variables.ts",
480
+ "chant-fx-checkout/variables-1.ts",
481
+ "chant-fx-checkout/variables-2.ts",
387
482
  "chant-fx-checkout/panels.ts",
388
483
  "chant-fx-checkout/row-details-for-job.ts",
389
484
  "chant-fx-checkout/dashboard.ts",
390
485
  ]);
391
- expect(result.warnings).toContainEqual(expect.stringContaining('variable "Filters" (adhoc) is not carried'));
486
+ expect(result.warnings.join("\n")).not.toContain("is not carried: chant has no");
392
487
  const built = await build(output, [grafanaSerializer]);
393
488
  expect(built.errors).toEqual([]);
394
489
  const text = (built.outputs.get("grafana") as SerializerResult).files!["dashboards/chant-fx-checkout.json"];
@@ -11,6 +11,7 @@
11
11
  * provenance and license in its README).
12
12
  * - Alerting provisioning files (test/fixtures/alerting/).
13
13
  * - What this lexicon's examples build.
14
+ * - Datasource provisioning files (test/fixtures/provisioning/).
14
15
  */
15
16
  import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
16
17
  import { dirname, join, resolve } from "path";
@@ -58,6 +59,13 @@ export const KUBE_PROMETHEUS: readonly string[] = readdirSync(join(fixturesDir,
58
59
  .sort()
59
60
  .map((f) => `kube-prometheus/${f}`);
60
61
 
62
+ /**
63
+ * Datasource provisioning files (test/fixtures/provisioning/, provenance in
64
+ * its README): one written for chant with every plugin whose settings are
65
+ * typed, and grafana/docker-otel-lgtm's.
66
+ */
67
+ export const PROVISIONING: readonly string[] = ["provisioning/typed-plugins.datasources.yaml", "provisioning/docker-otel-lgtm.datasources.yaml"];
68
+
61
69
  /**
62
70
  * The alerting provisioning corpus (test/fixtures/alerting/, provenance and
63
71
  * licenses in its README): Grafana 12.4.11 and 13.2.2 exports, and files
package/src/import/v2.ts CHANGED
@@ -20,9 +20,10 @@
20
20
  * pin bump that adds a v2 key fails until the key is placed in one of them.
21
21
  *
22
22
  * What the classic model can hold but chant cannot yet (library panels,
23
- * annotations, ad hoc, group by and switch variables) is written to the
24
- * classic JSON as Grafana would and reported by the classic importer
25
- * (`./parser.ts`), as for any classic dashboard.
23
+ * annotations) is written to the classic JSON as Grafana would and reported
24
+ * by the classic importer (`./parser.ts`), as for any classic dashboard. Ad
25
+ * hoc, group by and switch variables are written the same way and imported
26
+ * by the classic importer's variable mappings (`./mappings.ts`).
26
27
  *
27
28
  * A classic read of a dashboard Grafana stores as v2 is the lossy direction:
28
29
  * `storedAsV2` recognises it from the resource's