@intentius/chant-lexicon-grafana 0.99.0 → 0.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +12 -6
  2. package/dist/annotations.d.ts +61 -0
  3. package/dist/annotations.d.ts.map +1 -0
  4. package/dist/api/apply.d.ts +8 -4
  5. package/dist/api/apply.d.ts.map +1 -1
  6. package/dist/api/fake-grafana.d.ts +2 -0
  7. package/dist/api/fake-grafana.d.ts.map +1 -1
  8. package/dist/api/folders.d.ts +53 -17
  9. package/dist/api/folders.d.ts.map +1 -1
  10. package/dist/build.d.ts +26 -5
  11. package/dist/build.d.ts.map +1 -1
  12. package/dist/catalog.d.ts +1 -1
  13. package/dist/catalog.d.ts.map +1 -1
  14. package/dist/codegen/docs.d.ts.map +1 -1
  15. package/dist/codegen/generate.d.ts +7 -1
  16. package/dist/codegen/generate.d.ts.map +1 -1
  17. package/dist/composites/shared.d.ts +4 -3
  18. package/dist/composites/shared.d.ts.map +1 -1
  19. package/dist/dashboard.d.ts +42 -14
  20. package/dist/dashboard.d.ts.map +1 -1
  21. package/dist/datasource-refs.d.ts +9 -5
  22. package/dist/datasource-refs.d.ts.map +1 -1
  23. package/dist/datasource-settings.d.ts +834 -0
  24. package/dist/datasource-settings.d.ts.map +1 -0
  25. package/dist/datasource.d.ts +53 -11
  26. package/dist/datasource.d.ts.map +1 -1
  27. package/dist/deep-observe-hooks.d.ts +1 -1
  28. package/dist/deep-observe-hooks.d.ts.map +1 -1
  29. package/dist/deep-observe.d.ts +9 -4
  30. package/dist/deep-observe.d.ts.map +1 -1
  31. package/dist/describe-resources.d.ts +1 -0
  32. package/dist/describe-resources.d.ts.map +1 -1
  33. package/dist/detect.d.ts +4 -2
  34. package/dist/detect.d.ts.map +1 -1
  35. package/dist/folder.d.ts +68 -0
  36. package/dist/folder.d.ts.map +1 -0
  37. package/dist/import/generator.d.ts.map +1 -1
  38. package/dist/import/mappings.d.ts +12 -6
  39. package/dist/import/mappings.d.ts.map +1 -1
  40. package/dist/import/model.d.ts +7 -0
  41. package/dist/import/model.d.ts.map +1 -1
  42. package/dist/import/normalize.d.ts.map +1 -1
  43. package/dist/import/parser.d.ts +16 -1
  44. package/dist/import/parser.d.ts.map +1 -1
  45. package/dist/import/testdata/fixtures.d.ts +6 -0
  46. package/dist/import/testdata/fixtures.d.ts.map +1 -1
  47. package/dist/import/v2.d.ts +4 -3
  48. package/dist/import/v2.d.ts.map +1 -1
  49. package/dist/index.d.ts +11 -9
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/init-templates.d.ts +27 -0
  52. package/dist/init-templates.d.ts.map +1 -0
  53. package/dist/integrity.json +11 -9
  54. package/dist/lint/audit-catalog.d.ts +1 -1
  55. package/dist/lint/audit-catalog.d.ts.map +1 -1
  56. package/dist/lint/post-synth/graf101.d.ts +1 -1
  57. package/dist/lint/post-synth/graf104.d.ts +2 -2
  58. package/dist/lint/post-synth/graf106.d.ts +2 -2
  59. package/dist/lint/post-synth/graf110.d.ts +8 -0
  60. package/dist/lint/post-synth/graf110.d.ts.map +1 -0
  61. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -1
  62. package/dist/lint/post-synth/index.d.ts.map +1 -1
  63. package/dist/manifest.json +1 -1
  64. package/dist/meta.json +70 -0
  65. package/dist/okf/index.md +18 -3
  66. package/dist/okf/rules/GRAF101.md +2 -2
  67. package/dist/okf/rules/GRAF104.md +2 -2
  68. package/dist/okf/rules/GRAF106.md +2 -2
  69. package/dist/okf/rules/GRAF110.md +11 -0
  70. package/dist/okf/types/AdhocVariable.md +9 -0
  71. package/dist/okf/types/AzureMonitorQuery.md +9 -0
  72. package/dist/okf/types/BigQueryQuery.md +9 -0
  73. package/dist/okf/types/CloudMonitoringQuery.md +9 -0
  74. package/dist/okf/types/CloudWatchQuery.md +9 -0
  75. package/dist/okf/types/Dashboard.md +1 -1
  76. package/dist/okf/types/Datasource.md +1 -1
  77. package/dist/okf/types/DatasourceProvisioning.md +9 -0
  78. package/dist/okf/types/ElasticsearchQuery.md +9 -0
  79. package/dist/okf/types/ExternalDatasource.md +1 -1
  80. package/dist/okf/types/Folder.md +9 -0
  81. package/dist/okf/types/GroupByVariable.md +9 -0
  82. package/dist/okf/types/MSSQLQuery.md +9 -0
  83. package/dist/okf/types/MySQLQuery.md +9 -0
  84. package/dist/okf/types/PostgresQuery.md +9 -0
  85. package/dist/okf/types/PyroscopeQuery.md +9 -0
  86. package/dist/okf/types/SwitchVariable.md +9 -0
  87. package/dist/op/activities/grafana-apply.d.ts +11 -0
  88. package/dist/op/activities/grafana-apply.d.ts.map +1 -1
  89. package/dist/op/activities/index.d.ts +1 -1
  90. package/dist/op/activities/index.d.ts.map +1 -1
  91. package/dist/ownership.d.ts +11 -0
  92. package/dist/ownership.d.ts.map +1 -1
  93. package/dist/pin.d.ts +1 -1
  94. package/dist/pin.d.ts.map +1 -1
  95. package/dist/plugin.d.ts.map +1 -1
  96. package/dist/promql-check.d.ts.map +1 -1
  97. package/dist/query-models.d.ts +95 -0
  98. package/dist/query-models.d.ts.map +1 -0
  99. package/dist/query.d.ts +107 -10
  100. package/dist/query.d.ts.map +1 -1
  101. package/dist/rules/graf101.ts +2 -2
  102. package/dist/rules/graf104.ts +3 -3
  103. package/dist/rules/graf106.ts +3 -3
  104. package/dist/rules/graf110.ts +17 -0
  105. package/dist/rules/grafana-helpers.ts +13 -3
  106. package/dist/schema/azuremonitor.gen.d.ts +354 -0
  107. package/dist/schema/azuremonitor.gen.d.ts.map +1 -0
  108. package/dist/schema/bigquery.gen.d.ts +93 -0
  109. package/dist/schema/bigquery.gen.d.ts.map +1 -0
  110. package/dist/schema/cloudwatch.gen.d.ts +394 -0
  111. package/dist/schema/cloudwatch.gen.d.ts.map +1 -0
  112. package/dist/schema/elasticsearch.gen.d.ts +444 -0
  113. package/dist/schema/elasticsearch.gen.d.ts.map +1 -0
  114. package/dist/schema/googlecloudmonitoring.gen.d.ts +312 -0
  115. package/dist/schema/googlecloudmonitoring.gen.d.ts.map +1 -0
  116. package/dist/schema/grafanapyroscope.gen.d.ts +60 -0
  117. package/dist/schema/grafanapyroscope.gen.d.ts.map +1 -0
  118. package/dist/schema/index.d.ts +6 -0
  119. package/dist/schema/index.d.ts.map +1 -1
  120. package/dist/schema/table.gen.d.ts +13 -0
  121. package/dist/schema/table.gen.d.ts.map +1 -1
  122. package/dist/schema-validate.d.ts +9 -0
  123. package/dist/schema-validate.d.ts.map +1 -1
  124. package/dist/skill-defs.d.ts.map +1 -1
  125. package/dist/skills/chant-grafana-operations.md +108 -0
  126. package/dist/skills/chant-grafana.md +5 -4
  127. package/dist/spec/schemas.d.ts +9 -7
  128. package/dist/spec/schemas.d.ts.map +1 -1
  129. package/dist/spec/schemas.gen.d.ts +3 -0
  130. package/dist/spec/schemas.gen.d.ts.map +1 -0
  131. package/dist/validate-alerting.d.ts.map +1 -1
  132. package/dist/validate-output.d.ts +15 -2
  133. package/dist/validate-output.d.ts.map +1 -1
  134. package/dist/validate.d.ts.map +1 -1
  135. package/dist/validation.d.ts +14 -0
  136. package/dist/validation.d.ts.map +1 -0
  137. package/dist/variables.d.ts +102 -5
  138. package/dist/variables.d.ts.map +1 -1
  139. package/package.json +2 -2
  140. package/src/annotations.ts +84 -0
  141. package/src/api/apply.test.ts +19 -1
  142. package/src/api/apply.ts +18 -8
  143. package/src/api/fake-grafana.ts +6 -2
  144. package/src/api/folders.ts +106 -32
  145. package/src/build.test.ts +142 -6
  146. package/src/build.ts +126 -24
  147. package/src/catalog.ts +20 -2
  148. package/src/codegen/docs.ts +13 -6
  149. package/src/codegen/generate.ts +27 -1
  150. package/src/composites/shared.ts +3 -2
  151. package/src/dashboard.ts +59 -14
  152. package/src/datasource-refs.ts +30 -6
  153. package/src/datasource-settings.test.ts +106 -0
  154. package/src/datasource-settings.ts +941 -0
  155. package/src/datasource.ts +71 -11
  156. package/src/deep-observe-hooks.ts +16 -1
  157. package/src/deep-observe.test.ts +47 -1
  158. package/src/deep-observe.ts +36 -13
  159. package/src/describe-resources.test.ts +16 -0
  160. package/src/describe-resources.ts +36 -1
  161. package/src/detect.ts +3 -3
  162. package/src/folder.ts +118 -0
  163. package/src/generated/lexicon-grafana.json +70 -0
  164. package/src/import/generated-types.e2e.test.ts +15 -3
  165. package/src/import/generator.ts +2 -1
  166. package/src/import/mappings.ts +98 -26
  167. package/src/import/model.ts +7 -0
  168. package/src/import/normalize.ts +32 -3
  169. package/src/import/parser.test.ts +97 -21
  170. package/src/import/parser.ts +169 -18
  171. package/src/import/roundtrip.test.ts +104 -9
  172. package/src/import/testdata/fixtures.ts +8 -0
  173. package/src/import/v2.ts +4 -3
  174. package/src/index.ts +122 -13
  175. package/src/init-templates.test.ts +74 -0
  176. package/src/init-templates.ts +349 -0
  177. package/src/lint/audit-catalog.ts +9 -1
  178. package/src/lint/post-synth/graf101.ts +2 -2
  179. package/src/lint/post-synth/graf104.ts +3 -3
  180. package/src/lint/post-synth/graf106.ts +3 -3
  181. package/src/lint/post-synth/graf110.ts +17 -0
  182. package/src/lint/post-synth/grafana-helpers.ts +13 -3
  183. package/src/lint/post-synth/index.ts +2 -0
  184. package/src/lint/post-synth/post-synth.test.ts +99 -2
  185. package/src/load-cost.test.ts +35 -0
  186. package/src/op/activities/grafana-apply.e2e.test.ts +14 -5
  187. package/src/op/activities/grafana-apply.test.ts +16 -2
  188. package/src/op/activities/grafana-apply.ts +29 -3
  189. package/src/op/activities/index.ts +1 -1
  190. package/src/ownership.ts +19 -0
  191. package/src/panels.test.ts +3 -1
  192. package/src/pin.ts +12 -0
  193. package/src/plugin.test.ts +9 -2
  194. package/src/plugin.ts +9 -0
  195. package/src/promql-check.ts +5 -1
  196. package/src/query-models.ts +94 -0
  197. package/src/query.test.ts +166 -0
  198. package/src/query.ts +177 -12
  199. package/src/schema/azuremonitor.gen.ts +381 -0
  200. package/src/schema/bigquery.gen.ts +110 -0
  201. package/src/schema/cloudwatch.gen.ts +423 -0
  202. package/src/schema/elasticsearch.gen.ts +508 -0
  203. package/src/schema/googlecloudmonitoring.gen.ts +330 -0
  204. package/src/schema/grafanapyroscope.gen.ts +66 -0
  205. package/src/schema/index.ts +6 -0
  206. package/src/schema/table.gen.ts +14 -0
  207. package/src/schema-validate.ts +58 -7
  208. package/src/skill-defs.ts +17 -0
  209. package/src/skills/chant-grafana-operations.md +108 -0
  210. package/src/skills/chant-grafana.md +5 -4
  211. package/src/spec/overlay/table.overlay.json +19 -0
  212. package/src/spec/schemas/azuremonitor.jsonschema.json +679 -0
  213. package/src/spec/schemas/bigquery.jsonschema.json +290 -0
  214. package/src/spec/schemas/cloudwatch.jsonschema.json +721 -0
  215. package/src/spec/schemas/elasticsearch.jsonschema.json +1695 -0
  216. package/src/spec/schemas/googlecloudmonitoring.jsonschema.json +488 -0
  217. package/src/spec/schemas/grafanapyroscope.jsonschema.json +93 -0
  218. package/src/spec/schemas.gen.ts +40 -0
  219. package/src/spec/schemas.ts +9 -7
  220. package/src/validate-alerting.ts +4 -2
  221. package/src/validate-output.ts +96 -4
  222. package/src/validate.ts +14 -0
  223. package/src/validation.ts +27 -0
  224. package/src/variables.ts +113 -6
