@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
package/src/build.ts CHANGED
@@ -10,15 +10,28 @@
10
10
 
11
11
  import { dump } from "js-yaml";
12
12
  import type { Declarable } from "@intentius/chant/declarable";
13
- import { isDatasourceDeclaration, isDatasourceEntity, isExternalDatasource, type DatasourceEntity, type DatasourceRef, type ExternalDatasourceEntity } from "./datasource";
13
+ import {
14
+ isDatasourceDeclaration,
15
+ isDatasourceEntity,
16
+ isDatasourceProvisioningEntity,
17
+ isExternalDatasource,
18
+ type DatasourceEntity,
19
+ type DatasourceProvisioningProps,
20
+ type DatasourceRef,
21
+ type DeletedDatasource,
22
+ type ExternalDatasourceEntity,
23
+ } from "./datasource";
14
24
  import {
15
25
  isDashboardEntity,
16
26
  isDashboardProviderEntity,
17
27
  DEFAULT_DASHBOARDS_PATH,
18
28
  type DashboardEntity,
19
29
  type DashboardLinkInput,
20
- type DashboardProviderProps,
30
+ type DashboardProviderEntityProps,
21
31
  } from "./dashboard";
32
+ import { annotationJson } from "./annotations";
33
+ import { folderLevels, isFolderEntity, type FolderEntity } from "./folder";
34
+ import { resolveFolders, type FolderPlan } from "./api/folders";
22
35
  import { isPanelEntity, isRowEntity, type PanelEntity, type RowEntity, type PanelLink } from "./panels";
23
36
  import { isQueryEntity, type QueryEntity, type DatasourceInput } from "./query";
24
37
  import { isDatasourceVariable, isVariableEntity, type VariableEntity, type VariableHide } from "./variables";
@@ -67,6 +80,12 @@ export interface ProvisionedDatasource {
67
80
  version?: number;
68
81
  }
69
82
 
83
+ /** The settings of the datasource provisioning file besides its datasources. */
84
+ export interface DatasourceFileSettings {
85
+ prune: boolean;
86
+ deleteDatasources: DeletedDatasource[];
87
+ }
88
+
70
89
  /** One entry of `providers:` in a dashboard provisioning file. */
