@intentius/chant-lexicon-grafana 0.96.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 (259) hide show
  1. package/README.md +50 -0
  2. package/dist/build.d.ts +116 -0
  3. package/dist/build.d.ts.map +1 -0
  4. package/dist/catalog.d.ts +26 -0
  5. package/dist/catalog.d.ts.map +1 -0
  6. package/dist/codegen/docs-cli.d.ts +3 -0
  7. package/dist/codegen/docs-cli.d.ts.map +1 -0
  8. package/dist/codegen/docs.d.ts +8 -0
  9. package/dist/codegen/docs.d.ts.map +1 -0
  10. package/dist/codegen/generate-cli.d.ts +3 -0
  11. package/dist/codegen/generate-cli.d.ts.map +1 -0
  12. package/dist/codegen/generate.d.ts +21 -0
  13. package/dist/codegen/generate.d.ts.map +1 -0
  14. package/dist/codegen/package.d.ts +7 -0
  15. package/dist/codegen/package.d.ts.map +1 -0
  16. package/dist/codegen/schema-types.d.ts +41 -0
  17. package/dist/codegen/schema-types.d.ts.map +1 -0
  18. package/dist/composites/agent-dashboard.d.ts +70 -0
  19. package/dist/composites/agent-dashboard.d.ts.map +1 -0
  20. package/dist/composites/catalog.d.ts +3 -0
  21. package/dist/composites/catalog.d.ts.map +1 -0
  22. package/dist/composites/index.d.ts +12 -0
  23. package/dist/composites/index.d.ts.map +1 -0
  24. package/dist/composites/red-dashboard.d.ts +58 -0
  25. package/dist/composites/red-dashboard.d.ts.map +1 -0
  26. package/dist/composites/shared.d.ts +68 -0
  27. package/dist/composites/shared.d.ts.map +1 -0
  28. package/dist/composites/slo-dashboard.d.ts +62 -0
  29. package/dist/composites/slo-dashboard.d.ts.map +1 -0
  30. package/dist/dashboard.d.ts +84 -0
  31. package/dist/dashboard.d.ts.map +1 -0
  32. package/dist/datasource.d.ts +67 -0
  33. package/dist/datasource.d.ts.map +1 -0
  34. package/dist/detect.d.ts +20 -0
  35. package/dist/detect.d.ts.map +1 -0
  36. package/dist/index.d.ts +17 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/integrity.json +22 -0
  39. package/dist/lint/audit-catalog.d.ts +13 -0
  40. package/dist/lint/audit-catalog.d.ts.map +1 -0
  41. package/dist/lint/post-synth/graf101.d.ts +8 -0
  42. package/dist/lint/post-synth/graf101.d.ts.map +1 -0
  43. package/dist/lint/post-synth/graf102.d.ts +8 -0
  44. package/dist/lint/post-synth/graf102.d.ts.map +1 -0
  45. package/dist/lint/post-synth/graf103.d.ts +8 -0
  46. package/dist/lint/post-synth/graf103.d.ts.map +1 -0
  47. package/dist/lint/post-synth/graf104.d.ts +8 -0
  48. package/dist/lint/post-synth/graf104.d.ts.map +1 -0
  49. package/dist/lint/post-synth/graf105.d.ts +8 -0
  50. package/dist/lint/post-synth/graf105.d.ts.map +1 -0
  51. package/dist/lint/post-synth/graf106.d.ts +8 -0
  52. package/dist/lint/post-synth/graf106.d.ts.map +1 -0
  53. package/dist/lint/post-synth/graf107.d.ts +8 -0
  54. package/dist/lint/post-synth/graf107.d.ts.map +1 -0
  55. package/dist/lint/post-synth/grafana-helpers.d.ts +16 -0
  56. package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -0
  57. package/dist/lint/post-synth/index.d.ts +3 -0
  58. package/dist/lint/post-synth/index.d.ts.map +1 -0
  59. package/dist/lint/rules/grafana-ast.d.ts +21 -0
  60. package/dist/lint/rules/grafana-ast.d.ts.map +1 -0
  61. package/dist/lint/rules/index.d.ts +6 -0
  62. package/dist/lint/rules/index.d.ts.map +1 -0
  63. package/dist/lint/rules/literal-secret.d.ts +12 -0
  64. package/dist/lint/rules/literal-secret.d.ts.map +1 -0
  65. package/dist/lint/rules/uid-syntax.d.ts +12 -0
  66. package/dist/lint/rules/uid-syntax.d.ts.map +1 -0
  67. package/dist/lsp/completions.d.ts +4 -0
  68. package/dist/lsp/completions.d.ts.map +1 -0
  69. package/dist/lsp/hover.d.ts +4 -0
  70. package/dist/lsp/hover.d.ts.map +1 -0
  71. package/dist/manifest.json +8 -0
  72. package/dist/meta.json +107 -0
  73. package/dist/okf/index.md +39 -0
  74. package/dist/okf/rules/GRAF001.md +16 -0
  75. package/dist/okf/rules/GRAF002.md +15 -0
  76. package/dist/okf/rules/GRAF101.md +15 -0
  77. package/dist/okf/rules/GRAF102.md +15 -0
  78. package/dist/okf/rules/GRAF103.md +11 -0
  79. package/dist/okf/rules/GRAF104.md +15 -0
  80. package/dist/okf/rules/GRAF105.md +11 -0
  81. package/dist/okf/rules/GRAF106.md +11 -0
  82. package/dist/okf/rules/GRAF107.md +11 -0
  83. package/dist/okf/types/ConstantVariable.md +9 -0
  84. package/dist/okf/types/CustomVariable.md +9 -0
  85. package/dist/okf/types/Dashboard.md +14 -0
  86. package/dist/okf/types/DashboardProvider.md +9 -0
  87. package/dist/okf/types/Datasource.md +16 -0
  88. package/dist/okf/types/DatasourceVariable.md +9 -0
  89. package/dist/okf/types/GaugePanel.md +9 -0
  90. package/dist/okf/types/HeatmapPanel.md +9 -0
  91. package/dist/okf/types/IntervalVariable.md +9 -0
  92. package/dist/okf/types/LogsPanel.md +9 -0
  93. package/dist/okf/types/LokiQuery.md +9 -0
  94. package/dist/okf/types/PromQuery.md +9 -0
  95. package/dist/okf/types/QueryVariable.md +9 -0
  96. package/dist/okf/types/Row.md +9 -0
  97. package/dist/okf/types/StatPanel.md +9 -0
  98. package/dist/okf/types/TablePanel.md +9 -0
  99. package/dist/okf/types/TempoQuery.md +9 -0
  100. package/dist/okf/types/TextPanel.md +9 -0
  101. package/dist/okf/types/TextboxVariable.md +9 -0
  102. package/dist/okf/types/TimeSeriesPanel.md +9 -0
  103. package/dist/okf/types/TracesPanel.md +9 -0
  104. package/dist/package-cli.d.ts +3 -0
  105. package/dist/package-cli.d.ts.map +1 -0
  106. package/dist/panels.d.ts +138 -0
  107. package/dist/panels.d.ts.map +1 -0
  108. package/dist/pin.d.ts +32 -0
  109. package/dist/pin.d.ts.map +1 -0
  110. package/dist/plugin.d.ts +12 -0
  111. package/dist/plugin.d.ts.map +1 -0
  112. package/dist/query.d.ts +81 -0
  113. package/dist/query.d.ts.map +1 -0
  114. package/dist/rules/graf101.ts +17 -0
  115. package/dist/rules/graf102.ts +17 -0
  116. package/dist/rules/graf103.ts +17 -0
  117. package/dist/rules/graf104.ts +17 -0
  118. package/dist/rules/graf105.ts +17 -0
  119. package/dist/rules/graf106.ts +17 -0
  120. package/dist/rules/graf107.ts +17 -0
  121. package/dist/rules/grafana-ast.ts +70 -0
  122. package/dist/rules/grafana-helpers.ts +68 -0
  123. package/dist/rules/literal-secret.ts +48 -0
  124. package/dist/rules/uid-syntax.ts +56 -0
  125. package/dist/schema/dashboard.gen.d.ts +1231 -0
  126. package/dist/schema/dashboard.gen.d.ts.map +1 -0
  127. package/dist/schema/gauge.gen.d.ts +57 -0
  128. package/dist/schema/gauge.gen.d.ts.map +1 -0
  129. package/dist/schema/heatmap.gen.d.ts +295 -0
  130. package/dist/schema/heatmap.gen.d.ts.map +1 -0
  131. package/dist/schema/index.d.ts +18 -0
  132. package/dist/schema/index.d.ts.map +1 -0
  133. package/dist/schema/logs.gen.d.ts +29 -0
  134. package/dist/schema/logs.gen.d.ts.map +1 -0
  135. package/dist/schema/loki.gen.d.ts +69 -0
  136. package/dist/schema/loki.gen.d.ts.map +1 -0
  137. package/dist/schema/prometheus.gen.d.ts +169 -0
  138. package/dist/schema/prometheus.gen.d.ts.map +1 -0
  139. package/dist/schema/stat.gen.d.ts +75 -0
  140. package/dist/schema/stat.gen.d.ts.map +1 -0
  141. package/dist/schema/table.gen.d.ts +295 -0
  142. package/dist/schema/table.gen.d.ts.map +1 -0
  143. package/dist/schema/tempo.gen.d.ts +136 -0
  144. package/dist/schema/tempo.gen.d.ts.map +1 -0
  145. package/dist/schema/text.gen.d.ts +16 -0
  146. package/dist/schema/text.gen.d.ts.map +1 -0
  147. package/dist/schema/timeseries.gen.d.ts +179 -0
  148. package/dist/schema/timeseries.gen.d.ts.map +1 -0
  149. package/dist/schema-validate.d.ts +20 -0
  150. package/dist/schema-validate.d.ts.map +1 -0
  151. package/dist/serializer.d.ts +22 -0
  152. package/dist/serializer.d.ts.map +1 -0
  153. package/dist/skill-defs.d.ts +3 -0
  154. package/dist/skill-defs.d.ts.map +1 -0
  155. package/dist/skills/chant-grafana-provisioning.md +42 -0
  156. package/dist/skills/chant-grafana.md +93 -0
  157. package/dist/spec/fetch-cli.d.ts +3 -0
  158. package/dist/spec/fetch-cli.d.ts.map +1 -0
  159. package/dist/spec/schemas.d.ts +25 -0
  160. package/dist/spec/schemas.d.ts.map +1 -0
  161. package/dist/types/index.d.ts +3 -0
  162. package/dist/util.d.ts +17 -0
  163. package/dist/util.d.ts.map +1 -0
  164. package/dist/validate-cli.d.ts +3 -0
  165. package/dist/validate-cli.d.ts.map +1 -0
  166. package/dist/validate-output.d.ts +42 -0
  167. package/dist/validate-output.d.ts.map +1 -0
  168. package/dist/validate.d.ts +12 -0
  169. package/dist/validate.d.ts.map +1 -0
  170. package/dist/variables.d.ts +100 -0
  171. package/dist/variables.d.ts.map +1 -0
  172. package/package.json +79 -0
  173. package/src/build.test.ts +210 -0
  174. package/src/build.ts +573 -0
  175. package/src/catalog.ts +116 -0
  176. package/src/codegen/docs-cli.ts +4 -0
  177. package/src/codegen/docs.ts +97 -0
  178. package/src/codegen/generate-cli.ts +8 -0
  179. package/src/codegen/generate.ts +83 -0
  180. package/src/codegen/package.ts +41 -0
  181. package/src/codegen/schema-types.ts +173 -0
  182. package/src/composites/agent-dashboard.ts +235 -0
  183. package/src/composites/catalog.test.ts +37 -0
  184. package/src/composites/catalog.ts +223 -0
  185. package/src/composites/composites.test.ts +314 -0
  186. package/src/composites/index.ts +12 -0
  187. package/src/composites/red-dashboard.ts +159 -0
  188. package/src/composites/shared.ts +94 -0
  189. package/src/composites/slo-dashboard.ts +177 -0
  190. package/src/dashboard.ts +123 -0
  191. package/src/datasource.ts +93 -0
  192. package/src/detect.ts +30 -0
  193. package/src/generated/lexicon-grafana.json +107 -0
  194. package/src/import.e2e.test.ts +147 -0
  195. package/src/import.test.ts +45 -0
  196. package/src/index.ts +178 -0
  197. package/src/lint/audit-catalog.ts +87 -0
  198. package/src/lint/post-synth/graf101.ts +17 -0
  199. package/src/lint/post-synth/graf102.ts +17 -0
  200. package/src/lint/post-synth/graf103.ts +17 -0
  201. package/src/lint/post-synth/graf104.ts +17 -0
  202. package/src/lint/post-synth/graf105.ts +17 -0
  203. package/src/lint/post-synth/graf106.ts +17 -0
  204. package/src/lint/post-synth/graf107.ts +17 -0
  205. package/src/lint/post-synth/grafana-helpers.ts +68 -0
  206. package/src/lint/post-synth/index.ts +19 -0
  207. package/src/lint/post-synth/post-synth.test.ts +231 -0
  208. package/src/lint/rules/grafana-ast.ts +70 -0
  209. package/src/lint/rules/index.ts +9 -0
  210. package/src/lint/rules/literal-secret.ts +48 -0
  211. package/src/lint/rules/rules.test.ts +70 -0
  212. package/src/lint/rules/uid-syntax.ts +56 -0
  213. package/src/lsp/completions.test.ts +31 -0
  214. package/src/lsp/completions.ts +15 -0
  215. package/src/lsp/hover.test.ts +25 -0
  216. package/src/lsp/hover.ts +28 -0
  217. package/src/package-cli.ts +17 -0
  218. package/src/panels.ts +275 -0
  219. package/src/pin.ts +68 -0
  220. package/src/plugin.test.ts +50 -0
  221. package/src/plugin.ts +116 -0
  222. package/src/query.ts +160 -0
  223. package/src/schema/dashboard.gen.ts +1289 -0
  224. package/src/schema/gauge.gen.ts +64 -0
  225. package/src/schema/heatmap.gen.ts +321 -0
  226. package/src/schema/index.ts +17 -0
  227. package/src/schema/logs.gen.ts +34 -0
  228. package/src/schema/loki.gen.ts +77 -0
  229. package/src/schema/prometheus.gen.ts +181 -0
  230. package/src/schema/stat.gen.ts +86 -0
  231. package/src/schema/table.gen.ts +334 -0
  232. package/src/schema/tempo.gen.ts +146 -0
  233. package/src/schema/text.gen.ts +22 -0
  234. package/src/schema/timeseries.gen.ts +208 -0
  235. package/src/schema-validate.ts +143 -0
  236. package/src/schema.test.ts +76 -0
  237. package/src/serializer.test.ts +170 -0
  238. package/src/serializer.ts +37 -0
  239. package/src/skill-defs.ts +31 -0
  240. package/src/skills/chant-grafana-provisioning.md +42 -0
  241. package/src/skills/chant-grafana.md +93 -0
  242. package/src/spec/fetch-cli.ts +27 -0
  243. package/src/spec/schemas/dashboard.jsonschema.json +1901 -0
  244. package/src/spec/schemas/gauge.jsonschema.json +114 -0
  245. package/src/spec/schemas/heatmap.jsonschema.json +480 -0
  246. package/src/spec/schemas/logs.jsonschema.json +94 -0
  247. package/src/spec/schemas/loki.jsonschema.json +118 -0
  248. package/src/spec/schemas/prometheus.jsonschema.json +276 -0
  249. package/src/spec/schemas/stat.jsonschema.json +163 -0
  250. package/src/spec/schemas/table.jsonschema.json +751 -0
  251. package/src/spec/schemas/tempo.jsonschema.json +225 -0
  252. package/src/spec/schemas/text.jsonschema.json +72 -0
  253. package/src/spec/schemas/timeseries.jsonschema.json +471 -0
  254. package/src/spec/schemas.ts +56 -0
  255. package/src/util.ts +43 -0
  256. package/src/validate-cli.ts +7 -0
  257. package/src/validate-output.ts +380 -0
  258. package/src/validate.ts +108 -0
  259. package/src/variables.ts +161 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Documentation generation for the grafana lexicon: the generated reference
