@intentius/chant-lexicon-grafana 0.124.30 → 0.124.32

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 (166) hide show
  1. package/dist/actions/grafana.js +36 -0
  2. package/dist/actions/index.js +1 -0
  3. package/dist/alerting-build.js +262 -0
  4. package/dist/alerting.js +125 -0
  5. package/dist/annotations.js +47 -0
  6. package/dist/api/apply.js +320 -0
  7. package/dist/api/bind.js +44 -0
  8. package/dist/api/client.js +113 -0
  9. package/dist/api/converge.js +61 -0
  10. package/dist/api/dashboards.js +210 -0
  11. package/dist/api/datasources.js +24 -0
  12. package/dist/api/fake-grafana-writes.js +185 -0
  13. package/dist/api/fake-grafana.js +161 -0
  14. package/dist/api/folders.js +290 -0
  15. package/dist/api/library-panels.js +82 -0
  16. package/dist/build.js +767 -0
  17. package/dist/catalog.js +198 -0
  18. package/dist/codegen/docs-cli.js +3 -0
  19. package/dist/codegen/docs.js +130 -0
  20. package/dist/codegen/generate-cli.js +7 -0
  21. package/dist/codegen/generate.js +97 -0
  22. package/dist/codegen/package.js +28 -0
  23. package/dist/codegen/schema-types.js +159 -0
  24. package/dist/composites/agent-dashboard.js +460 -0
  25. package/dist/composites/catalog.js +306 -0
  26. package/dist/composites/index.js +8 -0
  27. package/dist/composites/red-dashboard.js +135 -0
  28. package/dist/composites/shared.js +74 -0
  29. package/dist/composites/slo-alert-rules.js +96 -0
  30. package/dist/composites/slo-dashboard.js +153 -0
  31. package/dist/config.js +110 -0
  32. package/dist/contact-point-check.js +61 -0
  33. package/dist/contact-point-secrets.js +32 -0
  34. package/dist/contact-point-settings.gen.js +421 -0
  35. package/dist/dashboard.js +60 -0
  36. package/dist/datasource-refs.js +170 -0
  37. package/dist/datasource-settings.js +57 -0
  38. package/dist/datasource.js +68 -0
  39. package/dist/deep-observe-hooks.js +266 -0
  40. package/dist/deep-observe.js +171 -0
  41. package/dist/describe-resources.js +249 -0
  42. package/dist/detect.js +119 -0
  43. package/dist/export-resources.js +73 -0
  44. package/dist/folder.js +86 -0
  45. package/dist/import/alerting.js +514 -0
  46. package/dist/import/edits.js +90 -0
  47. package/dist/import/embedded.js +60 -0
  48. package/dist/import/generator.js +656 -0
  49. package/dist/import/live-export.js +246 -0
  50. package/dist/import/mappings.js +317 -0
  51. package/dist/import/model.js +23 -0
  52. package/dist/import/normalize-alerting.js +106 -0
  53. package/dist/import/normalize.js +426 -0
  54. package/dist/import/operator.js +252 -0
  55. package/dist/import/parser.js +1480 -0
  56. package/dist/import/report.js +58 -0
  57. package/dist/import/testdata/fixtures.js +122 -0
  58. package/dist/import/v2.js +814 -0
  59. package/dist/index.js +59 -0
  60. package/dist/init-templates.js +305 -0
  61. package/dist/integrity.json +2 -2
  62. package/dist/k8s.js +548 -0
  63. package/dist/library-panel.js +83 -0
  64. package/dist/lint/audit-catalog.js +40 -0
  65. package/dist/lint/audit-lineage.js +9 -0
  66. package/dist/lint/post-synth/graf101.js +13 -0
  67. package/dist/lint/post-synth/graf102.js +13 -0
  68. package/dist/lint/post-synth/graf103.js +13 -0
  69. package/dist/lint/post-synth/graf104.js +13 -0
  70. package/dist/lint/post-synth/graf105.js +13 -0
  71. package/dist/lint/post-synth/graf106.js +13 -0
  72. package/dist/lint/post-synth/graf107.js +13 -0
  73. package/dist/lint/post-synth/graf108.js +13 -0
  74. package/dist/lint/post-synth/graf109.js +13 -0
  75. package/dist/lint/post-synth/graf110.js +13 -0
  76. package/dist/lint/post-synth/graf111.js +13 -0
  77. package/dist/lint/post-synth/graf112.js +13 -0
  78. package/dist/lint/post-synth/graf113.js +13 -0
  79. package/dist/lint/post-synth/graf114.js +13 -0
  80. package/dist/lint/post-synth/graf115.js +13 -0
  81. package/dist/lint/post-synth/graf116.js +13 -0
  82. package/dist/lint/post-synth/graf117.js +13 -0
  83. package/dist/lint/post-synth/graf118.js +37 -0
  84. package/dist/lint/post-synth/grafana-helpers.js +89 -0
  85. package/dist/lint/post-synth/index.js +38 -0
  86. package/dist/lint/rules/grafana-ast.js +70 -0
  87. package/dist/lint/rules/index.js +6 -0
  88. package/dist/lint/rules/literal-secret.js +109 -0
  89. package/dist/lint/rules/uid-syntax.js +53 -0
  90. package/dist/lsp/completions.js +11 -0
  91. package/dist/lsp/hover.js +29 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/op/activities/grafana-apply.js +147 -0
  94. package/dist/op/activities/index.js +7 -0
  95. package/dist/op/builders.js +19 -0
  96. package/dist/ownership.js +102 -0
  97. package/dist/package-cli.js +14 -0
  98. package/dist/panel-options.js +12 -0
  99. package/dist/panels.js +296 -0
  100. package/dist/pin.js +113 -0
  101. package/dist/plugin.js +172 -0
  102. package/dist/promql-check.js +176 -0
  103. package/dist/query-models.js +13 -0
  104. package/dist/query-syntax.js +129 -0
  105. package/dist/query.js +227 -0
  106. package/dist/schema/annotationslist.gen.js +5 -0
  107. package/dist/schema/azuremonitor.gen.js +5 -0
  108. package/dist/schema/barchart.gen.js +5 -0
  109. package/dist/schema/bargauge.gen.js +5 -0
  110. package/dist/schema/bigquery.gen.js +5 -0
  111. package/dist/schema/candlestick.gen.js +5 -0
  112. package/dist/schema/canvas.gen.js +5 -0
  113. package/dist/schema/cloudwatch.gen.js +5 -0
  114. package/dist/schema/dashboard.gen.js +6 -0
  115. package/dist/schema/dashboardlist.gen.js +5 -0
  116. package/dist/schema/datagrid.gen.js +5 -0
  117. package/dist/schema/elasticsearch.gen.js +5 -0
  118. package/dist/schema/expr.gen.js +5 -0
  119. package/dist/schema/gauge.gen.js +5 -0
  120. package/dist/schema/geomap.gen.js +5 -0
  121. package/dist/schema/googlecloudmonitoring.gen.js +5 -0
  122. package/dist/schema/grafanapyroscope.gen.js +5 -0
  123. package/dist/schema/heatmap.gen.js +5 -0
  124. package/dist/schema/histogram.gen.js +5 -0
  125. package/dist/schema/index.js +40 -0
  126. package/dist/schema/logs.gen.js +5 -0
  127. package/dist/schema/loki.gen.js +5 -0
  128. package/dist/schema/news.gen.js +5 -0
  129. package/dist/schema/nodegraph.gen.js +5 -0
  130. package/dist/schema/piechart.gen.js +5 -0
  131. package/dist/schema/prometheus.gen.js +5 -0
  132. package/dist/schema/stat.gen.js +5 -0
  133. package/dist/schema/statetimeline.gen.js +5 -0
  134. package/dist/schema/statushistory.gen.js +5 -0
  135. package/dist/schema/table.gen.js +5 -0
  136. package/dist/schema/tempo.gen.js +5 -0
  137. package/dist/schema/text.gen.js +5 -0
  138. package/dist/schema/timeseries.gen.js +5 -0
  139. package/dist/schema/trend.gen.js +5 -0
  140. package/dist/schema/xychart.gen.js +5 -0
  141. package/dist/schema-validate.js +265 -0
  142. package/dist/serializer.js +40 -0
  143. package/dist/skill-defs.js +60 -0
  144. package/dist/spec/fetch-cli.js +26 -0
  145. package/dist/spec/fetch-notifiers-cli.js +53 -0
  146. package/dist/spec/fetch-units-cli.js +40 -0
  147. package/dist/spec/notifiers.js +126 -0
  148. package/dist/spec/overlay.js +92 -0
  149. package/dist/spec/schemas.gen.js +42 -0
  150. package/dist/spec/schemas.js +62 -0
  151. package/dist/spec/units.gen.js +280 -0
  152. package/dist/spec/units.js +72 -0
  153. package/dist/transformations.js +127 -0
  154. package/dist/util.js +34 -0
  155. package/dist/validate-alerting.js +444 -0
  156. package/dist/validate-cli.js +7 -0
  157. package/dist/validate-output.js +635 -0
  158. package/dist/validate.js +122 -0
  159. package/dist/validation.js +13 -0
  160. package/dist/variables.js +70 -0
  161. package/package.json +10 -10
  162. package/src/codegen/package.ts +2 -2
  163. package/src/spec/fetch-notifiers-cli.ts +2 -2
  164. package/src/spec/fetch-units-cli.ts +2 -2
  165. package/src/spec/overlay.ts +2 -2
  166. package/src/spec/schemas.ts +2 -2