@@ -1,17 +1,19 @@
1
1
  /**
2
- * Folders, for the API applier (#2948). Every folder decision the applier
3
- * makes is in this module: which folders a build needs, the uid each gets,
4
- * how one is read, written, listed and deleted, and on which API.
2
+ * Folders: the one place a build's folders become uids and parents, and how
3
+ * the API applier (#2948) and observation read and write them.
5
4
  *
6
- * ## What the lexicon models today
5
+ * ## What a build declares
7
6
  *
8
- * A dashboard names its folder by title (`Dashboard.folder`), and a title
9
- * holds no nesting (the build flattens `/` in it). So the applier derives
10
- * one root-level folder per distinct title, with a stable uid made from the
11
- * title ({@link folderUidFor}), and a second apply finds it again by that
12
- * uid. {@link FolderPlan} already carries `parentUid`, and the writers
13
- * already send it, so #2953 (folder uid and nesting on the declaration) only
14
- * has to change {@link foldersForDashboards}.
7
+ * A dashboard names its folder as a path (`Dashboard.folder:
8
+ * "Platform/Kubernetes"`) or as a `Folder` (../folder.ts), and a `Folder`
9
+ * may be declared on its own. {@link resolveFolders} turns all of it into
10
+ * one {@link FolderPlan} per folder: every level of every path, the uid a
11
+ * `Folder` pins or {@link folderUidFor} of its path, and its parent's uid.
12
+ * The build calls it and writes the result to its index (`folders`, and a
13
+ * `folderUid` on each dashboard); the applier reads the index and calls it
14
+ * again, which also covers an index written before folders were listed.
15
+ * Two folders at one path with different uids, or two paths with one uid,
16
+ * throw: no delivery could make both.
15
17
  *
16
18
  * ## Which API
17
19
  *
@@ -25,7 +27,9 @@
25
27
  */