3
+ * pages from the core docs pipeline, plus the authored pages in docs/pages/.
4
+ */
5
+
6
+ import { dirname, join } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";
9
+
10
+ function serviceFromType(resourceType: string): string {
11
+ const parts = resourceType.split("::");
12
+ return parts.length >= 2 ? parts[1] : "Grafana";
13
+ }
14
+
15
+ const overview = `The grafana lexicon types what a [Grafana](https://grafana.com/docs/grafana/latest/) dashboard
16
+ shows: datasources, dashboards, rows, panels, queries and variables. \`chant build\`
17
+ writes one JSON file per dashboard, the JSON Grafana imports as it is, plus the
18
+ provisioning files Grafana reads for dashboards and datasources. Grafana server
19
+ settings, users and plugins are out of scope.
20
+
21
+ \`\`\`ts
22
+ import { Datasource, PromQuery, TimeSeriesPanel, Dashboard } from "@intentius/chant-lexicon-grafana";
23
+
24
+ const prometheus = new Datasource({ name: "Prometheus", type: "prometheus", url: "http://prometheus:9090" });
25
+ const rate = new PromQuery({ expr: "sum(rate(http_requests_total[$__rate_interval]))" });
26
+ const requests = new TimeSeriesPanel({ title: "Requests per second", datasource: prometheus, targets: [rate] });
27
+
28
+ export const overview = new Dashboard({ title: "Overview", panels: [requests] });
29
+ \`\`\`
30
+
31
+ Panels: time series, stat, gauge, table, logs, traces, heatmap and text, plus
32
+ rows. Queries: Prometheus (PromQL), Tempo (TraceQL) and Loki (LogQL). Panel
33
+ options and query fields are generated from Grafana's own schemas at a pinned
34
+ version, and every build validates the dashboards against the same schemas.
35
+ \`definePanel\` and \`defineQuery\` add plugins chant doesn't ship.
36
+
37
+ Three composites build whole dashboards from declarations in other lexicons:
38
+ \`RedDashboard\` from an otel \`spanmetrics\` connector, \`SloDashboard\` from a
39
+ prometheus \`Slo\` and \`AgentDashboard\` from the otel GenAI preset. Metric names
40
+ come from the declaration, so renaming one moves the panels. See
41
+ [Dashboards from Declarations](./composites/).
42
+
43
+ Checks catch a query aimed at a datasource nobody declared (GRAF101) or of the
44
+ wrong type (GRAF102), a \`$variable\` the dashboard doesn't declare (GRAF103),
45
+ duplicate uids and ids (GRAF104), panels off the grid or overlapping (GRAF105),
46
+ and anything Grafana's dashboard schema rejects (GRAF107).
47
+ `;
48
+
49
+ const outputFormat = `The grafana lexicon writes Grafana's own files, keyed by path under the
50
+ directory of the \`-o\` output:
51
+
52
+ - \`dashboards/<uid>.json\`, or \`dashboards/<folder>/<uid>.json\` for a
53
+ dashboard with a \`folder\`: the dashboard JSON, one file per dashboard.
54
+ - \`provisioning/datasources/chant.yaml\`: every declared datasource.
55
+ - \`provisioning/dashboards/chant.yaml\`: a file provider for the dashboards
56
+ directory; a default one unless a \`DashboardProvider\` is declared.
57
+
58
+ The primary output (the \`-o\` file itself) is a JSON index of the dashboards,
59
+ datasources and files that were built.
60
+
61
+ - Dashboard JSON carries the pinned schema's \`schemaVersion\`, no numeric \`id\`,
62
+ and every field Grafana's schema requires (link defaults, an empty
63
+ \`annotations.list\`, \`fieldConfig.overrides\`).
64
+ - Panels without a position are laid out in declaration order on the 24-column
65
+ grid. Ids and query \`refId\`s are assigned in order unless set.
66
+ - A declared \`Datasource\` is written as \`{ type, uid }\` wherever it is
67
+ referenced, and as its uid inside another datasource's \`jsonData\`.
68
+ - \`chant build\` rewrites JSON files with sorted keys; Grafana does not care
69
+ about key order.
70
+ - Grafana's files have no metadata channel chant could use without changing
71
+ what Grafana imports, so no ownership marker is stamped.
72
+ `;
73
+
74
+ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void> {
75
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
76
+
77
+ const config: DocsConfig = {
78
+ name: "grafana",
79
+ displayName: "Grafana",
80
+ description: "Typed Grafana dashboards, panels, queries and datasources, built to dashboard JSON and provisioning files",
81
+ distDir: join(pkgDir, "dist"),
82
+ outDir: join(pkgDir, "docs"),
83
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/grafana/",
84
+ overview,
85
+ outputFormat,
86
+ serviceFromType,
87
+ srcDir: join(pkgDir, "src"),
88
+ examplesDir: join(pkgDir, "examples"),
89
+ };
90
+
91
+ const result = docsPipeline(config);
92
+ writeDocsSite(config, result);
93
+
94
+ if (opts?.verbose) {
95
+ console.error(`Generated ${result.pages.size} documentation pages`);
96
+ }
97
+ }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for `npm run generate` in lexicon-grafana.
4
+ */
5
+ import { generate, writeGeneratedFiles } from "./generate";
6
+
7
+ const result = await generate({ verbose: true });
8
+ writeGeneratedFiles(result);
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The grafana lexicon's generate step, offline and deterministic.
3
+ *
4
+ * 1. `src/schema/<name>.gen.ts` from each vendored schema in `src/spec/schemas/`
5
+ * (see `src/pin.ts`). These are committed, and a test fails when they
6
+ * drift from what this step would write.
7
+ * 2. `src/generated/lexicon-grafana.json`, the registry of entity classes
8
+ * the package exports, derived from the catalog so the packaged
9
+ * `dist/meta.json` cannot disagree with the classes.
10
+ */
11
+
12
+ import { dirname, join } from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { mkdirSync, writeFileSync } from "fs";
15
+ import type { GenerateResult } from "@intentius/chant/codegen/generate";
16
+ import { SCHEMA_NAMES, type SchemaName } from "../pin";
17
+ import { digestMismatches, loadSchema } from "../spec/schemas";
18
+ import { schemaModule } from "./schema-types";
19
+
20
+ export type { GenerateResult };
21
+
22
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
23
+
24
+ /** The dashboard schema's own default `schemaVersion`, the one the serializer emits. */
25
+ function dashboardExtras(schema: Record<string, unknown>): string[] {
26
+ const defs = schema.definitions as Record<string, { properties?: Record<string, { default?: unknown }> }>;
27
+ const v = defs.Dashboard?.properties?.schemaVersion?.default;
28
+ if (typeof v !== "number") throw new Error("grafana: dashboard schema has no default schemaVersion");
29
+ return ["/** The dashboard `schemaVersion` the pinned schema defaults to, and the one chant emits. */", `export const DASHBOARD_SCHEMA_VERSION = ${v};`];
30
+ }
31
+
32
+ /** Every generated schema module, keyed by path relative to the package root. */
33
+ export function schemaModules(): Record<string, string> {
34
+ const out: Record<string, string> = {};
35
+ for (const name of SCHEMA_NAMES) {
36
+ const schema = loadSchema(name as SchemaName);
37
+ const extra = name === "dashboard" ? dashboardExtras(schema) : [];
38
+ out[`src/schema/${name}.gen.ts`] = schemaModule(name, schema, extra).source;
39
+ }
40
+ return out;
41
+ }
42
+
43
+ /** The packaged types entry re-exports the compiled declarations. */
44
+ const TYPES_DTS = `// The grafana lexicon's entity classes are hand-written in src/, with panel
45
+ // and query types generated into src/schema/; this entry re-exports them.
46
+ export * from "../index";
47
+ `;
48
+
49
+ export async function generate(opts?: { verbose?: boolean; force?: boolean }): Promise<GenerateResult> {
50
+ const bad = digestMismatches();
51
+ if (bad.length > 0) {
52
+ throw new Error(
53
+ `grafana: vendored schemas do not match GRAFANA_SCHEMA_PIN: ${bad.map((b) => `${b.name} (${b.actual.slice(0, 12)}…)`).join(", ")}. ` +
54
+ "Run `just fetch-schemas` and update src/pin.ts.",
55
+ );
56
+ }
57
+ const modules = schemaModules();
58
+ for (const [rel, source] of Object.entries(modules)) {
59
+ mkdirSync(dirname(join(pkgDir, rel)), { recursive: true });
60
+ writeFileSync(join(pkgDir, rel), source);
61
+ }
62
+ const { BUILTIN_CATALOG, lexiconRegistry } = await import("../catalog");
63
+ const lexiconJSON = `${JSON.stringify(lexiconRegistry(), null, 2)}\n`;
64
+ if (opts?.verbose) {
65
+ console.error(`grafana: ${Object.keys(modules).length} schema modules, ${BUILTIN_CATALOG.length} entity classes`);
66
+ }
67
+ return {
68
+ lexiconJSON,
69
+ typesDTS: TYPES_DTS,
70
+ indexTS: "",
71
+ resources: BUILTIN_CATALOG.length,
72
+ properties: 0,
73
+ enums: 0,
74
+ warnings: [],
75
+ };
76
+ }
77
+
78
+ /** Write the registry to src/generated/lexicon-grafana.json, for tools that read it from there. */
79
+ export function writeGeneratedFiles(result: GenerateResult): void {
80
+ const dir = join(pkgDir, "src", "generated");
81
+ mkdirSync(dir, { recursive: true });
82
+ writeFileSync(join(dir, "lexicon-grafana.json"), result.lexiconJSON);
83
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * grafana lexicon packaging, delegating to core's packagePipeline.
3
+ */
4
+
5
+ import { readFileSync } from "fs";
6
+ import { join, dirname } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import {
9
+ packagePipeline,
10
+ collectSkills,
11
+ type PackageOptions,
12
+ type PackageResult,
13
+ } from "@intentius/chant/codegen/package";
14
+ import { generate } from "./generate";
15
+ import { grafanaSkills } from "../skill-defs";
16
+
17
+ export type { PackageOptions, PackageResult };
18
+
19
+ // src/codegen/package.ts -> src/
20
+ const srcDir = dirname(dirname(fileURLToPath(import.meta.url)));
21
+
22
+ export async function packageLexicon(opts: PackageOptions = {}): Promise<PackageResult> {
23
+ const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
24
+ return packagePipeline(
25
+ {
26
+ generate: (genOpts) => generate({ verbose: genOpts.verbose, force: genOpts.force }),
27
+ buildManifest: () => ({
28
+ name: "grafana",
29
+ version: pkgJson.version ?? "0.0.1",
30
+ chantVersion: ">=0.1.0",
31
+ namespace: "Grafana",
32
+ intrinsics: [],
33
+ pseudoParameters: {},
34
+ }),
35
+ srcDir,
36
+ collectSkills: () => collectSkills(grafanaSkills()),
37
+ version: pkgJson.version ?? "0.0.1",
38
+ },
39
+ opts,
40
+ );
41
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * JSON Schema (draft-07, the subset cog emits) to TypeScript declarations.
3
+ *
4
+ * Each vendored schema becomes one module under `src/schema/`: one exported
5
+ * type per entry in `definitions`, with the schema's descriptions as doc
6
+ * comments. `required` is kept, so the types say what the schema says; the
7
+ * entity props wrap panel options in `DeepPartial` where Grafana fills the
8
+ * rest in on import.
9
+ */
10
+
11
+ import type { SchemaName } from "../pin";
12
+
13
+ interface Node {
14
+ type?: string | string[];
15
+ $ref?: string;
16
+ enum?: unknown[];
17
+ const?: unknown;
18
+ oneOf?: Node[];
19
+ anyOf?: Node[];
20
+ allOf?: Node[];
21
+ items?: Node | Node[];
22
+ properties?: Record<string, Node>;
23
+ additionalProperties?: boolean | Node;
24
+ required?: string[];
25
+ description?: string;
26
+ deprecated?: boolean;
27
+ default?: unknown;
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
32
+
33
+ /**
34
+ * Keys written quoted even though they are identifiers. The repo's egress
35
+ * scan (test/egress-catalogue.ts) reads a bare `fetch` as a call to the
36
+ * global, and the dashboard schema has an action option named `fetch`.
37
+ */
38
+ const QUOTED_KEYS = new Set(["fetch"]);
39
+
40
+ /** `dataquery` -> `Dataquery`: every exported type starts upper-case. */
41
+ export function typeName(defName: string): string {
42
+ return defName.charAt(0).toUpperCase() + defName.slice(1);
43
+ }
44
+
45
+ function refName(ref: string): string {
46
+ const m = /^#\/definitions\/(.+)$/.exec(ref);
47
+ if (!m) throw new Error(`schema-types: only local #/definitions refs are supported, got "${ref}"`);
48
+ return typeName(m[1]);
49
+ }
50
+
51
+ function literal(value: unknown): string {
52
+ return JSON.stringify(value);
53
+ }
54
+
55
+ function paren(t: string): string {
56
+ return /[|&]/.test(t) && !/^\{[\s\S]*\}$/.test(t) ? `(${t})` : t;
57
+ }
58
+
59
+ function union(parts: string[]): string {
60
+ const uniq = [...new Set(parts)];
61
+ if (uniq.includes("unknown")) return "unknown";
62
+ return uniq.length === 0 ? "never" : uniq.join(" | ");
63
+ }
64
+
65
+ function docComment(node: Node, indent: string): string[] {
66
+ const lines: string[] = [];
67
+ const text = typeof node.description === "string" ? node.description.trim() : "";
68
+ const deprecated = node.deprecated === true || typeof node["x-deprecation-message"] === "string";
69
+ if (!text && !deprecated) return lines;
70
+ lines.push(`${indent}/**`);
71
+ for (const raw of text ? text.replace(/\*\//g, "*\\/").split("\n") : []) {
72
+ const l = raw.trimEnd();
73
+ lines.push(`${indent} *${l ? ` ${l}` : ""}`);
74
+ }
75
+ if (deprecated) {
76
+ const msg = typeof node["x-deprecation-message"] === "string" ? ` ${String(node["x-deprecation-message"])}` : "";
77
+ lines.push(`${indent} * @deprecated${msg.replace(/\*\//g, "*\\/")}`);
78
+ }
79
+ lines.push(`${indent} */`);
80
+ return lines;
81
+ }
82
+
83
+ function objectType(node: Node, indent: string): string {
84
+ const props = node.properties ?? {};
85
+ const required = new Set(node.required ?? []);
86
+ const inner = `${indent} `;
87
+ const lines: string[] = ["{"];
88
+ for (const [key, child] of Object.entries(props)) {
89
+ lines.push(...docComment(child, inner));
90
+ const k = IDENT.test(key) && !QUOTED_KEYS.has(key) ? key : JSON.stringify(key);
91
+ lines.push(`${inner}${k}${required.has(key) ? "" : "?"}: ${tsType(child, inner)};`);
92
+ }
93
+ const ap = node.additionalProperties;
94
+ if (ap !== undefined && ap !== false) {
95
+ const t = ap === true || Object.keys(ap).length === 0 ? "unknown" : tsType(ap, inner);
96
+ lines.push(`${inner}[key: string]: ${Object.keys(props).length > 0 ? "unknown" : t};`);
97
+ }
98
+ lines.push(`${indent}}`);
99
+ return lines.length === 2 ? "Record<string, never>" : lines.join("\n");
100
+ }
101
+
102
+ /** The TypeScript type for one schema node. */
103
+ export function tsType(node: Node | boolean | undefined, indent = ""): string {
104
+ if (node === undefined || node === true) return "unknown";
105
+ if (node === false) return "never";
106
+ if (node.$ref) return refName(node.$ref);
107
+ if (node.const !== undefined) return literal(node.const);
108
+ if (Array.isArray(node.enum)) return union(node.enum.map(literal));
109
+ if (node.oneOf) return union(node.oneOf.map((n) => paren(tsType(n, indent))));
110
+ if (node.anyOf) return union(node.anyOf.map((n) => paren(tsType(n, indent))));
111
+ if (node.allOf) return node.allOf.map((n) => paren(tsType(n, indent))).join(" & ");
112
+
113
+ const types = Array.isArray(node.type) ? node.type : node.type ? [node.type] : [];
114
+ if (types.length === 0) return node.properties ? objectType(node, indent) : "unknown";
115
+ return union(
116
+ types.map((t) => {
117
+ switch (t) {
118
+ case "string":
119
+ return "string";
120
+ case "number":
121
+ case "integer":
122
+ return "number";
123
+ case "boolean":
124
+ return "boolean";
125
+ case "null":
126
+ return "null";
127
+ case "array": {
128
+ if (Array.isArray(node.items)) return `[${node.items.map((n) => tsType(n, indent)).join(", ")}]`;
129
+ return `${paren(tsType(node.items, indent))}[]`;
130
+ }
131
+ case "object": {
132
+ if (node.properties) return objectType(node, indent);
133
+ const ap = node.additionalProperties;
134
+ if (ap === false) return "Record<string, never>";
135
+ if (ap === undefined || ap === true || Object.keys(ap).length === 0) return "Record<string, unknown>";
136
+ return `Record<string, ${tsType(ap, indent)}>`;
137
+ }
138
+ default:
139
+ return "unknown";
140
+ }
141
+ }),
142
+ );
143
+ }
144
+
145
+ export interface GeneratedModule {
146
+ /** Module source, ready to write. */
147
+ source: string;
148
+ /** Exported type names, in definition order. */
149
+ types: string[];
150
+ }
151
+
152
+ /** One schema file to one TypeScript module. */
153
+ export function schemaModule(name: SchemaName, schema: Record<string, unknown>, extra: string[] = []): GeneratedModule {
154
+ const defs = (schema.definitions ?? {}) as Record<string, Node>;
155
+ const out: string[] = [
156
+ "// Code generated by chant generate from the vendored Grafana schema",
157
+ `// src/spec/schemas/${name}.jsonschema.json (see src/pin.ts). DO NOT EDIT.`,
158
+ "/* eslint-disable */",
159
+ "",
160
+ ];
161
+ const types: string[] = [];
162
+ for (const [defName, def] of Object.entries(defs)) {
163
+ const tn = typeName(defName);
164
+ types.push(tn);
165
+ out.push(...docComment(def, ""));
166
+ const t = tsType(def, "");
167
+ if (t.startsWith("{")) out.push(`export interface ${tn} ${t}`);
168
+ else out.push(`export type ${tn} = ${t};`);
169
+ out.push("");
170
+ }
171
+ out.push(...extra);
172
+ return { source: `${out.join("\n").replace(/\n+$/, "")}\n`, types };
173
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * `AgentDashboard`: latency and errors per model and per tool, and token
3
+ * usage per model, from the otel lexicon's GenAI preset.
4
+ *
5
+ * Metric names come from `genAiMetrics()` (or the `metrics` of
6
+ * `genAiComponents()`), and label names from the preset's GenAI attribute
7
+ * keys, so a preset with another `namespace` moves every query here.
8
+ */
9
+
10
+ import { Composite, type CompositeInstance } from "@intentius/chant/composite";
11
+ import { GENAI_ATTRIBUTES, type GenAiMetric, type GenAiMetrics } from "@intentius/chant-lexicon-otel/genai";
12
+ import { prometheusLabel, SPAN_STATUS_ERROR } from "@intentius/chant-lexicon-otel/metric-names";
13
+ import { Dashboard, type DashboardEntity } from "../dashboard";
14
+ import { Row, StatPanel, TimeSeriesPanel } from "../panels";
15
+ import { PromQuery } from "../query";
16
+ import { QueryVariable } from "../variables";
17
+ import { slugUid } from "../util";
18
+ import {
19
+ dashboardProps,
20
+ durationUnit,
21
+ legend,
22
+ quantile,
23
+ quantileName,
24
+ requireDatasource,
25
+ selector,
26
+ sumRate,
27
+ type DashboardOptions,
28
+ type Matcher,
29
+ } from "./shared";
30
+
31
+ export interface AgentDashboardProps extends DashboardOptions {
32
+ /** The preset's metrics: `genAiMetrics(options)` with the options the collector was built with, or `genAiComponents(options)`. */
33
+ genAi: GenAiMetrics | { metrics: GenAiMetrics };
34
+ /** The latency quantile per model and per tool (default 0.95). */
35
+ quantile?: number;
36
+ }
37
+
38
+ export type AgentDashboardMembers = { dashboard: DashboardEntity };
39
+
40
+ /** What `AgentDashboard(...)` returns: its dashboard, as `dashboard`. */
41
+ export type AgentDashboardInstance = CompositeInstance<AgentDashboardMembers> & AgentDashboardMembers;
42
+
43
+ function metricsOf(genAi: AgentDashboardProps["genAi"]): GenAiMetrics {
44
+ const m = (genAi as { metrics?: GenAiMetrics }).metrics ?? (genAi as GenAiMetrics);
45
+ if (!m || !m.calls || !m.duration || !m.inputTokens || !m.outputTokens) {
46
+ throw new Error("AgentDashboard: genAi must be genAiMetrics(...) or genAiComponents(...) from the otel lexicon");
47
+ }
48
+ return m;
49
+ }
50
+
51
+ function labelOf(metric: GenAiMetric, attribute: string): string {
52
+ if (!metric.dimensions.includes(attribute)) {
53
+ throw new Error(`AgentDashboard: ${metric.name} has no ${attribute} dimension, so it can't be broken down by it`);
54
+ }
55
+ return prometheusLabel(attribute);
56
+ }
57
+
58
+ /** The PromQL the agent dashboard runs, from the preset's metrics. Exposed for tests and for panels of your own. */
59
+ export function agentQueries(m: GenAiMetrics, q = 0.95) {
60
+ const model = labelOf(m.calls, GENAI_ATTRIBUTES.requestModel);
61
+ const tool = labelOf(m.calls, GENAI_ATTRIBUTES.toolName);
62
+ const errorType = labelOf(m.calls, GENAI_ATTRIBUTES.errorType);
63
+ const status = labelOf(m.calls, "status.code");
64
+ const service = labelOf(m.calls, "service.name");
65
+ labelOf(m.duration, GENAI_ATTRIBUTES.requestModel);
66
+ labelOf(m.duration, GENAI_ATTRIBUTES.toolName);
67
+ const tokenModel = labelOf(m.inputTokens, GENAI_ATTRIBUTES.requestModel);
68
+ labelOf(m.outputTokens, GENAI_ATTRIBUTES.requestModel);
69
+
70
+ const scope: Matcher[] = [
71
+ [service, "=~", "$service"],
72
+ [model, "=~", "$model"],
73
+ ];
74
+ const toolScope: Matcher[] = [...scope, [tool, "!=", ""]];
75
+ const err: Matcher = [status, "=", SPAN_STATUS_ERROR];
76
+ const calls = m.calls.prometheus;
77
+ const buckets = `${m.duration.prometheus}_bucket`;
78
+ const ratio = (matchers: Matcher[], by: string[]) =>
79
+ `${sumRate(selector(calls, [...matchers, err]), by)}\n/\n${sumRate(selector(calls, matchers), by)}`;
80
+ const tokens: Matcher[] = [[tokenModel, "=~", "$model"]];
81
+
82
+ return {
83
+ labels: { model, tool, errorType, status, service, tokenModel },
84
+ services: `label_values(${calls}, ${service})`,
85
+ models: `label_values(${calls}, ${model})`,
86
+ model: {
87
+ rate: sumRate(selector(calls, scope), [model]),
88
+ errorRatio: ratio(scope, [model]),
89
+ latency: quantile(q, selector(buckets, scope), [model]),
90
+ },
91
+ tool: {
92
+ rate: sumRate(selector(calls, toolScope), [tool]),
93
+ errorRatio: ratio(toolScope, [tool]),
94
+ latency: quantile(q, selector(buckets, toolScope), [tool]),
95
+ },
96
+ errorsByType: sumRate(selector(calls, [...scope, err]), [errorType]),
97
+ tokens: {
98
+ inputRate: sumRate(selector(m.inputTokens.prometheus, tokens), [tokenModel]),
99
+ outputRate: sumRate(selector(m.outputTokens.prometheus, tokens), [tokenModel]),
100
+ inputTotal: `sum(increase(${selector(m.inputTokens.prometheus, tokens)}[$__range]))`,
101
+ outputTotal: `sum(increase(${selector(m.outputTokens.prometheus, tokens)}[$__range]))`,
102
+ },
103
+ };
104
+ }
105
+
106
+ /**
107
+ * A dashboard for agents instrumented with GenAI spans: calls, errors and
108
+ * latency per model and per tool, errors by type, and token usage per
109
+ * model, from the GenAI preset's metrics.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * import { genAiMetrics } from "@intentius/chant-lexicon-otel";
114
+ * import { AgentDashboard } from "@intentius/chant-lexicon-grafana";
115
+ *
116
+ * export const agents = AgentDashboard({ genAi: genAiMetrics(), datasource: prometheus });
117
+ * ```
118
+ */
119
+ export const AgentDashboard = Composite<AgentDashboardProps, AgentDashboardMembers>((props) => {
120
+ requireDatasource("AgentDashboard", props.datasource);
121
+ if (!props.genAi) throw new Error("AgentDashboard: genAi is required (genAiMetrics(...) from the otel lexicon)");
122
+ const m = metricsOf(props.genAi);
123
+ const qv = props.quantile ?? 0.95;
124
+ if (!(typeof qv === "number" && qv > 0 && qv < 1)) throw new Error(`AgentDashboard: quantile must be between 0 and 1, got ${String(qv)}`);
125
+ const q = agentQueries(m, qv);
126
+ const ds = props.datasource;
127
+ const p = quantileName(qv);
128
+ const latencyUnit = durationUnit(m.duration.unit);
129
+ const prefix = m.calls.name.replace(/\.calls$/, "");
130
+
131
+ const service = new QueryVariable({
132
+ name: "service",
133
+ label: "Service",
134
+ datasource: ds,
135
+ query: q.services,
136
+ multi: true,
137
+ includeAll: true,
138
+ allValue: ".*",
139
+ refresh: "onTimeRangeChange",
140
+ sort: 1,
141
+ });
142
+ const model = new QueryVariable({
143
+ name: "model",
144
+ label: "Model",
145
+ datasource: ds,
146
+ query: q.models,
147
+ multi: true,
148
+ includeAll: true,
149
+ allValue: ".*",
150
+ refresh: "onTimeRangeChange",
151
+ sort: 1,
152
+ });
153
+
154
+ const series = (title: string, expr: string, by: string, unit: string, description?: string) =>
155
+ new TimeSeriesPanel({
156
+ title,
157
+ ...(description ? { description } : {}),
158
+ datasource: ds,
159
+ gridPos: { w: 8, h: 8 },
160
+ targets: [new PromQuery({ expr, legendFormat: legend(by) })],
161
+ fieldConfig: { defaults: { unit, ...(unit === "percentunit" ? { min: 0 } : {}) } },
162
+ });
163
+
164
+ const { labels } = q;
165
+ const models = new Row({
166
+ title: "Models",
167
+ panels: [
168
+ series("Calls by model", q.model.rate, labels.model, "reqps", `GenAI operations per second, from ${m.calls.prometheus}.`),
169
+ series("Errors by model", q.model.errorRatio, labels.model, "percentunit", `Share of operations that ended with ${labels.status}="${SPAN_STATUS_ERROR}".`),
170
+ series(`Latency ${p} by model`, q.model.latency, labels.model, latencyUnit, `From ${m.duration.prometheus}.`),
171
+ ],
172
+ });
173
+ const tools = new Row({
174
+ title: "Tools",
175
+ panels: [
176
+ series("Tool calls", q.tool.rate, labels.tool, "reqps", `Operations with a ${GENAI_ATTRIBUTES.toolName}, per second.`),
177
+ series("Errors by tool", q.tool.errorRatio, labels.tool, "percentunit"),
178
+ series(`Latency ${p} by tool`, q.tool.latency, labels.tool, latencyUnit),
179
+ ],
180
+ });
181
+ const errors = new Row({
182
+ title: "Errors",
183
+ panels: [
184
+ new TimeSeriesPanel({
185
+ title: "Errors by type",
186
+ description: `Failed operations per second by ${GENAI_ATTRIBUTES.errorType}.`,
187
+ datasource: ds,
188
+ gridPos: { w: 24, h: 8 },
189
+ targets: [new PromQuery({ expr: q.errorsByType, legendFormat: legend(labels.errorType) })],
190
+ fieldConfig: { defaults: { unit: "reqps" } },
191
+ }),
192
+ ],
193
+ });
194
+ const tokenStat = (title: string, expr: string) =>
195
+ new StatPanel({
196
+ title,
197
+ description: "Over the dashboard's time range.",
198
+ datasource: ds,
199
+ gridPos: { w: 4, h: 8 },
200
+ targets: [new PromQuery({ expr, legendFormat: title })],
201
+ options: { graphMode: "none", colorMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
202
+ fieldConfig: { defaults: { unit: "short", decimals: 0 } },
203
+ });
204
+ const tokenRate = (title: string, expr: string, source: string) =>
205
+ new TimeSeriesPanel({
206
+ title,
207
+ description: `Tokens per second by model, from ${source}.`,
208
+ datasource: ds,
209
+ gridPos: { w: 8, h: 8 },
210
+ targets: [new PromQuery({ expr, legendFormat: legend(labels.tokenModel) })],
211
+ fieldConfig: { defaults: { unit: "short" }, overrides: [] },
212
+ options: { tooltip: { mode: "multi" } },
213
+ });
214
+ const tokens = new Row({
215
+ title: "Tokens",
216
+ panels: [
217
+ tokenRate("Input tokens by model", q.tokens.inputRate, m.inputTokens.prometheus),
218
+ tokenRate("Output tokens by model", q.tokens.outputRate, m.outputTokens.prometheus),
219
+ tokenStat("Input tokens", q.tokens.inputTotal),
220
+ tokenStat("Output tokens", q.tokens.outputTotal),
221
+ ],
222
+ });
223
+
224
+ const dashboard = new Dashboard({
225
+ ...dashboardProps(props, {
226
+ title: "Agents: models, tools and tokens",
227
+ uid: slugUid(`${prefix}-agents`),
228
+ description: `Calls, errors and latency per model and tool, and token usage per model, from the ${prefix} GenAI metrics.`,
229
+ tags: ["genai", "agents"],
230
+ }),
231
+ variables: [service, model],
232
+ panels: [models, tools, errors, tokens],
233
+ });
234
+ return { dashboard };
235
+ }, "AgentDashboard");
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The composite catalog matches what this lexicon exports (#2662).
3
+ *
4
+ * `catalog.ts` is generated by `npm run generate:composite-catalogs` and is
5
+ * what `chant serve mcp` lists.
6
+ */
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import { isCompositeDefinition } from "@intentius/chant";
10
+ import * as composites from "./index";
11
+ import { compositeCatalog } from "./catalog";
12
+ import { grafanaPlugin } from "../plugin";
13
+
14
+ const exported = Object.entries(composites)
15
+ .filter(([, value]) => isCompositeDefinition(value))
16
+ .map(([name]) => name)
17
+ .sort();
18
+ const catalogued = compositeCatalog.map((entry) => entry.name).sort();
19
+
20
+ describe("the grafana composite catalog", () => {
21
+ test("every exported composite has an entry, and every entry names an exported composite", () => {
22
+ expect(exported).toEqual(["AgentDashboard", "RedDashboard", "SloDashboard"]);
23
+ expect(catalogued, "regenerate with `npm run generate:composite-catalogs -- grafana`").toEqual(exported);
24
+ });
25
+
26
+ test("every entry says what it is and what it bundles", () => {
27
+ for (const entry of compositeCatalog) {
28
+ expect(entry.lexicon, entry.name).toBe("grafana");
29
+ expect(entry.description.length, entry.name).toBeGreaterThan(0);
30
+ expect(entry.bundles, entry.name).toEqual(["Dashboard"]);
31
+ }
32
+ });
33
+
34
+ test("the plugin contributes it", () => {
35
+ expect(grafanaPlugin.composites?.()).toBe(compositeCatalog);
36
+ });
37
+ });