package/dist/build.js ADDED
@@ -0,0 +1,767 @@
1
+ /**
2
+ * From declared entities to Grafana's own files: dashboard JSON, the
3
+ * datasource provisioning file and the dashboard provisioning file.
4
+ *
5
+ * Plain functions, so another lexicon (a k8s ConfigMap, a docker volume)
6
+ * can render the same files with `grafanaFiles()` or one dashboard with
7
+ * `dashboardJson()`, and the serializer is a thin wrapper around
8
+ * `buildGrafana()`.
9
+ */
10
+ import { dump } from "js-yaml";
11
+ import { isDatasourceDeclaration, isDatasourceEntity, isDatasourceProvisioningEntity, isExternalDatasource, } from "./datasource.js";
12
+ import { isDashboardEntity, isDashboardProviderEntity, DEFAULT_DASHBOARDS_PATH, } from "./dashboard.js";
13
+ import { annotationJson } from "./annotations.js";
14
+ import { folderLevels, folderUidFor, isFolderEntity } from "./folder.js";
15
+ import { resolveFolders } from "./api/folders.js";
16
+ import { isPanelEntity, isRowEntity } from "./panels.js";
17
+ import { isLibraryPanelEntity, isLibraryPanelRefEntity, referencedLibraryPanel, } from "./library-panel.js";
18
+ import { LIBRARY_PANEL_KIND } from "./api/library-panels.js";
19
+ import { isQueryEntity } from "./query.js";
20
+ import { isDatasourceVariable, isVariableEntity } from "./variables.js";
21
+ import { DASHBOARD_SCHEMA_VERSION } from "./schema/dashboard.gen.js";
22
+ import { GRAFANA_SCHEMA_PIN } from "./pin.js";
23
+ import { compact, slugUid } from "./util.js";
24
+ import { ALERTING_FILE, alertingYaml, buildAlerting } from "./alerting-build.js";
25
+ import { DEFAULT_PROVIDER_NAME } from "./ownership.js";
26
+ /** Grafana's grid is 24 columns wide. */
27
+ export const GRID_COLUMNS = 24;
28
+ /** Where each file goes, relative to the build output directory. */
29
+ export const DATASOURCES_FILE = "provisioning/datasources/chant.yaml";
30
+ export const DASHBOARD_PROVIDERS_FILE = "provisioning/dashboards/chant.yaml";
31
+ export const DASHBOARDS_DIR = "dashboards";
32
+ // ── References ──────────────────────────────────────────────────
33
+ function isRef(value) {
34
+ return (typeof value === "object" &&
35
+ value !== null &&
36
+ typeof value.type === "string" &&
37
+ typeof value.uid === "string" &&
38
+ !("lexicon" in value));
39
+ }
40
+ /** The `{ type, uid }` Grafana stores for a datasource (declared or external), a datasource variable, or a ref. */
41
+ export function datasourceRef(input) {
42
+ if (input === undefined || input === null)
43
+ return undefined;
44
+ if (isDatasourceDeclaration(input))
45
+ return { type: input.datasourceType, uid: input.uid };
46
+ if (isDatasourceVariable(input))
47
+ return { type: input.pluginType, uid: `\${${input.variableName}}` };
48
+ if (isRef(input))
49
+ return { type: input.type, uid: input.uid };
50
+ throw new Error("grafana: a datasource must be a Datasource, an ExternalDatasource, a DatasourceVariable or a { type, uid } ref");
51
+ }
52
+ function sameRef(a, b) {
53
+ return a?.type === b?.type && a?.uid === b?.uid;
54
+ }
55
+ const MIXED = { type: "datasource", uid: "-- Mixed --" };
56
+ // ── Links ───────────────────────────────────────────────────────
57
+ function dashboardLink(link) {
58
+ return compact({
59
+ title: link.title,
60
+ type: link.type ?? "link",
61
+ url: link.url,
62
+ icon: link.icon ?? "external link",
63
+ tooltip: link.tooltip ?? "",
64
+ tags: link.tags ?? [],
65
+ asDropdown: link.asDropdown ?? false,
66
+ placement: link.placement,
67
+ targetBlank: link.targetBlank ?? false,
68
+ includeVars: link.includeVars ?? false,
69
+ keepTime: link.keepTime ?? false,
70
+ });
71
+ }
72
+ // ── Variables ───────────────────────────────────────────────────
73
+ const HIDE = { label: 0, valueOnly: 1, hidden: 2, controlsMenu: 3 };
74
+ const REFRESH = { never: 0, onLoad: 1, onTimeRangeChange: 2 };
75
+ /** A custom variable value in its `query`: a comma is escaped as `\\,`, and one already escaped is left alone. */
76
+ function escapeCustom(v) {
77
+ return v.replace(/\\?,/g, "\\,");
78
+ }
79
+ /**
80
+ * The options Grafana makes of a custom variable's `query`: split on commas
81
+ * not escaped as `\\,`, and `text : value` (spaces around the colon) read as
82
+ * an option showing `text` that sets `value`. The same parse as Grafana's
83
+ * custom variable (public/app/features/variables/custom/reducer.ts, and
84
+ * CustomVariable in @grafana/scenes).
85
+ */
86
+ export function customVariableOptions(query) {
87
+ return (query.match(/(?:\\,|[^,])+/g) ?? []).map((item) => {
88
+ const text = item.replace(/\\,/g, ",");
89
+ const kv = /^\s*(.+)\s:\s(.+)$/.exec(text);
90
+ return kv ? { text: kv[1].trim(), value: kv[2].trim() } : { text: text.trim(), value: text.trim() };
91
+ });
92
+ }
93
+ function optionsOf(values, current) {
94
+ const selected = current ? (Array.isArray(current.value) ? current.value : [current.value]) : values.slice(0, 1);
95
+ return values.map((v) => ({ selected: selected.includes(v), text: v, value: v }));
96
+ }
97
+ /** One variable as Grafana's `templating.list` entry. */
98
+ export function variableModel(variable) {
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ const p = variable.props;
101
+ const common = {
102
+ type: variable.variableKind,
103
+ name: p.name,
104
+ label: p.label,
105
+ description: p.description,
106
+ hide: p.hide === undefined ? undefined : HIDE[p.hide],
107
+ skipUrlSync: p.skipUrlSync,
108
+ };
109
+ const multi = { multi: p.multi, includeAll: p.includeAll, allValue: p.allValue, allowCustomValue: p.allowCustomValue };
110
+ switch (variable.variableKind) {
111
+ case "query":
112
+ return compact({
113
+ ...common,
114
+ datasource: datasourceRef(p.datasource),
115
+ query: p.query,
116
+ definition: p.definition ?? queryText(p.query),
117
+ regex: p.regex,
118
+ refresh: REFRESH[(p.refresh ?? "onLoad")],
119
+ sort: p.sort,
120
+ ...multi,
121
+ current: p.current,
122
+ options: [],
123
+ });
124
+ case "custom": {
125
+ const values = p.values;
126
+ const query = values.map(escapeCustom).join(",");
127
+ const options = customVariableOptions(query);
128
+ const current = p.current ?? (options[0] !== undefined ? { text: options[0].text, value: options[0].value } : undefined);
129
+ const selected = current ? (Array.isArray(current.value) ? current.value : [current.value]) : [];
130
+ return compact({
131
+ ...common,
132
+ query,
133
+ ...multi,
134
+ current,
135
+ options: options.map((o) => ({ selected: selected.includes(o.value), ...o })),
136
+ });
137
+ }
138
+ case "interval": {
139
+ const values = p.values;
140
+ const current = p.current ?? (values[0] !== undefined ? { text: values[0], value: values[0] } : undefined);
141
+ return compact({
142
+ ...common,
143
+ query: values.join(","),
144
+ current,
145
+ options: optionsOf(values, current),
146
+ auto: p.auto ?? false,
147
+ auto_count: p.autoCount ?? 30,
148
+ auto_min: p.autoMin ?? "10s",
149
+ refresh: 2,
150
+ });
151
+ }
152
+ case "datasource":
153
+ return compact({
154
+ ...common,
155
+ query: p.pluginType,
156
+ regex: p.regex ?? "",
157
+ ...multi,
158
+ current: p.current,
159
+ refresh: 1,
160
+ options: [],
161
+ });
162
+ case "constant":
163
+ return compact({
164
+ ...common,
165
+ hide: 2,
166
+ query: p.value,
167
+ current: { text: p.value, value: p.value },
168
+ });
169
+ case "textbox":
170
+ return compact({
171
+ ...common,
172
+ query: p.value ?? "",
173
+ current: { text: p.value ?? "", value: p.value ?? "" },
174
+ });
175
+ case "adhoc":
176
+ return compact({
177
+ ...common,
178
+ datasource: datasourceRef(p.datasource),
179
+ filters: p.filters ?? [],
180
+ baseFilters: p.baseFilters ?? [],
181
+ defaultKeys: p.defaultKeys,
182
+ allowCustomValue: p.allowCustomValue,
183
+ enableGroupBy: p.enableGroupBy,
184
+ });
185
+ case "groupby": {
186
+ const options = p.options?.map((o) => (typeof o === "string" ? { text: o, value: o } : o));
187
+ const defaultValue = Array.isArray(p.defaultValue) ? { text: p.defaultValue, value: p.defaultValue } : p.defaultValue;
188
+ return compact({
189
+ ...common,
190
+ datasource: datasourceRef(p.datasource),
191
+ options,
192
+ current: p.current ?? defaultValue,
193
+ defaultValue,
194
+ allowCustomValue: p.allowCustomValue,
195
+ });
196
+ }
197
+ case "switch": {
198
+ const on = p.enabledValue ?? "true";
199
+ const off = p.disabledValue ?? "false";
200
+ const value = p.enabled ? on : off;
201
+ return compact({
202
+ ...common,
203
+ current: { text: value, value },
204
+ options: [
205
+ { text: on, value: on },
206
+ { text: off, value: off },
207
+ ],
208
+ });
209
+ }
210
+ }
211
+ }
212
+ /** The text of a variable query: the string, or an object query's `query`. */
213
+ function queryText(query) {
214
+ if (typeof query === "string")
215
+ return query;
216
+ if (query && typeof query === "object" && typeof query.query === "string")
217
+ return query.query;
218
+ return undefined;
219
+ }
220
+ // ── Panels and layout ───────────────────────────────────────────
221
+ function repeatName(repeat) {
222
+ if (repeat === undefined)
223
+ return undefined;
224
+ return typeof repeat === "string" ? repeat : repeat.variableName;
225
+ }
226
+ const REF_IDS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
227
+ function refIdAt(i) {
228
+ return i < REF_IDS.length ? REF_IDS[i] : `${REF_IDS[Math.floor(i / REF_IDS.length) - 1]}${REF_IDS[i % REF_IDS.length]}`;
229
+ }
230
+ /** One query as a panel target. */
231
+ export function targetJson(query, index, panelDatasource) {
232
+ const def = query.queryDefinition;
233
+ const { datasource, ...model } = query.props;
234
+ const ref = datasourceRef(datasource) ?? panelDatasource;
235
+ return compact({
236
+ datasource: ref,
237
+ refId: model.refId ?? refIdAt(index),
238
+ ...(def.defaults ?? {}),
239
+ ...model,
240
+ });
241
+ }
242
+ /** A bitmask of grid columns [x, x + w), clipped to the grid. */
243
+ function columns(x, w) {
244
+ const from = Math.max(0, x);
245
+ const to = Math.min(GRID_COLUMNS, x + w);
246
+ return to > from ? ((1 << (to - from)) - 1) << from : 0;
247
+ }
248
+ /**
249
+ * Places panels on Grafana's grid. It keeps an occupancy grid, so a panel
250
+ * placed automatically never lands on one placed explicitly (reserved up
251
+ * front) or on one placed before it.
252
+ */
253
+ class Layout {
254
+ constructor(top = 0) {
255
+ this.x = 0;
256
+ this.lineH = 0;
257
+ /** Taken cells: one column bitmask per grid line. */
258
+ this.taken = [];
259
+ this.y = top;
260
+ this.bottom = top;
261
+ }
262
+ /** Marks the cells of a panel with both `x` and `y` as taken, before anything is placed. */
263
+ reserve(gp, size) {
264
+ if (gp?.x === undefined || gp?.y === undefined)
265
+ return;
266
+ this.take(gp.x, gp.y, gp.w ?? size.w, gp.h ?? size.h);
267
+ }
268
+ place(gp, size) {
269
+ const w = gp?.w ?? size.w;
270
+ const h = gp?.h ?? size.h;
271
+ if (gp?.x !== undefined && gp?.y !== undefined) {
272
+ this.take(gp.x, gp.y, w, h);
273
+ this.bottom = Math.max(this.bottom, gp.y + h);
274
+ return { h, w, x: gp.x, y: gp.y };
275
+ }
276
+ if (gp?.y !== undefined) {
277
+ // Only `y`: the first free column on that line, else on the first line below with room.
278
+ for (let y = Math.max(0, gp.y);; y++) {
279
+ const x = this.freeColumn(0, y, w, h);
280
+ if (x === undefined)
281
+ continue;
282
+ this.take(x, y, w, h);
283
+ this.bottom = Math.max(this.bottom, y + h);
284
+ return { h, w, x, y };
285
+ }
286
+ }
287
+ // Flow: the first free spot on the current line after the previous panel, else the next line.
288
+ // Only `x`: that column, on the first line where it is free.
289
+ for (;;) {
290
+ const x = gp?.x === undefined
291
+ ? this.freeColumn(this.x, this.y, w, h)
292
+ : gp.x >= this.x && this.free(gp.x, this.y, w, h)
293
+ ? gp.x
294
+ : undefined;
295
+ if (x !== undefined) {
296
+ const pos = { h, w, x, y: this.y };
297
+ this.take(x, this.y, w, h);
298
+ this.x = x + w;
299
+ this.lineH = Math.max(this.lineH, h);
300
+ this.bottom = Math.max(this.bottom, this.y + h);
301
+ return pos;
302
+ }
303
+ this.y += this.lineH || 1;
304
+ this.x = 0;
305
+ this.lineH = 0;
306
+ }
307
+ }
308
+ /** Marks the line of a row header with an explicit `y` as taken, before anything is placed. */
309
+ reserveRow(y) {
310
+ if (y !== undefined)
311
+ this.take(0, y, GRID_COLUMNS, 1);
312
+ }
313
+ /** A full-width row header on line `explicitY`, else on the first free line below everything placed so far. */
314
+ row(explicitY) {
315
+ let y = explicitY ?? Math.max(this.bottom, this.y + this.lineH);
316
+ if (explicitY === undefined)
317
+ while (!this.free(0, y, GRID_COLUMNS, 1))
318
+ y++;
319
+ this.take(0, y, GRID_COLUMNS, 1);
320
+ this.x = 0;
321
+ this.y = y + 1;
322
+ this.lineH = 0;
323
+ this.bottom = y + 1;
324
+ return { h: 1, w: GRID_COLUMNS, x: 0, y };
325
+ }
326
+ /** The first column at or after `from` where a w×h panel fits on line `y`. A panel wider than the grid fits only at 0. */
327
+ freeColumn(from, y, w, h) {
328
+ for (let x = from; x === 0 || x + w <= GRID_COLUMNS; x++)
329
+ if (this.free(x, y, w, h))
330
+ return x;
331
+ return undefined;
332
+ }
333
+ free(x, y, w, h) {
334
+ const mask = columns(x, w);
335
+ for (let line = Math.max(0, y); line < y + h; line++)
336
+ if ((this.taken[line] ?? 0) & mask)
337
+ return false;
338
+ return true;
339
+ }
340
+ take(x, y, w, h) {
341
+ const mask = columns(x, w);
342
+ for (let line = Math.max(0, y); line < y + h; line++)
343
+ this.taken[line] = (this.taken[line] ?? 0) | mask;
344
+ }
345
+ }
346
+ class Ids {
347
+ constructor(taken) {
348
+ this.taken = taken;
349
+ this.next = 1;
350
+ }
351
+ take(explicit) {
352
+ if (explicit !== undefined)
353
+ return explicit;
354
+ while (this.taken.has(this.next))
355
+ this.next++;
356
+ return this.next++;
357
+ }
358
+ }
359
+ /** The `id` an item sets, if any. A `LibraryPanel` listed as it is has none: its panel's `id` is not the reference's. */
360
+ function ownId(item) {
361
+ if (isLibraryPanelEntity(item))
362
+ return undefined;
363
+ return typeof item.props.id === "number" ? item.props.id : undefined;
364
+ }
365
+ /** The `gridPos` an item sets, if any. */
366
+ function ownGridPos(item) {
367
+ return isLibraryPanelEntity(item) ? undefined : item.props.gridPos;
368
+ }
369
+ /** The size an item takes when its `gridPos` leaves `w` or `h` out: its panel class's, for a library panel the panel it holds. */
370
+ function defaultSize(item) {
371
+ if (isPanelEntity(item))
372
+ return item.panelDefinition.defaultSize;
373
+ const lp = referencedLibraryPanel(item).entity;
374
+ return lp && isPanelEntity(lp.props.panel) ? lp.props.panel.panelDefinition.defaultSize : LIBRARY_PANEL_SIZE;
375
+ }
376
+ /** The size of a reference to a library panel the project does not declare, when its `gridPos` leaves it out. */
377
+ const LIBRARY_PANEL_SIZE = { w: 12, h: 8 };
378
+ function isPlaceable(item) {
379
+ return isPanelEntity(item) || isLibraryPanelEntity(item) || isLibraryPanelRefEntity(item);
380
+ }
381
+ function explicitIds(items) {
382
+ const out = new Set();
383
+ for (const item of items) {
384
+ const id = ownId(item);
385
+ if (id !== undefined)
386
+ out.add(id);
387
+ if (isRowEntity(item))
388
+ for (const p of item.props.panels ?? [])
389
+ if (ownId(p) !== undefined)
390
+ out.add(ownId(p));
391
+ }
392
+ return out;
393
+ }
394
+ /** A library panel reference as the dashboard holds it: Grafana draws the panel from the library. */
395
+ function libraryRefJson(item, gridPos, id) {
396
+ const { uid, name } = referencedLibraryPanel(item);
397
+ return compact({
398
+ id,
399
+ gridPos,
400
+ libraryPanel: { uid, name },
401
+ title: isLibraryPanelRefEntity(item) ? item.props.title : undefined,
402
+ });
403
+ }
404
+ /** One placed item: a panel, or a reference to a library panel. */
405
+ function itemJson(item, gridPos, id) {
406
+ return isPanelEntity(item) ? panelJson(item, gridPos, id) : libraryRefJson(item, gridPos, id);
407
+ }
408
+ function panelJson(panel, gridPos, id) {
409
+ const p = panel.props;
410
+ const def = panel.panelDefinition;
411
+ const own = datasourceRef(p.datasource);
412
+ const queries = (p.targets ?? []).filter(isQueryEntity);
413
+ // A query under a Mixed panel names its own datasource; one that names none goes to the default, not to Mixed.
414
+ const targets = queries.map((q, i) => targetJson(q, i, sameRef(own, MIXED) ? undefined : own));
415
+ const refs = targets.map((t) => t.datasource);
416
+ let datasource = own;
417
+ if (!datasource && refs.length > 0) {
418
+ datasource = refs.every((r) => sameRef(r, refs[0])) ? refs[0] : MIXED;
419
+ }
420
+ else if (datasource && refs.some((r) => r && !sameRef(r, datasource))) {
421
+ datasource = MIXED;
422
+ }
423
+ const fieldConfig = {
424
+ defaults: { ...(p.fieldConfig?.defaults ?? {}) },
425
+ overrides: p.fieldConfig?.overrides ?? [],
426
+ };
427
+ return compact({
428
+ type: def.type,
429
+ id,
430
+ title: p.title ?? "",
431
+ description: p.description,
432
+ gridPos,
433
+ datasource,
434
+ targets: targets.length > 0 ? targets : undefined,
435
+ options: (p.options ?? {}),
436
+ fieldConfig,
437
+ transformations: p.transformations,
438
+ links: p.links?.map(dashboardLink),
439
+ repeat: repeatName(p.repeat),
440
+ repeatDirection: p.repeatDirection,
441
+ maxPerRow: p.maxPerRow,
442
+ maxDataPoints: p.maxDataPoints,
443
+ interval: p.interval,
444
+ timeFrom: p.timeFrom,
445
+ timeShift: p.timeShift,
446
+ hideTimeOverride: p.hideTimeOverride,
447
+ transparent: p.transparent,
448
+ pluginVersion: p.pluginVersion,
449
+ });
450
+ }
451
+ /** A dashboard's `panels` array: rows, panels and library panel references, laid out, with ids. */
452
+ export function panelsJson(items) {
453
+ const layout = new Layout();
454
+ // Panels on the dashboard's grid with both x and y, and rows with a y, take their cells first; a collapsed row's panels are laid out on their own.
455
+ for (const item of items) {
456
+ if (isRowEntity(item))
457
+ layout.reserveRow(item.props.gridPos?.y);
458
+ const onGrid = isRowEntity(item) ? (item.props.collapsed ? [] : (item.props.panels ?? [])) : [item];
459
+ for (const p of onGrid.filter(isPlaceable))
460
+ layout.reserve(ownGridPos(p), defaultSize(p));
461
+ }
462
+ const ids = new Ids(explicitIds(items));
463
+ const out = [];
464
+ for (const item of items) {
465
+ if (isRowEntity(item)) {
466
+ const r = item.props;
467
+ const pos = layout.row(r.gridPos?.y);
468
+ const rowId = ids.take(r.id);
469
+ const rowRef = datasourceRef(r.datasource);
470
+ const collapsed = r.collapsed ?? false;
471
+ // A collapsed row's panels sit under its header on a grid of their own; the next item starts right under the header.
472
+ const rowLayout = collapsed ? new Layout(pos.y + 1) : layout;
473
+ const panels = (r.panels ?? []).filter(isPlaceable);
474
+ if (collapsed)
475
+ for (const p of panels)
476
+ rowLayout.reserve(ownGridPos(p), defaultSize(p));
477
+ const children = panels.map((p) => {
478
+ const gp = rowLayout.place(ownGridPos(p), defaultSize(p));
479
+ return itemJson(p, gp, ids.take(ownId(p)));
480
+ });
481
+ out.push(compact({
482
+ type: "row",
483
+ collapsed,
484
+ title: r.title,
485
+ datasource: rowRef,
486
+ gridPos: pos,
487
+ id: rowId,
488
+ panels: collapsed ? children : [],
489
+ repeat: repeatName(r.repeat),
490
+ }));
491
+ if (!collapsed)
492
+ out.push(...children);
493
+ }
494
+ else if (isPlaceable(item)) {
495
+ const gp = layout.place(ownGridPos(item), defaultSize(item));
496
+ out.push(itemJson(item, gp, ids.take(ownId(item))));
497
+ }
498
+ }
499
+ return out;
500
+ }
501
+ // ── Dashboards ──────────────────────────────────────────────────
502
+ const GRAPH_TOOLTIP = { default: 0, sharedCrosshair: 1, sharedTooltip: 2 };
503
+ /** The dashboard's uid: its own, else from its export name, else from its title. */
504
+ export function dashboardUid(dashboard, exportName) {
505
+ return dashboard.props.uid ?? slugUid(exportName ?? dashboard.props.title);
506
+ }
507
+ /** Every `LibraryPanel` a dashboard places, top level and in rows, each once, in order. */
508
+ export function dashboardLibraryPanels(dashboard) {
509
+ const out = new Map();
510
+ const visit = (item) => {
511
+ if (!isLibraryPanelEntity(item) && !isLibraryPanelRefEntity(item))
512
+ return;
513
+ const lp = referencedLibraryPanel(item).entity;
514
+ if (!lp)
515
+ return;
516
+ const prior = out.get(lp.uid);
517
+ if (prior && prior !== lp)
518
+ throw new Error(`grafana: dashboard "${dashboard.props.title}" places two different LibraryPanels with the uid "${lp.uid}"`);
519
+ out.set(lp.uid, lp);
520
+ };
521
+ for (const item of dashboard.props.panels ?? []) {
522
+ if (isRowEntity(item))
523
+ for (const p of item.props.panels ?? [])
524
+ visit(p);
525
+ else
526
+ visit(item);
527
+ }
528
+ return [...out.values()];
529
+ }
530
+ /** A library panel's own panel JSON: a panel's, without the `gridPos` and `id` each reference has. */
531
+ export function libraryPanelModel(lp) {
532
+ if (!isPanelEntity(lp.props.panel))
533
+ throw new Error(`grafana: LibraryPanel "${lp.props.name}" has no panel`);
534
+ const { gridPos: _gridPos, id: _id, ...model } = panelJson(lp.props.panel, { h: 0, w: 0, x: 0, y: 0 }, 0);
535
+ return model;
536
+ }
537
+ /**
538
+ * The `__elements` entry of a library panel, as Grafana's "Export for
539
+ * sharing externally" writes it, plus the uid of its folder when it names
540
+ * one (`folderUid` maps a folder path to its uid; the build passes the one
541
+ * that knows the declared `Folder`s).
542
+ */
543
+ export function libraryElementJson(lp, folderUid = folderUidFor) {
544
+ const inFolder = lp.folderEntity?.uid ?? (lp.props.folder && folderLevels(lp.props.folder).length > 0 ? folderUid(lp.props.folder) : undefined);
545
+ return compact({ name: lp.props.name, uid: lp.uid, kind: LIBRARY_PANEL_KIND, model: libraryPanelModel(lp), folderUid: inFolder });
546
+ }
547
+ /**
548
+ * One dashboard as the JSON Grafana imports. The library panels it places
549
+ * are written into `__elements`, keyed by uid.
550
+ */
551
+ export function renderDashboard(dashboard, exportName, folderUid) {
552
+ const p = dashboard.props;
553
+ const libraryPanels = dashboardLibraryPanels(dashboard);
554
+ return compact({
555
+ __elements: libraryPanels.length > 0 ? Object.fromEntries(libraryPanels.map((lp) => [lp.uid, libraryElementJson(lp, folderUid)])) : undefined,
556
+ annotations: { list: (p.annotations ?? []).map((a) => annotationJson(a, datasourceRef)) },
557
+ description: p.description,
558
+ editable: p.editable ?? true,
559
+ fiscalYearStartMonth: p.fiscalYearStartMonth ?? 0,
560
+ graphTooltip: GRAPH_TOOLTIP[p.graphTooltip ?? "default"],
561
+ links: (p.links ?? []).map(dashboardLink),
562
+ liveNow: p.liveNow,
563
+ panels: panelsJson(p.panels ?? []),
564
+ refresh: p.refresh,
565
+ schemaVersion: p.schemaVersion ?? DASHBOARD_SCHEMA_VERSION,
566
+ tags: p.tags ?? [],
567
+ templating: { list: (p.variables ?? []).filter(isVariableEntity).map(variableModel) },
568
+ time: p.time ?? { from: "now-6h", to: "now" },
569
+ timepicker: p.timepicker ?? {},
570
+ timezone: p.timezone ?? "browser",
571
+ title: p.title,
572
+ uid: dashboardUid(dashboard, exportName),
573
+ weekStart: p.weekStart ?? "",
574
+ });
575
+ }
576
+ /** One dashboard as formatted JSON text. */
577
+ export function dashboardJson(dashboard, exportName) {
578
+ return `${JSON.stringify(renderDashboard(dashboard, exportName), null, 2)}\n`;
579
+ }
580
+ // ── Datasources and providers ───────────────────────────────────
581
+ /** Replace every declared or external datasource inside plugin settings with its uid. */
582
+ function resolveNested(value) {
583
+ if (isDatasourceDeclaration(value))
584
+ return value.uid;
585
+ if (Array.isArray(value))
586
+ return value.map(resolveNested);
587
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
588
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveNested(v)]));
589
+ }
590
+ return value;
591
+ }
592
+ /** One datasource as a provisioning entry. */
593
+ export function provisionedDatasource(ds) {
594
+ const p = ds.props;
595
+ return compact({
596
+ name: p.name,
597
+ type: p.type,
598
+ uid: ds.uid,
599
+ access: p.access ?? "proxy",
600
+ url: p.url,
601
+ isDefault: p.isDefault,
602
+ basicAuth: p.basicAuth,
603
+ basicAuthUser: p.basicAuthUser,
604
+ user: p.user,
605
+ database: p.database,
606
+ withCredentials: p.withCredentials,
607
+ jsonData: p.jsonData === undefined ? undefined : resolveNested(p.jsonData),
608
+ secureJsonData: p.secureJsonData,
609
+ editable: p.editable ?? false,
610
+ orgId: p.orgId,
611
+ version: p.version,
612
+ });
613
+ }
614
+ /** One `ExternalDatasource` as the checks and the index see it. */
615
+ export function externalDatasourceRecord(ds) {
616
+ return compact({ type: ds.datasourceType, uid: ds.uid, name: ds.props.name });
617
+ }
618
+ /** One dashboard provider as a provisioning entry. */
619
+ export function provisionedProvider(p) {
620
+ const pinnedFolder = p.folder !== undefined || p.folderUid !== undefined;
621
+ return compact({
622
+ name: p.name,
623
+ orgId: p.orgId ?? 1,
624
+ folder: p.folder ?? "",
625
+ folderUid: p.folderUid,
626
+ type: "file",
627
+ disableDeletion: p.disableDeletion ?? false,
628
+ allowUiUpdates: p.allowUiUpdates ?? false,
629
+ updateIntervalSeconds: p.updateIntervalSeconds ?? 30,
630
+ options: {
631
+ path: p.path ?? DEFAULT_DASHBOARDS_PATH,
632
+ foldersFromFilesStructure: p.foldersFromFilesStructure ?? !pinnedFolder,
633
+ },
634
+ });
635
+ }
636
+ function yamlFile(header, body) {
637
+ return `# ${header}\n${dump(body, { lineWidth: -1, noRefs: true, quotingType: '"' })}`;
638
+ }
639
+ /** `prune` and `deleteDatasources` as a build writes them: pruning on unless a `DatasourceProvisioning` turns it off. */
640
+ export function datasourceFileSettings(props) {
641
+ return {
642
+ prune: props?.prune ?? true,
643
+ deleteDatasources: (props?.deleteDatasources ?? []).map((d) => ({ name: d.name, ...(d.orgId !== undefined ? { orgId: d.orgId } : {}) })),
644
+ };
645
+ }
646
+ /**
647
+ * The datasource provisioning file for these datasources. It prunes by
648
+ * default (`prune: true`), so a datasource removed from the build is removed
649
+ * from Grafana; `deleteDatasources` comes first, as Grafana runs it first.
650
+ */
651
+ export function datasourcesYaml(datasources, settings = datasourceFileSettings(undefined)) {
652
+ return yamlFile("Grafana datasource provisioning, generated by chant.", {
653
+ apiVersion: 1,
654
+ ...(settings.prune ? { prune: true } : {}),
655
+ ...(settings.deleteDatasources.length > 0 ? { deleteDatasources: settings.deleteDatasources } : {}),
656
+ datasources,
657
+ });
658
+ }
659
+ /** The dashboard provisioning file for these providers. */
660
+ export function dashboardProvidersYaml(providers) {
661
+ return yamlFile("Grafana dashboard provisioning, generated by chant.", { apiVersion: 1, providers });
662
+ }
663
+ /**
664
+ * A dashboard's `folder` as its directory under `dashboards/`: one
665
+ * subdirectory per level (`folderLevels`), which a provider with
666
+ * `foldersFromFilesStructure` turns into nested folders on Grafana 13.1 and
667
+ * later (GRAF109 says what earlier versions do). Empty for the General
668
+ * folder.
669
+ */
670
+ function folderDir(folder) {
671
+ return folderLevels(folder).join("/");
672
+ }
673
+ /** A `Folder` and every folder above it. */
674
+ function withAncestors(folder, into) {
675
+ for (let f = folder; f; f = f.props.parent)
676
+ into.add(f);
677
+ }
678
+ // ── The whole build ─────────────────────────────────────────────
679
+ /** Everything the grafana entities in a build render to. */
680
+ export function buildGrafana(entities) {
681
+ const named = entities instanceof Map ? [...entities.entries()] : [...entities].map((e) => [undefined, e]);
682
+ const files = {};
683
+ // Sorted by name, so the provisioning file doesn't depend on which source file was read first.
684
+ const datasources = named
685
+ .filter(([, e]) => isDatasourceEntity(e))
686
+ .map(([, e]) => provisionedDatasource(e))
687
+ .sort((a, b) => a.name.localeCompare(b.name));
688
+ const externalDatasources = named
689
+ .filter(([, e]) => isExternalDatasource(e))
690
+ .map(([, e]) => externalDatasourceRecord(e))
691
+ .sort((a, b) => a.uid.localeCompare(b.uid));
692
+ // Library panels: every one declared, and every one a dashboard places.
693
+ const libraryPanels = new Set();
694
+ for (const [, e] of named) {
695
+ if (isLibraryPanelEntity(e))
696
+ libraryPanels.add(e);
697
+ else if (isDashboardEntity(e))
698
+ for (const lp of dashboardLibraryPanels(e))
699
+ libraryPanels.add(lp);
700
+ }
701
+ // Folders: every declared `Folder` and every one a dashboard or library panel holds, with their parents, pin uids; every path either names is a folder per level.
702
+ const declaredFolders = new Set();
703
+ for (const [, e] of named) {
704
+ if (isFolderEntity(e))
705
+ withAncestors(e, declaredFolders);
706
+ else if (isDashboardEntity(e) && e.folderEntity)
707
+ withAncestors(e.folderEntity, declaredFolders);
708
+ }
709
+ for (const lp of libraryPanels)
710
+ if (lp.folderEntity)
711
+ withAncestors(lp.folderEntity, declaredFolders);
712
+ const resolved = resolveFolders({
713
+ declared: [...declaredFolders].map((f) => ({ uid: f.uid, path: f.path })),
714
+ paths: [
715
+ ...named.flatMap(([, e]) => (isDashboardEntity(e) && e.props.folder ? [e.props.folder] : [])),
716
+ ...[...libraryPanels].flatMap((lp) => (lp.props.folder ? [lp.props.folder] : [])),
717
+ ],
718
+ }, { duplicateUids: "keep" });
719
+ const folderUidOfPath = (path) => resolved.uidOf(folderDir(path)) ?? folderUidFor(path);
720
+ const dashboards = [];
721
+ for (const [name, e] of named) {
722
+ if (!isDashboardEntity(e))
723
+ continue;
724
+ const json = renderDashboard(e, name, folderUidOfPath);
725
+ const uid = json.uid;
726
+ const folder = e.props.folder;
727
+ const sub = folder ? folderDir(folder) : "";
728
+ const dir = sub ? `${DASHBOARDS_DIR}/${sub}` : DASHBOARDS_DIR;
729
+ let file = `${dir}/${uid}.json`;
730
+ for (let n = 2; file in files; n++)
731
+ file = `${dir}/${uid}-${n}.json`;
732
+ files[file] = `${JSON.stringify(json, null, 2)}\n`;
733
+ const folderUid = sub ? resolved.uidOf(sub) : undefined;
734
+ dashboards.push({ uid, title: e.props.title, ...(folder ? { folder } : {}), ...(folderUid ? { folderUid } : {}), file, json });
735
+ }
736
+ const settings = named.map(([, e]) => e).filter(isDatasourceProvisioningEntity);
737
+ if (settings.length > 1)
738
+ throw new Error("grafana: declare at most one DatasourceProvisioning; the build writes one datasource provisioning file");
739
+ const datasourceFile = datasourceFileSettings(settings[0]?.props);
740
+ const declaredProviders = named.filter(([, e]) => isDashboardProviderEntity(e)).map(([, e]) => e.props);
741
+ const providers = declaredProviders.length > 0
742
+ ? declaredProviders.map(provisionedProvider)
743
+ : dashboards.length > 0
744
+ ? [provisionedProvider({ name: DEFAULT_PROVIDER_NAME })]
745
+ : [];
746
+ if (datasources.length > 0 || datasourceFile.deleteDatasources.length > 0)
747
+ files[DATASOURCES_FILE] = datasourcesYaml(datasources, datasourceFile);
748
+ if (providers.length > 0)
749
+ files[DASHBOARD_PROVIDERS_FILE] = dashboardProvidersYaml(providers);
750
+ const alerting = buildAlerting(named.map(([, e]) => e));
751
+ if (alerting)
752
+ files[ALERTING_FILE] = alertingYaml(alerting.file);
753
+ const index = {
754
+ grafanaSchema: `${GRAFANA_SCHEMA_PIN.source}@${GRAFANA_SCHEMA_PIN.ref}`,
755
+ dashboards: dashboards.map(({ uid, title, folder, folderUid, file }) => ({ uid, title, ...(folder ? { folder } : {}), ...(folderUid ? { folderUid } : {}), file })),
756
+ ...(resolved.folders.length > 0 ? { folders: resolved.folders } : {}),
757
+ datasources: datasources.map(({ name, type, uid }) => ({ name, type, uid })),
758
+ ...(externalDatasources.length > 0 ? { externalDatasources } : {}),
759
+ ...(alerting ? { alerting: alerting.index } : {}),
760
+ files: Object.keys(files).sort(),
761
+ };
762
+ return { dashboards, datasources, externalDatasources, providers, folders: resolved.folders, datasourceFile, ...(alerting ? { alerting: alerting.file } : {}), files, index };
763
+ }
764
+ /** Every file the grafana entities render to, by path relative to the output directory. */
765
+ export function grafanaFiles(entities) {
766
+ return buildGrafana(entities).files;
767
+ }