26
28
 
27
29
  import { GrafanaApiError, type GrafanaClient } from "./client";
28
- import { slugUid } from "../util";
30
+ import { folderLevels, folderUidFor } from "../folder";
31
+
32
+ export { folderUidFor };
29
33
 
30
34
  type Json = Record<string, unknown>;
31
35
 
@@ -55,6 +59,8 @@ export interface FolderPlan {
55
59
  readonly title: string;
56
60
  /** The parent folder's uid; absent for a root-level folder. */
57
61
  readonly parentUid?: string;
62
+ /** Its titles from the root, joined with `/`: the directory file provisioning reads it from. */
63
+ readonly path?: string;
58
64
  }
59
65
 
60
66
  /** A folder as Grafana holds it. */
@@ -64,6 +70,8 @@ export interface LiveFolder {
64
70
  readonly parentUid?: string;
65
71
  /** `metadata.labels`; empty on the legacy API. */
66
72
  readonly labels: Readonly<Record<string, string>>;
73
+ /** `metadata.annotations`; empty on the legacy API. */
74
+ readonly annotations?: Readonly<Record<string, string>>;
67
75
  readonly via: "apis" | "legacy";
68
76
  /** The request path it lives at. */
69
77
  readonly address: string;
@@ -71,31 +79,74 @@ export interface LiveFolder {
71
79
  readonly version?: number;
72
80
  }
73
81
 
74
- /**
75
- * The uid chant gives the folder a dashboard names by title. Stable, so a
76
- * second apply finds the folder the first one made; Grafana's own limit is
77
- * 40 characters, which `slugUid` keeps to.
78
- */
79
- export function folderUidFor(title: string): string {
80
- return slugUid(title);
82
+ /** The folders a build resolves to, and the uid of each path. */
83
+ export interface ResolvedFolders {
84
+ /** One per folder, sorted by path, so a parent comes before its children. */
85
+ readonly folders: FolderPlan[];
86
+ /** The uid of the folder at `path` (any spelling `folderLevels` reads the same); undefined for the General folder. */
87
+ uidOf(path: string | undefined): string | undefined;
81
88
  }
82
89
 
83
90
  /**
84
- * The folders a set of dashboards needs, one per distinct title, sorted by
85
- * uid. The one place a dashboard's folder becomes a folder plan.
91
+ * Every folder a build needs, with its uid and parent: the folders declared
92
+ * with a uid (`Folder`s, or dashboards already resolved), and every level of
93
+ * every path a dashboard names. A level no declaration pins gets
94
+ * {@link folderUidFor} of its path. Throws when two declarations put
95
+ * different uids at one path, and when two paths would get one uid unless
96
+ * `duplicateUids` is `keep`: the build keeps both and GRAF104 reports them,
97
+ * since file provisioning, which gives folders uids of its own, can still
98
+ * deliver them.
86
99
  */
87
- export function foldersForDashboards(dashboards: Iterable<{ folder?: string }>): FolderPlan[] {
88
- const byUid = new Map<string, FolderPlan>();
89
- for (const d of dashboards) {
90
- if (!d.folder) continue;
91
- const uid = folderUidFor(d.folder);
92
- const seen = byUid.get(uid);
93
- if (seen && seen.title !== d.folder) {
94
- throw new Error(`grafana apply: the folders "${seen.title}" and "${d.folder}" would both get the uid "${uid}"; rename one`);
100
+ export function resolveFolders(
101
+ input: { declared?: Iterable<{ uid: string; path: string }>; paths?: Iterable<string | undefined> },
102
+ options: { duplicateUids?: "throw" | "keep" } = {},
103
+ ): ResolvedFolders {
104
+ const byPath = new Map<string, string>();
105
+ const key = (path: string) => folderLevels(path).join("/");
106
+ for (const d of input.declared ?? []) {
107
+ const path = key(d.path);
108
+ if (path === "") continue;
109
+ const prior = byPath.get(path);
110
+ if (prior !== undefined && prior !== d.uid) {
111
+ throw new Error(`grafana: the folder "${path}" is declared with the uids "${prior}" and "${d.uid}"; a folder has one uid`);
95
112
  }
96
- byUid.set(uid, { uid, title: d.folder });
113
+ byPath.set(path, d.uid);
114
+ }
115
+ const addPath = (path: string) => {
116
+ const levels = folderLevels(path);
117
+ for (let i = 1; i <= levels.length; i++) {
118
+ const prefix = levels.slice(0, i).join("/");
119
+ if (!byPath.has(prefix)) byPath.set(prefix, folderUidFor(prefix));
120
+ }
121
+ };
122
+ for (const path of [...byPath.keys()]) addPath(path);
123
+ for (const path of input.paths ?? []) if (path) addPath(path);
124
+
125
+ const byUid = new Map<string, string>();
126
+ const folders: FolderPlan[] = [];
127
+ for (const path of [...byPath.keys()].sort((a, b) => a.localeCompare(b))) {
128
+ const uid = byPath.get(path)!;
129
+ const seen = byUid.get(uid);
130
+ if (seen !== undefined && options.duplicateUids !== "keep") throw new Error(`grafana: the folders "${seen}" and "${path}" would both get the uid "${uid}"; give one a Folder with its own uid`);
131
+ byUid.set(uid, path);
132
+ const levels = path.split("/");
133
+ const parentUid = levels.length > 1 ? byPath.get(levels.slice(0, -1).join("/")) : undefined;
134
+ folders.push({ uid, title: levels[levels.length - 1], ...(parentUid ? { parentUid } : {}), path });
97
135
  }
98
- return [...byUid.values()].sort((a, b) => a.uid.localeCompare(b.uid));
136
+ return { folders, uidOf: (path) => (path === undefined ? undefined : byPath.get(key(path))) };
137
+ }
138
+
139
+ /**
140
+ * The folders a set of dashboards needs: every level of every folder they
141
+ * name, sorted by path. A dashboard that already carries its `folderUid`
142
+ * pins its folder's uid.
143
+ */
144
+ export function foldersForDashboards(dashboards: Iterable<{ folder?: string; folderUid?: string }>): FolderPlan[] {
145
+ const list = [...dashboards];
146
+ return resolveFolders({
147
+ declared: list.filter((d) => d.folder && d.folderUid).map((d) => ({ uid: d.folderUid!, path: d.folder! })),
148
+ paths: list.map((d) => d.folder),
149
+ }).folders;
99
150
  }
100
151
 
101
152
  /** The folder API the server behind `client` serves, discovered once per client. */
@@ -120,12 +171,14 @@ export function folderPath(api: FolderApi, namespace: string, uid: string): stri
120
171
  function fromResource(resource: Json, address: string): LiveFolder {
121
172
  const metadata = isObject(resource.metadata) ? resource.metadata : {};
122
173
  const spec = isObject(resource.spec) ? resource.spec : {};
123
- const parentUid = stringMap(metadata.annotations)[FOLDER_ANNOTATION];
174
+ const annotations = stringMap(metadata.annotations);
175
+ const parentUid = annotations[FOLDER_ANNOTATION];
124
176
  return {
125
177
  uid: typeof metadata.name === "string" ? metadata.name : "",
126
178
  title: typeof spec.title === "string" ? spec.title : "",
127
179
  ...(parentUid ? { parentUid } : {}),
128
180
  labels: stringMap(metadata.labels),
181
+ annotations,
129
182
  via: "apis",
130
183
  address,
131
184
  };
@@ -152,6 +205,27 @@ export async function readFolder(client: GrafanaClient, uid: string): Promise<Li
152
205
  return api.kind === "apis" ? fromResource(body, address) : fromLegacy(body, address);
153
206
  }
154
207
 
208
+ /**
209
+ * The path of the folder with this uid, its titles from the root joined with
210
+ * `/`, the form `Dashboard.folder` takes. One read per folder per client;
211
+ * undefined when the folder, or one of its parents, cannot be found.
212
+ */
213
+ export async function liveFolderPath(client: GrafanaClient, uid: string): Promise<string | undefined> {
214
+ const titles: string[] = [];
215
+ const seen = new Set<string>();
216
+ for (let at: string | undefined = uid; at; ) {
217
+ // Grafana's own limit on nesting is 8 levels; a longer chain is a cycle.
218
+ if (seen.has(at) || seen.size > 8) return undefined;
219
+ seen.add(at);
220
+ const id: string = at;
221
+ const folder = await client.once(`folder:${id}`, () => readFolder(client, id));
222
+ if (!folder) return undefined;
223
+ titles.unshift(folder.title);
224
+ at = folder.parentUid;
225
+ }
226
+ return titles.join("/");
227
+ }
228
+
155
229
  /** Every folder in the organisation over `/apis`, paged. The legacy API has no labels to list by, so it is not listed. */
156
230
  export async function listFolders(client: GrafanaClient): Promise<LiveFolder[]> {
157
231
  const api = await folderApi(client);
package/src/build.test.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  import { describe, expect, test } from "vitest";
2
- import { Datasource } from "./datasource";
3
- import { Dashboard } from "./dashboard";
2
+ import { Datasource, DatasourceProvisioning, ExternalDatasource } from "./datasource";
3
+ import { Dashboard, DashboardProvider } from "./dashboard";
4
+ import { Folder } from "./folder";
5
+ import { load } from "js-yaml";
4
6
  import { Row, StatPanel, TextPanel, TimeSeriesPanel, TablePanel, definePanel } from "./panels";
5
7
  import { PromQuery, TempoQuery, LokiQuery, defineQuery } from "./query";
6
8
  import {
9
+ AdhocVariable,
7
10
  CustomVariable,
8
11
  ConstantVariable,
9
12
  DatasourceVariable,
13
+ GroupByVariable,
10
14
  IntervalVariable,
11
15
  QueryVariable,
16
+ SwitchVariable,
12
17
  TextboxVariable,
13
18
  } from "./variables";
14
19
  import { panelsJson, renderDashboard, variableModel, dashboardJson, grafanaFiles, buildGrafana, customVariableOptions, type PanelJson } from "./build";
@@ -302,6 +307,53 @@ describe("variables", () => {
302
307
  expect(variableModel(new TextboxVariable({ name: "q" }))).toMatchObject({ type: "textbox", query: "" });
303
308
  });
304
309
 
310
+ test("an object query is written as it is, its text as the definition", () => {
311
+ const query = { qryType: 1 as const, query: "label_values(up, job)", refId: "PrometheusVariableQueryEditor-VariableQuery" };
312
+ expect(variableModel(new QueryVariable({ name: "job", datasource: prometheus, query }))).toMatchObject({ query, definition: "label_values(up, job)" });
313
+ expect(variableModel(new QueryVariable({ name: "ns", datasource: prometheus, query: { namespace: "AWS/EC2" }, definition: "Namespaces" }))).toMatchObject({
314
+ query: { namespace: "AWS/EC2" },
315
+ definition: "Namespaces",
316
+ });
317
+ });
318
+
319
+ test("ad hoc, group by and switch variables as Grafana writes them", () => {
320
+ const ds = new DatasourceVariable({ name: "ds", pluginType: "prometheus" });
321
+ expect(variableModel(new AdhocVariable({ name: "f", datasource: ds, filters: [{ key: "ns", operator: "=", value: "shop" }] }))).toEqual({
322
+ type: "adhoc",
323
+ name: "f",
324
+ datasource: { type: "prometheus", uid: "${ds}" },
325
+ filters: [{ key: "ns", operator: "=", value: "shop" }],
326
+ baseFilters: [],
327
+ });
328
+ expect(variableModel(new GroupByVariable({ name: "by", datasource: prometheus, options: ["pod", { text: "Node", value: "node" }], defaultValue: ["pod"] }))).toEqual({
329
+ type: "groupby",
330
+ name: "by",
331
+ datasource: { type: "prometheus", uid: prometheus.uid },
332
+ options: [
333
+ { text: "pod", value: "pod" },
334
+ { text: "Node", value: "node" },
335
+ ],
336
+ current: { text: ["pod"], value: ["pod"] },
337
+ defaultValue: { text: ["pod"], value: ["pod"] },
338
+ });
339
+ expect(variableModel(new SwitchVariable({ name: "on" }))).toEqual({
340
+ type: "switch",
341
+ name: "on",
342
+ current: { text: "false", value: "false" },
343
+ options: [
344
+ { text: "true", value: "true" },
345
+ { text: "false", value: "false" },
346
+ ],
347
+ });
348
+ expect(variableModel(new SwitchVariable({ name: "q", enabled: true, enabledValue: "0.99", disabledValue: "0.5" }))).toMatchObject({
349
+ current: { text: "0.99", value: "0.99" },
350
+ options: [
351
+ { text: "0.99", value: "0.99" },
352
+ { text: "0.5", value: "0.5" },
353
+ ],
354
+ });
355
+ });
356
+
305
357
  test("a custom value in Grafana's `text : value` syntax shows the text and sets the value (#2944)", () => {
306
358
  const v = variableModel(new CustomVariable({ name: "env", values: ["Production : prod", "Staging : stg", "dev"] }));
307
359
  expect(v).toMatchObject({
@@ -357,6 +409,10 @@ describe("variables", () => {
357
409
  new DatasourceVariable({ name: "ds", pluginType: "prometheus" }),
358
410
  new ConstantVariable({ name: "cluster", value: "x" }),
359
411
  new TextboxVariable({ name: "q", value: "y" }),
412
+ new QueryVariable({ name: "pods", datasource: prometheus, query: { qryType: 1, query: "label_values(kube_pod_info, pod)" } }),
413
+ new AdhocVariable({ name: "f", datasource: prometheus, baseFilters: [{ key: "cluster", operator: "=", value: "prod" }], defaultKeys: [{ text: "pod" }] }),
414
+ new GroupByVariable({ name: "by", datasource: prometheus, options: ["pod"] }),
415
+ new SwitchVariable({ name: "on", enabled: true }),
360
416
  ],
361
417
  });
362
418
  expect(validateDashboardSchema(renderDashboard(dash) as unknown as Record<string, unknown>)).toEqual([]);
@@ -366,16 +422,16 @@ describe("variables", () => {
366
422
  describe("extension points", () => {
367
423
  test("definePanel and defineQuery produce classes that render and lay out like built-ins", () => {
368
424
  const ClockPanel = definePanel<{ mode?: "time" | "countdown" }>()({ type: "grafana-clock-panel", className: "ClockPanel", defaultSize: { w: 8, h: 8 } });
369
- const ElasticQuery = defineQuery<{ query: string; refId?: string }>()({ datasourceType: "elasticsearch", className: "ElasticQuery" });
370
- const es = new Datasource({ name: "Logs ES", type: "elasticsearch" });
425
+ const OpenSearchQuery = defineQuery<{ query: string; refId?: string }>()({ datasourceType: "grafana-opensearch-datasource", className: "OpenSearchQuery" });
426
+ const os = new Datasource({ name: "Logs OS", type: "grafana-opensearch-datasource" });
371
427
  const json = renderDashboard(
372
- new Dashboard({ title: "Custom", panels: [new ClockPanel({ options: { mode: "countdown" }, datasource: es, targets: [new ElasticQuery({ query: "*" })] })] }),
428
+ new Dashboard({ title: "Custom", panels: [new ClockPanel({ options: { mode: "countdown" }, datasource: os, targets: [new OpenSearchQuery({ query: "*" })] })] }),
373
429
  );
374
430
  expect(json.panels![0]).toMatchObject({
375
431
  type: "grafana-clock-panel",
376
432
  gridPos: { w: 8, h: 8, x: 0, y: 0 },
377
433
  options: { mode: "countdown" },
378
- datasource: { type: "elasticsearch", uid: "logs-es" },
434
+ datasource: { type: "grafana-opensearch-datasource", uid: "logs-os" },
379
435
  targets: [{ refId: "A", query: "*" }],
380
436
  });
381
437
  expect(validateDashboardSchema(json as unknown as Record<string, unknown>)).toEqual([]);
@@ -413,3 +469,83 @@ describe("folders (#2944)", () => {
413
469
  expect(file(" / ")).toBe("dashboards/d.json");
414
470
  });
415
471
  });
472
+
473
+ describe("folder uids and nesting (#2953)", () => {
474
+ const platform = new Folder({ title: "Platform", uid: "plat" });
475
+ const k8s = new Folder({ title: "Kubernetes", parent: platform });
476
+
477
+ test("a Folder is written where its path is, and the index lists every level with its uid and parent", () => {
478
+ const built = buildGrafana(
479
+ new Map<string, never>([
480
+ ["pods", new Dashboard({ title: "Pods", uid: "pods", folder: k8s }) as never],
481
+ ["nodes", new Dashboard({ title: "Nodes", uid: "nodes", folder: "Platform/Kubernetes/Nodes" }) as never],
482
+ ["spare", new Folder({ title: "Spare" }) as never],
483
+ ]),
484
+ );
485
+ expect(built.dashboards.map((d) => [d.file, d.folder, d.folderUid])).toEqual([
486
+ ["dashboards/Platform/Kubernetes/pods.json", "Platform/Kubernetes", "platform-kubernetes"],
487
+ ["dashboards/Platform/Kubernetes/Nodes/nodes.json", "Platform/Kubernetes/Nodes", "platform-kubernetes-nodes"],
488
+ ]);
489
+ expect(built.index.folders).toEqual([
490
+ { uid: "plat", title: "Platform", path: "Platform" },
491
+ { uid: "platform-kubernetes", title: "Kubernetes", parentUid: "plat", path: "Platform/Kubernetes" },
492
+ { uid: "platform-kubernetes-nodes", title: "Nodes", parentUid: "platform-kubernetes", path: "Platform/Kubernetes/Nodes" },
493
+ { uid: "spare", title: "Spare", path: "Spare" },
494
+ ]);
495
+ });
496
+
497
+ test("a path and a Folder at the same path are one folder; the Folder's uid wins", () => {
498
+ const built = buildGrafana([new Dashboard({ title: "A", uid: "a", folder: "Platform" }), new Dashboard({ title: "B", uid: "b", folder: platform })]);
499
+ expect(built.dashboards.map((d) => d.folderUid)).toEqual(["plat", "plat"]);
500
+ expect(built.folders).toEqual([{ uid: "plat", title: "Platform", path: "Platform" }]);
501
+ });
502
+
503
+ test("a Folder title is one level, and two uids at one path are refused", () => {
504
+ expect(() => new Folder({ title: "Platform/Kubernetes" })).toThrow(/not one folder level/);
505
+ expect(() => buildGrafana([new Folder({ title: "X", uid: "one" }), new Folder({ title: "X", uid: "two" })])).toThrow(/declared with the uids "one" and "two"/);
506
+ });
507
+
508
+ test("a root-level Folder on a DashboardProvider is written as folder and folderUid; a nested one is refused", () => {
509
+ const built = buildGrafana([new DashboardProvider({ name: "ops", folder: platform, path: "/d" })]);
510
+ expect(built.providers[0]).toMatchObject({ folder: "Platform", folderUid: "plat", options: { foldersFromFilesStructure: false } });
511
+ expect(() => new DashboardProvider({ name: "ops", folder: k8s })).toThrow(/nested; Grafana creates a provider's folder at the root/);
512
+ });
513
+ });
514
+
515
+ describe("annotations (#2953)", () => {
516
+ test("an annotation is written with its datasource resolved and Grafana's defaults filled in", () => {
517
+ const json = renderDashboard(
518
+ new Dashboard({
519
+ title: "D",
520
+ annotations: [
521
+ { name: "Deploys", datasource: prometheus, target: new PromQuery({ expr: "changes(up[5m]) > 0" }), titleFormat: "deploy" },
522
+ { name: "Incidents", datasource: { type: "grafana", uid: "-- Grafana --" }, enable: false, iconColor: "blue", target: { type: "tags", tags: ["incident"], limit: 100, matchAny: false } },
523
+ ],
524
+ }),
525
+ );
526
+ expect(json.annotations).toEqual({
527
+ list: [
528
+ { datasource: { type: "prometheus", uid: "prometheus" }, enable: true, iconColor: "red", name: "Deploys", titleFormat: "deploy", target: expect.objectContaining({ refId: "Anno", expr: "changes(up[5m]) > 0" }) },
529
+ { datasource: { type: "grafana", uid: "-- Grafana --" }, enable: false, iconColor: "blue", name: "Incidents", target: { type: "tags", tags: ["incident"], limit: 100, matchAny: false } },
530
+ ],
531
+ });
532
+ expect(validateDashboardSchema(json as never).filter((p) => p.severity === "error")).toEqual([]);
533
+ expect(renderDashboard(new Dashboard({ title: "E" })).annotations).toEqual({ list: [] });
534
+ });
535
+ });
536
+
537
+ describe("datasource prune (#2953)", () => {
538
+ const files = (entities: unknown[]) => load(buildGrafana(entities as never).files["provisioning/datasources/chant.yaml"] ?? "null") as Record<string, unknown> | null;
539
+
540
+ test("the datasource file prunes by default, so a removed Datasource leaves Grafana", () => {
541
+ expect(files([prometheus])).toMatchObject({ apiVersion: 1, prune: true });
542
+ });
543
+
544
+ test("DatasourceProvisioning turns pruning off and lists datasources to delete", () => {
545
+ const out = files([prometheus, new DatasourceProvisioning({ prune: false, deleteDatasources: [{ name: "Old" }, { name: "Other", orgId: 2 }] })]);
546
+ expect(out).toEqual({ apiVersion: 1, deleteDatasources: [{ name: "Old" }, { name: "Other", orgId: 2 }], datasources: [expect.objectContaining({ name: "Prometheus" })] });
547
+ expect(files([new DatasourceProvisioning({ deleteDatasources: [{ name: "Old" }] })])).toEqual({ apiVersion: 1, prune: true, deleteDatasources: [{ name: "Old" }], datasources: [] });
548
+ expect(files([new ExternalDatasource({ type: "prometheus", uid: "p" })])).toBeNull();
549
+ expect(() => buildGrafana([new DatasourceProvisioning({}), new DatasourceProvisioning({})])).toThrow(/at most one DatasourceProvisioning/);
550
+ });
551
+ });