71
90
  export interface ProvisionedProvider {
72
91
  name: string;
@@ -84,6 +103,8 @@ export interface BuiltDashboard {
84
103
  uid: string;
85
104
  title: string;
86
105
  folder?: string;
106
+ /** The uid of its folder, when it is in one. */
107
+ folderUid?: string;
87
108
  /** Path of its JSON file, relative to the output directory. */
88
109
  file: string;
89
110
  json: DashboardJson;
@@ -95,6 +116,10 @@ export interface BuiltGrafana {
95
116
  /** `ExternalDatasource` declarations: the checks count them, the provisioning file leaves them out. */
96
117
  externalDatasources: ExternalDatasourceRecord[];
97
118
  providers: ProvisionedProvider[];
119
+ /** Every folder the dashboards and `Folder`s name, each level of a path its own, sorted by path. */
120
+ folders: FolderPlan[];
121
+ /** `prune` and `deleteDatasources` of the datasource provisioning file. */
122
+ datasourceFile: DatasourceFileSettings;
98
123
  /** The alerting provisioning file, when the build declares any alerting. */
99
124
  alerting?: AlertingFile;
100
125
  /** Every output file by path, ready to write. */
@@ -105,7 +130,9 @@ export interface BuiltGrafana {
105
130
 
106
131
  export interface GrafanaIndex {
107
132
  grafanaSchema: string;
108
- dashboards: Array<{ uid: string; title: string; folder?: string; file: string }>;
133
+ dashboards: Array<{ uid: string; title: string; folder?: string; folderUid?: string; file: string }>;
134
+ /** The folders the dashboards are in, and any `Folder` declared: what the API applier creates, parents first. */
135
+ folders?: FolderPlan[];
109
136
  datasources: Array<{ name: string; type: string; uid: string }>;
110
137
  /** Datasources declared with `ExternalDatasource`: referenced, never provisioned. */
111
138
  externalDatasources?: ExternalDatasourceRecord[];
@@ -215,7 +242,7 @@ export function variableModel(variable: VariableEntity): VariableModel {
215
242
  ...common,
216
243
  datasource: datasourceRef(p.datasource),
217
244
  query: p.query,
218
- definition: p.query,
245
+ definition: p.definition ?? queryText(p.query),
219
246
  regex: p.regex,
220
247
  refresh: REFRESH[(p.refresh ?? "onLoad") as keyof typeof REFRESH],
221
248
  sort: p.sort,
@@ -274,9 +301,51 @@ export function variableModel(variable: VariableEntity): VariableModel {
274
301
  query: p.value ?? "",
275
302
  current: { text: p.value ?? "", value: p.value ?? "" },
276
303
  }) as VariableModel;
304
+ case "adhoc":
305
+ return compact({
306
+ ...common,
307
+ datasource: datasourceRef(p.datasource),
308
+ filters: p.filters ?? [],
309
+ baseFilters: p.baseFilters ?? [],
310
+ defaultKeys: p.defaultKeys,
311
+ allowCustomValue: p.allowCustomValue,
312
+ enableGroupBy: p.enableGroupBy,
313
+ }) as VariableModel;
314
+ case "groupby": {
315
+ const options = p.options?.map((o: string | { text: string; value: string }) => (typeof o === "string" ? { text: o, value: o } : o));
316
+ const defaultValue = Array.isArray(p.defaultValue) ? { text: p.defaultValue, value: p.defaultValue } : p.defaultValue;
317
+ return compact({
318
+ ...common,
319
+ datasource: datasourceRef(p.datasource),
320
+ options,
321
+ current: p.current ?? defaultValue,
322
+ defaultValue,
323
+ allowCustomValue: p.allowCustomValue,
324
+ }) as VariableModel;
325
+ }
326
+ case "switch": {
327
+ const on: string = p.enabledValue ?? "true";
328
+ const off: string = p.disabledValue ?? "false";
329
+ const value = p.enabled ? on : off;
330
+ return compact({
331
+ ...common,
332
+ current: { text: value, value },
333
+ options: [
334
+ { text: on, value: on },
335
+ { text: off, value: off },
336
+ ],
337
+ }) as VariableModel;
338
+ }
277
339
  }
278
340
  }
279
341
 
342
+ /** The text of a variable query: the string, or an object query's `query`. */
343
+ function queryText(query: unknown): string | undefined {
344
+ if (typeof query === "string") return query;
345
+ if (query && typeof query === "object" && typeof (query as { query?: unknown }).query === "string") return (query as { query: string }).query;
346
+ return undefined;
347
+ }
348
+
280
349
  // ── Panels and layout ───────────────────────────────────────────
281
350
 
282
351
  function repeatName(repeat: VariableEntity | string | undefined): string | undefined {
@@ -531,7 +600,7 @@ export function dashboardUid(dashboard: DashboardEntity, exportName?: string): s
531
600
  export function renderDashboard(dashboard: DashboardEntity, exportName?: string): DashboardJson {
532
601
  const p = dashboard.props;
533
602
  return compact({
534
- annotations: { list: [] },
603
+ annotations: { list: (p.annotations ?? []).map((a) => annotationJson(a, datasourceRef)) },
535
604
  description: p.description,
536
605
  editable: p.editable ?? true,
537
606
  fiscalYearStartMonth: p.fiscalYearStartMonth ?? 0,
@@ -598,7 +667,7 @@ export function externalDatasourceRecord(ds: ExternalDatasourceEntity): External
598
667
  }
599
668
 
600
669
  /** One dashboard provider as a provisioning entry. */
601
- export function provisionedProvider(p: DashboardProviderProps): ProvisionedProvider {
670
+ export function provisionedProvider(p: DashboardProviderEntityProps): ProvisionedProvider {
602
671
  const pinnedFolder = p.folder !== undefined || p.folderUid !== undefined;
603
672
  return compact({
604
673
  name: p.name,
@@ -620,9 +689,26 @@ function yamlFile(header: string, body: Record<string, unknown>): string {
620
689
  return `# ${header}\n${dump(body, { lineWidth: -1, noRefs: true, quotingType: '"' })}`;
621
690
  }
622
691
 
623
- /** The datasource provisioning file for these datasources. */
624
- export function datasourcesYaml(datasources: ProvisionedDatasource[]): string {
625
- return yamlFile("Grafana datasource provisioning, generated by chant.", { apiVersion: 1, datasources });
692
+ /** `prune` and `deleteDatasources` as a build writes them: pruning on unless a `DatasourceProvisioning` turns it off. */
693
+ export function datasourceFileSettings(props: DatasourceProvisioningProps | undefined): DatasourceFileSettings {
694
+ return {
695
+ prune: props?.prune ?? true,
696
+ deleteDatasources: (props?.deleteDatasources ?? []).map((d) => ({ name: d.name, ...(d.orgId !== undefined ? { orgId: d.orgId } : {}) })),
697
+ };
698
+ }
699
+
700
+ /**
701
+ * The datasource provisioning file for these datasources. It prunes by
702
+ * default (`prune: true`), so a datasource removed from the build is removed
703
+ * from Grafana; `deleteDatasources` comes first, as Grafana runs it first.
704
+ */
705
+ export function datasourcesYaml(datasources: ProvisionedDatasource[], settings: DatasourceFileSettings = datasourceFileSettings(undefined)): string {
706
+ return yamlFile("Grafana datasource provisioning, generated by chant.", {
707
+ apiVersion: 1,
708
+ ...(settings.prune ? { prune: true } : {}),
709
+ ...(settings.deleteDatasources.length > 0 ? { deleteDatasources: settings.deleteDatasources } : {}),
710
+ datasources,
711
+ });
626
712
  }
627
713
 
628
714
  /** The dashboard provisioning file for these providers. */
@@ -632,19 +718,18 @@ export function dashboardProvidersYaml(providers: ProvisionedProvider[]): string
632
718
 
633
719
  /**
634
720
  * A dashboard's `folder` as its directory under `dashboards/`: one
635
- * subdirectory per `/`-separated level, which a provider with
721
+ * subdirectory per level (`folderLevels`), which a provider with
636
722
  * `foldersFromFilesStructure` turns into nested folders on Grafana 13.1 and
637
- * later (GRAF109 says what earlier versions do). Leading dots and blank
638
- * levels are dropped, so a folder can't name a directory outside
639
- * `dashboards/`; a backslash becomes `-`. Empty when nothing is left: the
640
- * General folder.
723
+ * later (GRAF109 says what earlier versions do). Empty for the General
724
+ * folder.
641
725
  */
642
726
  function folderDir(folder: string): string {
643
- return folder
644
- .split("/")
645
- .map((level) => level.replace(/\\+/g, "-").trim().replace(/^\.+/, "").trim())
646
- .filter((level) => level !== "")
647
- .join("/");
727
+ return folderLevels(folder).join("/");
728
+ }
729
+
730
+ /** A `Folder` and every folder above it. */
731
+ function withAncestors(folder: FolderEntity, into: Set<FolderEntity>): void {
732
+ for (let f: FolderEntity | undefined = folder; f; f = f.props.parent) into.add(f);
648
733
  }
649
734
 
650
735
  // ── The whole build ─────────────────────────────────────────────
@@ -666,6 +751,17 @@ export function buildGrafana(entities: Map<string, Declarable> | Iterable<Declar
666
751
  .map(([, e]) => externalDatasourceRecord(e as ExternalDatasourceEntity))
667
752
  .sort((a, b) => a.uid.localeCompare(b.uid));
668
753
 
754
+ // Folders: every declared `Folder` and every one a dashboard holds, with their parents, pin uids; every path a dashboard names is a folder per level.
755
+ const declaredFolders = new Set<FolderEntity>();
756
+ for (const [, e] of named) {
757
+ if (isFolderEntity(e)) withAncestors(e, declaredFolders);
758
+ else if (isDashboardEntity(e) && e.folderEntity) withAncestors(e.folderEntity, declaredFolders);
759
+ }
760
+ const resolved = resolveFolders({
761
+ declared: [...declaredFolders].map((f) => ({ uid: f.uid, path: f.path })),
762
+ paths: named.flatMap(([, e]) => (isDashboardEntity(e) && e.props.folder ? [e.props.folder] : [])),
763
+ }, { duplicateUids: "keep" });
764
+
669
765
  const dashboards: BuiltDashboard[] = [];
670
766
  for (const [name, e] of named) {
671
767
  if (!isDashboardEntity(e)) continue;
@@ -677,10 +773,15 @@ export function buildGrafana(entities: Map<string, Declarable> | Iterable<Declar
677
773
  let file = `${dir}/${uid}.json`;
678
774
  for (let n = 2; file in files; n++) file = `${dir}/${uid}-${n}.json`;
679
775
  files[file] = `${JSON.stringify(json, null, 2)}\n`;
680
- dashboards.push({ uid, title: e.props.title, ...(folder ? { folder } : {}), file, json });
776
+ const folderUid = sub ? resolved.uidOf(sub) : undefined;
777
+ dashboards.push({ uid, title: e.props.title, ...(folder ? { folder } : {}), ...(folderUid ? { folderUid } : {}), file, json });
681
778
  }
682
779
 
683
- const declaredProviders = named.filter(([, e]) => isDashboardProviderEntity(e)).map(([, e]) => (e as unknown as { props: DashboardProviderProps }).props);
780
+ const settings = named.map(([, e]) => e).filter(isDatasourceProvisioningEntity);
781
+ if (settings.length > 1) throw new Error("grafana: declare at most one DatasourceProvisioning; the build writes one datasource provisioning file");
782
+ const datasourceFile = datasourceFileSettings(settings[0]?.props);
783
+
784
+ const declaredProviders = named.filter(([, e]) => isDashboardProviderEntity(e)).map(([, e]) => (e as unknown as { props: DashboardProviderEntityProps }).props);
684
785
  const providers =
685
786
  declaredProviders.length > 0
686
787
  ? declaredProviders.map(provisionedProvider)
@@ -688,20 +789,21 @@ export function buildGrafana(entities: Map<string, Declarable> | Iterable<Declar
688
789
  ? [provisionedProvider({ name: DEFAULT_PROVIDER_NAME })]
689
790
  : [];
690
791
 
691
- if (datasources.length > 0) files[DATASOURCES_FILE] = datasourcesYaml(datasources);
792
+ if (datasources.length > 0 || datasourceFile.deleteDatasources.length > 0) files[DATASOURCES_FILE] = datasourcesYaml(datasources, datasourceFile);
692
793
  if (providers.length > 0) files[DASHBOARD_PROVIDERS_FILE] = dashboardProvidersYaml(providers);
693
794
  const alerting = buildAlerting(named.map(([, e]) => e));
694
795
  if (alerting) files[ALERTING_FILE] = alertingYaml(alerting.file);
695
796
 
696
797
  const index: GrafanaIndex = {
697
798
  grafanaSchema: `${GRAFANA_SCHEMA_PIN.source}@${GRAFANA_SCHEMA_PIN.ref}`,
698
- dashboards: dashboards.map(({ uid, title, folder, file }) => ({ uid, title, ...(folder ? { folder } : {}), file })),
799
+ dashboards: dashboards.map(({ uid, title, folder, folderUid, file }) => ({ uid, title, ...(folder ? { folder } : {}), ...(folderUid ? { folderUid } : {}), file })),
800
+ ...(resolved.folders.length > 0 ? { folders: resolved.folders } : {}),
699
801
  datasources: datasources.map(({ name, type, uid }) => ({ name, type, uid })),
700
802
  ...(externalDatasources.length > 0 ? { externalDatasources } : {}),
701
803
  ...(alerting ? { alerting: alerting.index } : {}),
702
804
  files: Object.keys(files).sort(),
703
805
  };
704
- return { dashboards, datasources, externalDatasources, providers, ...(alerting ? { alerting: alerting.file } : {}), files, index };
806
+ return { dashboards, datasources, externalDatasources, providers, folders: resolved.folders, datasourceFile, ...(alerting ? { alerting: alerting.file } : {}), files, index };
705
807
  }
706
808
 
707
809
  /** Every file the grafana entities render to, by path relative to the output directory. */
package/src/catalog.ts CHANGED
@@ -5,7 +5,8 @@
5
5
  */
6
6
 
7
7
  import type { LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
8
- import { DATASOURCE_TYPE, EXTERNAL_DATASOURCE_TYPE } from "./datasource";
8
+ import { DATASOURCE_PROVISIONING_TYPE, DATASOURCE_TYPE, EXTERNAL_DATASOURCE_TYPE } from "./datasource";
9
+ import { FOLDER_TYPE } from "./folder";
9
10
  import { DASHBOARD_TYPE, DASHBOARD_PROVIDER_TYPE } from "./dashboard";
10
11
  import * as panels from "./panels";
11
12
  import * as queries from "./query";
@@ -21,7 +22,7 @@ import {
21
22
  NOTIFICATION_TEMPLATE_TYPE,
22
23
  } from "./alerting";
23
24
 
24
- export type CatalogKind = "dashboard" | "datasource" | "provider" | "panel" | "row" | "query" | "variable" | "alerting" | "expression";
25
+ export type CatalogKind = "dashboard" | "folder" | "datasource" | "provider" | "panel" | "row" | "query" | "variable" | "alerting" | "expression";
25
26
 
26
27
  export interface CatalogEntry {
27
28
  className: string;
@@ -67,6 +68,9 @@ const VARIABLES: Array<[string, string, string]> = [
67
68
  ["DatasourceVariable", "datasource", "A choice of datasource of one plugin type; usable wherever a datasource is"],
68
69
  ["ConstantVariable", "constant", "A hidden, fixed value"],
69
70
  ["TextboxVariable", "textbox", "A free-text box"],
71
+ ["AdhocVariable", "adhoc", "Key/value filters Grafana adds to every query sent to the variable's datasource"],
72
+ ["GroupByVariable", "groupby", "A choice of label keys Grafana groups every query to the variable's datasource by (experimental in Grafana)"],
73
+ ["SwitchVariable", "switch", "An on/off switch with a value for each state (Grafana 12.3 and later)"],
70
74
  ];
71
75
 
72
76
  const EXPRESSIONS: Array<[string, string, string]> = [
@@ -100,6 +104,20 @@ export const BUILTIN_CATALOG: CatalogEntry[] = [
100
104
  entityKind: "resource",
101
105
  description: "A datasource that already exists in Grafana: referenced like a Datasource and checked by GRAF101/GRAF102, never provisioned",
102
106
  },
107
+ {
108
+ className: "Folder",
109
+ entityType: FOLDER_TYPE,
110
+ kind: "folder",
111
+ entityKind: "resource",
112
+ description: "A folder with a stable uid, nested with parent; a dashboard's folder may be one instead of a path",
113
+ },
114
+ {
115
+ className: "DatasourceProvisioning",
116
+ entityType: DATASOURCE_PROVISIONING_TYPE,
117
+ kind: "provider",
118
+ entityKind: "resource",
119
+ description: "The datasource provisioning file's prune (on by default) and deleteDatasources",
120
+ },
103
121
  {
104
122
  className: "DashboardProvider",
105
123
  entityType: DASHBOARD_PROVIDER_TYPE,
@@ -31,10 +31,17 @@ export const overview = new Dashboard({ title: "Overview", panels: [requests] })
31
31
 
32
32
  Panels: time series, stat, gauge, table, logs, traces, heatmap, text, bar
33
33
  chart, bar gauge, pie chart, state timeline, status history, histogram, node
34
- graph, XY chart, trend, canvas, geomap, flame graph and alert list, plus rows. Queries: Prometheus (PromQL), Tempo (TraceQL) and Loki (LogQL). Panel
35
- options and query fields are generated from Grafana's own schemas at a pinned
36
- version, corrected against Grafana's CUE, and track Grafana 12.4 and 13.x. Every
37
- build validates the dashboards against the same schemas.
34
+ graph, XY chart, trend, canvas, geomap, flame graph and alert list, plus rows.
35
+ Queries: Prometheus (PromQL), Tempo (TraceQL), Loki (LogQL), Elasticsearch,
36
+ CloudWatch, Azure Monitor, Cloud Monitoring, BigQuery, Pyroscope, and SQL for
37
+ PostgreSQL, MySQL and MSSQL. Panel options and query fields are generated from
38
+ Grafana's own schemas at a pinned version, corrected against Grafana's CUE, and
39
+ track Grafana 12.4 and 13.x. Every build validates the dashboards against the
40
+ same schemas.
41
+ Dashboards are written in the classic (v1) JSON model. Grafana 13's v2 model
42
+ (tabs, auto grids, conditional rendering) cannot be declared yet; import and
43
+ drift read a v2 dashboard by converting it to the classic model, and say what
44
+ the conversion loses.
38
45
  \`definePanel\` and \`defineQuery\` add plugins chant doesn't ship.
39
46
  \`chant import\` turns dashboard JSON exported from Grafana into this TypeScript,
40
47
  and it builds back to the same dashboard (see
@@ -65,8 +72,8 @@ wrong type (GRAF102), a \`$variable\` the dashboard doesn't declare (GRAF103),
65
72
  duplicate uids and ids (GRAF104), panels off the grid or overlapping (GRAF105),
66
73
  anything Grafana's dashboard schema rejects (GRAF107), PromQL sent to a
67
74
  Prometheus that doesn't parse (GRAF108), dashboard providers that load a
68
- dashboard twice or not into its declared folder (GRAF109), and a unit Grafana
69
- doesn't know (GRAF115). For alerting, they catch rules whose condition or expressions name
75
+ dashboard twice or not into its declared folder (GRAF109), a panel repeated over a variable that only ever
76
+ holds one value (GRAF110), and a unit Grafana doesn't know (GRAF115). For alerting, they catch rules whose condition or expressions name
70
77
  no query (GRAF111), queries to undeclared datasources (GRAF112), routes to
71
78
  undeclared contact points or mute timings (GRAF113), and uids, intervals and
72
79
  duplicates Grafana refuses (GRAF114).
@@ -4,7 +4,11 @@
4
4
  * 1. `src/schema/<name>.gen.ts` from each vendored schema in `src/spec/schemas/`
5
5
  * (see `src/pin.ts`). These are committed, and a test fails when they
6
6
  * drift from what this step would write.
7
- * 2. `src/generated/lexicon-grafana.json`, the registry of entity classes
7
+ * 2. `src/spec/schemas.gen.ts`, the same schemas with the overlay applied, as
8
+ * JSON text. Validation (GRAF107, the `/validation` subpath) reads them
9
+ * from there rather than from the files, so it needs no filesystem access
10
+ * and works bundled. Committed and checked the same way.
11
+ * 3. `src/generated/lexicon-grafana.json`, the registry of entity classes
8
12
  * the package exports, derived from the catalog so the packaged
9
13
  * `dist/meta.json` cannot disagree with the classes.
10
14
  */
@@ -29,6 +33,27 @@ function dashboardExtras(schema: Record<string, unknown>): string[] {
29
33
  return ["/** The dashboard `schemaVersion` the pinned schema defaults to, and the one chant emits. */", `export const DASHBOARD_SCHEMA_VERSION = ${v};`];
30
34
  }
31
35
 
36
+ /** `src/spec/schemas.gen.ts`: each schema validation uses, overlay applied, as one line of JSON text. */
37
+ export function schemaDataModule(): string {
38
+ const lines = [
39
+ "// Code generated by chant generate. DO NOT EDIT.",
40
+ "// The vendored Grafana schemas (src/spec/schemas/, see src/pin.ts) with the",
41
+ "// correction overlay (src/spec/overlay/) applied, as JSON text, for",
42
+ "// src/schema-validate.ts. A module rather than a file read, so validation",
43
+ "// needs no filesystem access and survives bundling; text rather than object",
44
+ "// literals, so loading it costs little until a schema is parsed.",
45
+ "",
46
+ 'import type { SchemaName } from "../pin";',
47
+ "",
48
+ "export const SCHEMA_JSON: Readonly<Record<SchemaName, string>> = {",
49
+ ];
50
+ for (const name of SCHEMA_NAMES) {
51
+ lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(JSON.stringify(loadSchema(name as SchemaName)))},`);
52
+ }
53
+ lines.push("};", "");
54
+ return lines.join("\n");
55
+ }
56
+
32
57
  /** Every generated schema module, keyed by path relative to the package root. */
33
58
  export function schemaModules(): Record<string, string> {
34
59
  const out: Record<string, string> = {};
@@ -37,6 +62,7 @@ export function schemaModules(): Record<string, string> {
37
62
  const extra = name === "dashboard" ? dashboardExtras(schema) : [];
38
63
  out[`src/schema/${name}.gen.ts`] = schemaModule(name, schema, extra).source;
39
64
  }
65
+ out["src/spec/schemas.gen.ts"] = schemaDataModule();
40
66
  return out;
41
67
  }
42
68
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { DatasourceInput } from "../query";
7
7
  import type { DashboardLinkInput } from "../dashboard";
8
+ import type { FolderEntity } from "../folder";
8
9
  import type { ThresholdsConfig } from "../schema/dashboard.gen";
9
10
 
10
11
  /** Dashboard settings every composite takes; each has a default derived from what it reads. */
@@ -16,8 +17,8 @@ export interface DashboardOptions {
16
17
  uid?: string;
17
18
  description?: string;
18
19
  tags?: string[];
19
- /** The Grafana folder to provision it into. */
20
- folder?: string;
20
+ /** The Grafana folder to provision it into: a path, or a `Folder` to pin its uid. */
21
+ folder?: string | FolderEntity;
21
22
  /** Initial time range. */
22
23
  time?: { from: string; to: string };
23
24
  /** Auto-refresh interval, e.g. `30s`. */
package/src/dashboard.ts CHANGED
@@ -12,6 +12,8 @@ import type { Declarable } from "@intentius/chant/declarable";
12
12
  import type { PanelEntity, RowEntity } from "./panels";
13
13
  import type { VariableEntity } from "./variables";
14
14
  import type { DashboardLink, TimePickerConfig } from "./schema/dashboard.gen";
15
+ import { isFolderEntity, type FolderEntity } from "./folder";
16
+ import type { AnnotationInput } from "./annotations";
15
17
 
16
18
  /** A dashboard link: `title` and `type` required, the rest defaulted as Grafana's editor would. */
17
19
  export type DashboardLinkInput = Partial<DashboardLink> & Pick<DashboardLink, "title" | "type">;
@@ -54,26 +56,50 @@ export interface DashboardProps {
54
56
  panels?: Array<PanelEntity<any, any> | RowEntity>;
55
57
  links?: DashboardLinkInput[];
56
58
  /**
57
- * The Grafana folder to provision it into. Written as a subdirectory of
58
- * `dashboards/`, which the provider maps to folders
59
- * (`foldersFromFilesStructure`). `"Platform/Kubernetes"` nests one
60
- * directory in another, which Grafana 13.1 and later make a Kubernetes
61
- * folder inside Platform; earlier versions use only the last level
62
- * (GRAF109 warns). Leave it out for the General folder.
59
+ * Annotation queries: events drawn on the dashboard's time series panels
60
+ * (deploys, incidents), each from a datasource. Grafana adds its own
61
+ * "Annotations & Alerts" query to every dashboard that has none with
62
+ * `builtIn: 1`, so leave that one out unless you change it.
63
63
  */
64
- folder?: string;
64
+ annotations?: AnnotationInput[];
65
+ /**
66
+ * The Grafana folder: a path (`"Platform/Kubernetes"`) or a `Folder`,
67
+ * which pins the folder's uid. Written as a subdirectory of `dashboards/`,
68
+ * which the provider maps to folders (`foldersFromFilesStructure`); a path
69
+ * nests one directory in another, which Grafana 13.1 and later make a
70
+ * Kubernetes folder inside Platform, and earlier versions use only the
71
+ * last level (GRAF109 warns). The API applier creates each level with its
72
+ * uid. Leave it out for the General folder.
73
+ */
74
+ folder?: string | FolderEntity;
65
75
  }
66
76
 
77
+ /** A dashboard's props as the entity holds them: `folder` is always the path, and a `Folder` given for it is `folderEntity`. */
78
+ export type DashboardEntityProps = Omit<DashboardProps, "folder"> & { folder?: string };
79
+
67
80
  export interface DashboardEntity extends Declarable {
68
- readonly props: DashboardProps;
81
+ readonly props: DashboardEntityProps;
82
+ /** The `Folder` the dashboard was given, when it was given one rather than a path. */
83
+ readonly folderEntity?: FolderEntity;
69
84
  }
70
85
 
71
86
  export const DASHBOARD_TYPE = "Grafana::Dashboard";
72
87
 
73
88
  const DashboardBase = createResource(DASHBOARD_TYPE, "grafana", {}) as unknown as (this: object, props: Record<string, unknown>) => void;
74
89
 
75
- /** A Grafana dashboard. */
90
+ /**
91
+ * A Grafana dashboard. A `Folder` given as its `folder` is held as the
92
+ * folder's path, so everything that reads the dashboard's props (the build,
93
+ * the checks, observation's diff) sees one form; the `Folder` itself, for its
94
+ * uid, is `folderEntity`.
95
+ */
76
96
  export const Dashboard = function (this: object, props: DashboardProps) {
97
+ const folder = props.folder;
98
+ if (isFolderEntity(folder)) {
99
+ DashboardBase.call(this, { ...props, folder: folder.path } as unknown as Record<string, unknown>);
100
+ Object.defineProperty(this, "folderEntity", { value: folder, enumerable: false });
101
+ return;
102
+ }
77
103
  DashboardBase.call(this, props as unknown as Record<string, unknown>);
78
104
  } as unknown as new (props: DashboardProps) => DashboardEntity;
79
105
  Object.defineProperty(Dashboard, "name", { value: "Dashboard" });
@@ -95,11 +121,14 @@ export interface DashboardProviderProps {
95
121
  name: string;
96
122
  orgId?: number;
97
123
  /**
98
- * Put every dashboard this provider loads in this folder. Setting it (or
99
- * `folderUid`) turns `foldersFromFilesStructure` off by default, so each
100
- * dashboard's own `folder` is ignored (GRAF109 warns).
124
+ * Put every dashboard this provider loads in this folder: a title, or a
125
+ * root-level `Folder`, whose title and uid are written as `folder` and
126
+ * `folderUid` (Grafana creates a provider's folder at the root, so a
127
+ * nested `Folder` is refused). Setting it (or `folderUid`) turns
128
+ * `foldersFromFilesStructure` off by default, so each dashboard's own
129
+ * `folder` is ignored (GRAF109 warns).
101
130
  */
102
- folder?: string;
131
+ folder?: string | FolderEntity;
103
132
  folderUid?: string;
104
133
  /** Where the `dashboards/` output is mounted in the Grafana container. Defaults to `/var/lib/grafana/dashboards`. */
105
134
  path?: string;
@@ -110,8 +139,11 @@ export interface DashboardProviderProps {
110
139
  updateIntervalSeconds?: number;
111
140
  }
112
141
 
142
+ /** A provider's props as the entity holds them: a `Folder` given for `folder` is written out as `folder` and `folderUid`. */
143
+ export type DashboardProviderEntityProps = Omit<DashboardProviderProps, "folder"> & { folder?: string };
144
+
113
145
  export interface DashboardProviderEntity extends Declarable {
114
- readonly props: DashboardProviderProps;
146
+ readonly props: DashboardProviderEntityProps;
115
147
  }
116
148
 
117
149
  export const DASHBOARD_PROVIDER_TYPE = "Grafana::DashboardProvider";
@@ -123,6 +155,19 @@ const ProviderBase = createResource(DASHBOARD_PROVIDER_TYPE, "grafana", {}) as u
123
155
 
124
156
  /** An entry in the dashboard provisioning file. Optional: a default one is written when none is declared. */
125
157
  export const DashboardProvider = function (this: object, props: DashboardProviderProps) {
158
+ const folder = props.folder;
159
+ if (isFolderEntity(folder)) {
160
+ if (folder.props.parent) {
161
+ throw new Error(
162
+ `grafana: DashboardProvider "${props.name}" puts its dashboards in the Folder "${folder.path}", which is nested; Grafana creates a provider's folder at the root. Give it a root-level Folder, or leave folder out and set each dashboard's folder`,
163
+ );
164
+ }
165
+ if (props.folderUid !== undefined && props.folderUid !== folder.uid) {
166
+ throw new Error(`grafana: DashboardProvider "${props.name}" has folderUid "${props.folderUid}", but its Folder's uid is "${folder.uid}"`);
167
+ }
168
+ ProviderBase.call(this, { ...props, folder: folder.props.title, folderUid: folder.uid } as unknown as Record<string, unknown>);
169
+ return;
170
+ }
126
171
  ProviderBase.call(this, props as unknown as Record<string, unknown>);
127
172
  } as unknown as new (props: DashboardProviderProps) => DashboardProviderEntity;
128
173
  Object.defineProperty(DashboardProvider, "name", { value: "DashboardProvider" });
@@ -48,14 +48,15 @@ export type ResolvedDatasource =
48
48
 
49
49
  /** One place a dashboard names, or inherits, a datasource. */
50
50
  export interface DatasourceUse {
51
- /** A panel's own `datasource`, one of its queries (`targets`), or a query variable. */
52
- kind: "panel" | "query" | "variable";
51
+ /** A panel's own `datasource`, one of its queries (`targets`), a query, ad hoc or group by variable, or an annotation query. */
52
+ kind: "panel" | "query" | "variable" | "annotation";
53
53
  /** A human description, e.g. `panel "Latency" (id 3) query B`. */
54
54
  where: string;
55
55
  panel?: Json;
56
56
  target?: Json;
57
57
  variable?: Json;
58
- /** The ref written on this panel, query or variable; unset when a query inherits its panel's. */
58
+ annotation?: Json;
59
+ /** The ref written on this panel, query, variable or annotation; unset when a query inherits its panel's. */
59
60
  ref?: DatasourceRefJson;
60
61
  /** Where the request goes: the query's own ref, else its panel's. */
61
62
  resolved: ResolvedDatasource;
@@ -146,10 +147,20 @@ export function resolveDatasourceRef(
146
147
  return { kind: "undeclared", uid: ref.uid, ...(ref.type ? { type: ref.type } : {}) };
147
148
  }
148
149
 
150
+ /** Variable types that send requests to a datasource of their own: a query variable's query, an ad hoc or group by variable's key and value lookups. */
151
+ const DATASOURCE_VARIABLE_TYPES: ReadonlySet<string> = new Set(["query", "adhoc", "groupby"]);
152
+
153
+ /** A dashboard's `annotations.list`. */
154
+ export function annotationsOf(dashboard: Json): Json[] {
155
+ const list = (dashboard.annotations as { list?: unknown } | undefined)?.list;
156
+ return Array.isArray(list) ? (list as Json[]).filter((a) => a && typeof a === "object" && !Array.isArray(a)) : [];
157
+ }
158
+
149
159
  /**
150
160
  * Every place a dashboard names or inherits a datasource: each panel's own
151
- * ref, each query (with its own ref, or its panel's when it has none), and
152
- * each query variable. Rows are left out; their panels carry the ref.
161
+ * ref, each query (with its own ref, or its panel's when it has none), each
162
+ * query, ad hoc and group by variable, and each annotation query with a datasource. Rows are left
163
+ * out; their panels carry the ref.
153
164
  */
154
165
  export function datasourceUses(dashboard: Json, known: ReadonlyMap<string, KnownDatasource>): DatasourceUse[] {
155
166
  const dsVars = datasourceVariables(dashboard);
@@ -175,7 +186,7 @@ export function datasourceUses(dashboard: Json, known: ReadonlyMap<string, Known
175
186
  }
176
187
  }
177
188
  for (const variable of variablesOf(dashboard)) {
178
- if (variable.type !== "query") continue;
189
+ if (!DATASOURCE_VARIABLE_TYPES.has(String(variable.type))) continue;
179
190
  const ref = refOf(variable.datasource);
180
191
  out.push({
181
192
  kind: "variable",
@@ -185,5 +196,18 @@ export function datasourceUses(dashboard: Json, known: ReadonlyMap<string, Known
185
196
  resolved: resolveDatasourceRef(ref, dsVars, known),
186
197
  });
187
198
  }
199
+ for (const annotation of annotationsOf(dashboard)) {
200
+ const ref = refOf(annotation.datasource);
201
+ if (!ref) continue;
202
+ const target = refOf(annotation.target) as Json | undefined;
203
+ out.push({
204
+ kind: "annotation",
205
+ where: `annotation "${String(annotation.name ?? "?")}"`,
206
+ annotation,
207
+ ...(target ? { target } : {}),
208
+ ref,
209
+ resolved: resolveDatasourceRef(ref, dsVars, known),
210
+ });
211
+ }
188
212
  return out;
189
213
  }