@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,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { hover } from "./hover";
3
+
4
+ describe("grafana LSP hover", () => {
5
+ it("describes a panel and its plugin id", () => {
6
+ const info = hover({
7
+ uri: "file:///dashboard.ts",
8
+ content: "new StatPanel({})",
9
+ position: { line: 0, character: 6 },
10
+ word: "StatPanel",
11
+ lineText: "new StatPanel({})",
12
+ });
13
+ expect(info?.contents).toContain("Grafana::Panel::stat");
14
+ expect(info?.contents).toContain("Panel plugin `stat`");
15
+ });
16
+
17
+ it("describes a query and its datasource type", () => {
18
+ const info = hover({ uri: "file:///q.ts", content: "new TempoQuery({})", position: { line: 0, character: 6 }, word: "TempoQuery", lineText: "new TempoQuery({})" });
19
+ expect(info?.contents).toContain("`tempo` datasources");
20
+ });
21
+
22
+ it("returns undefined for an unknown word", () => {
23
+ expect(hover({ uri: "file:///x.ts", content: "", position: { line: 0, character: 0 }, word: "Nope", lineText: "" })).toBeUndefined();
24
+ });
25
+ });
@@ -0,0 +1,28 @@
1
+ import type { HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
2
+ import { LexiconIndex, lexiconHover, type LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
3
+ import { BUILTIN_CATALOG, lexiconRegistry } from "../catalog";
4
+ import { GRAFANA_SCHEMA_PIN } from "../pin";
5
+
6
+ let cachedIndex: LexiconIndex | null = null;
7
+
8
+ function getIndex(): LexiconIndex {
9
+ cachedIndex ??= new LexiconIndex(lexiconRegistry());
10
+ return cachedIndex;
11
+ }
12
+
13
+ /** Hover for grafana entity classes: what it declares and, for panels and queries, the Grafana plugin id. */
14
+ export function hover(ctx: HoverContext): HoverInfo | undefined {
15
+ return lexiconHover(ctx, getIndex(), resourceHover);
16
+ }
17
+
18
+ function resourceHover(className: string, entry: LexiconEntry): HoverInfo | undefined {
19
+ const cat = BUILTIN_CATALOG.find((c) => c.className === className);
20
+ const lines = [`**${className}**`, "", `grafana type: \`${entry.resourceType}\``];
21
+ if (cat?.description) lines.push("", cat.description);
22
+ if (cat?.kind === "panel") {
23
+ lines.push("", `Panel plugin \`${cat.pluginId}\`. Options typed from ${GRAFANA_SCHEMA_PIN.source} ${GRAFANA_SCHEMA_PIN.ref}.`);
24
+ } else if (cat?.kind === "query") {
25
+ lines.push("", `For \`${cat.pluginId}\` datasources. Typed from ${GRAFANA_SCHEMA_PIN.source} ${GRAFANA_SCHEMA_PIN.ref}.`);
26
+ }
27
+ return { contents: lines.join("\n") };
28
+ }
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Thin entry point for `npm run bundle` in lexicon-grafana.
4
+ */
5
+ import { join, dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { writeBundleSpec } from "@intentius/chant/codegen/package";
8
+ import { generate, writeGeneratedFiles } from "./codegen/generate";
9
+ import { packageLexicon } from "./codegen/package";
10
+
11
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
12
+
13
+ writeGeneratedFiles(await generate({ verbose: true }));
14
+ const { spec, stats } = await packageLexicon({ verbose: true });
15
+ writeBundleSpec(spec, join(pkgDir, "dist"));
16
+
17
+ console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
package/src/panels.ts ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Panels and rows.
3
+ *
4
+ * Each built-in panel class is defined through `definePanel`, with its
5
+ * `options` and `fieldConfig.defaults.custom` typed from that panel's
6
+ * schema at `GRAFANA_SCHEMA_PIN`. Every option is optional: Grafana fills
7
+ * in what a panel leaves out when it loads the dashboard. A panel plugin
8
+ * chant doesn't ship (a community visualization, an in-house plugin) comes
9
+ * in through `definePanel` too and is laid out, serialized and checked the
10
+ * same way.
11
+ *
12
+ * `gridPos` is optional. A panel without `x` and `y` is placed by the
13
+ * dashboard: left to right in declaration order, wrapping at Grafana's
14
+ * 24-column width, below the previous row of panels. A panel with both is
15
+ * placed exactly there, and GRAF105 reports overlaps.
16
+ */
17
+
18
+ import { createProperty } from "@intentius/chant/runtime";
19
+ import type { Declarable } from "@intentius/chant/declarable";
20
+ import type { DeepPartial } from "./util";
21
+ import type { DatasourceInput, QueryEntity } from "./query";
22
+ import type { VariableEntity } from "./variables";
23
+ import type { SchemaName } from "./pin";
24
+ import type {
25
+ DashboardLink,
26
+ DataTransformerConfig,
27
+ FieldConfig,
28
+ FieldConfigSource,
29
+ GridPos,
30
+ } from "./schema/dashboard.gen";
31
+ import type * as timeseries from "./schema/timeseries.gen";
32
+ import type * as stat from "./schema/stat.gen";
33
+ import type * as gauge from "./schema/gauge.gen";
34
+ import type * as table from "./schema/table.gen";
35
+ import type * as logs from "./schema/logs.gen";
36
+ import type * as heatmap from "./schema/heatmap.gen";
37
+ import type * as text from "./schema/text.gen";
38
+
39
+ /** `fieldConfig` with `defaults.custom` typed for the panel. */
40
+ export interface PanelFieldConfig<C> {
41
+ defaults?: DeepPartial<Omit<FieldConfig, "custom">> & { custom?: DeepPartial<C> };
42
+ overrides?: FieldConfigSource["overrides"];
43
+ }
44
+
45
+ /** A panel link: Grafana's `DashboardLink` fields, all but `title` optional. */
46
+ export type PanelLink = Partial<DashboardLink> & { title: string; url?: string };
47
+
48
+ export interface PanelProps<O = Record<string, unknown>, C = Record<string, unknown>> {
49
+ title?: string;
50
+ description?: string;
51
+ /** Position and size in the 24-column grid. Leave out `x`/`y` to let the dashboard place it. */
52
+ gridPos?: Partial<GridPos>;
53
+ /** The datasource for every query that doesn't name its own. */
54
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
55
+ datasource?: DatasourceInput<any>;
56
+ /** The panel's queries. `refId`s default to A, B, C… in order. */
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ targets?: QueryEntity<any, any>[];
59
+ /** The panel plugin's own options, typed from its schema. */
60
+ options?: DeepPartial<O>;
61
+ fieldConfig?: PanelFieldConfig<C>;
62
+ transformations?: DataTransformerConfig[];
63
+ links?: PanelLink[];
64
+ /** Repeat the panel once per value of this variable. */
65
+ repeat?: VariableEntity | string;
66
+ repeatDirection?: "h" | "v";
67
+ maxPerRow?: number;
68
+ maxDataPoints?: number;
69
+ interval?: string;
70
+ timeFrom?: string;
71
+ timeShift?: string;
72
+ hideTimeOverride?: boolean;
73
+ transparent?: boolean;
74
+ pluginVersion?: string;
75
+ /** Panel id within the dashboard. Defaults to position order, 1, 2, 3… */
76
+ id?: number;
77
+ }
78
+
79
+ export interface PanelDefinition<T extends string = string> {
80
+ /** The panel plugin id Grafana stores in `type`. */
81
+ type: T;
82
+ className: string;
83
+ description?: string;
84
+ /** Size used when `gridPos` leaves `w` or `h` out. */
85
+ defaultSize: { w: number; h: number };
86
+ /** The vendored schema its options follow, when there is one (GRAF107 validates against it). */
87
+ schema?: SchemaName;
88
+ /** True only for the panels this package ships. */
89
+ builtin: boolean;
90
+ }
91
+
92
+ export interface PanelEntity<O = Record<string, unknown>, C = Record<string, unknown>> extends Declarable {
93
+ readonly props: PanelProps<O, C>;
94
+ readonly panelDefinition: PanelDefinition;
95
+ }
96
+
97
+ export interface PanelClass<T extends string = string, O = Record<string, unknown>, C = Record<string, unknown>> {
98
+ new (props?: PanelProps<O, C>): PanelEntity<O, C>;
99
+ readonly definition: PanelDefinition<T>;
100
+ }
101
+
102
+ export const PANEL_TYPE_PREFIX = "Grafana::Panel::";
103
+ export const ROW_TYPE = "Grafana::Row";
104
+
105
+ const REGISTRY_KEY = Symbol.for("chant.grafana.panelDefinitions");
106
+ function registry(): Map<string, PanelDefinition> {
107
+ const g = globalThis as unknown as Record<symbol, Map<string, PanelDefinition> | undefined>;
108
+ return (g[REGISTRY_KEY] ??= new Map());
109
+ }
110
+
111
+ /** Every registered panel definition, built-ins first. */
112
+ export function registeredPanels(): PanelDefinition[] {
113
+ return [...registry().values()];
114
+ }
115
+
116
+ export function panelDefinitionFor(type: string): PanelDefinition | undefined {
117
+ return registry().get(type);
118
+ }
119
+
120
+ const PLUGIN_ID = /^[a-z0-9][a-z0-9-_]*$/;
121
+
122
+ function makePanelClass<T extends string, O, C>(def: PanelDefinition<T>): PanelClass<T, O, C> {
123
+ if (!PLUGIN_ID.test(def.type)) throw new Error(`grafana: "${def.type}" is not a panel plugin id`);
124
+ if (def.type === "row") throw new Error('grafana: "row" is not a panel; use Row');
125
+ const existing = registry().get(def.type);
126
+ if (existing?.builtin && !def.builtin) {
127
+ throw new Error(`grafana: the "${def.type}" panel is built in (${existing.className}); use that class.`);
128
+ }
129
+ registry().set(def.type, def);
130
+ const Base = createProperty(`${PANEL_TYPE_PREFIX}${def.type}`, "grafana") as unknown as (
131
+ this: object,
132
+ props: Record<string, unknown>,
133
+ ) => void;
134
+ const Cls = function (this: object, props?: Record<string, unknown>) {
135
+ Base.call(this, props ?? {});
136
+ Object.defineProperty(this, "panelDefinition", { value: def, enumerable: false });
137
+ };
138
+ Object.defineProperty(Cls, "name", { value: def.className });
139
+ Object.defineProperty(Cls, "definition", { value: def, enumerable: false });
140
+ return Cls as unknown as PanelClass<T, O, C>;
141
+ }
142
+
143
+ /**
144
+ * Define a panel class for a panel plugin chant doesn't ship.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * interface PieOptions { pieType?: "pie" | "donut"; legend?: { showLegend?: boolean } }
149
+ *
150
+ * export const PieChartPanel = definePanel<PieOptions>()({
151
+ * type: "piechart",
152
+ * className: "PieChartPanel",
153
+ * defaultSize: { w: 8, h: 8 },
154
+ * });
155
+ * ```
156
+ */
157
+ export function definePanel<O = Record<string, unknown>, C = Record<string, unknown>>() {
158
+ return function <T extends string>(def: Omit<PanelDefinition<T>, "builtin">): PanelClass<T, O, C> {
159
+ return makePanelClass<T, O, C>({ ...def, builtin: false });
160
+ };
161
+ }
162
+
163
+ export function isPanelEntity(value: unknown): value is PanelEntity {
164
+ return (
165
+ typeof value === "object" &&
166
+ value !== null &&
167
+ (value as Declarable).lexicon === "grafana" &&
168
+ typeof (value as Declarable).entityType === "string" &&
169
+ (value as Declarable).entityType.startsWith(PANEL_TYPE_PREFIX)
170
+ );
171
+ }
172
+
173
+ // ── Built-in panels ─────────────────────────────────────────────
174
+
175
+ export const TimeSeriesPanel = makePanelClass<"timeseries", timeseries.Options, timeseries.FieldConfig>({
176
+ type: "timeseries",
177
+ className: "TimeSeriesPanel",
178
+ description: "Time series: values over time as lines, bars or points",
179
+ defaultSize: { w: 12, h: 8 },
180
+ schema: "timeseries",
181
+ builtin: true,
182
+ });
183
+
184
+ export const StatPanel = makePanelClass<"stat", stat.Options, Record<string, unknown>>({
185
+ type: "stat",
186
+ className: "StatPanel",
187
+ description: "Stat: one big number per series, with an optional sparkline",
188
+ defaultSize: { w: 6, h: 4 },
189
+ schema: "stat",
190
+ builtin: true,
191
+ });
192
+
193
+ export const GaugePanel = makePanelClass<"gauge", gauge.Options, Record<string, unknown>>({
194
+ type: "gauge",
195
+ className: "GaugePanel",
196
+ description: "Gauge: a value against its min, max and thresholds",
197
+ defaultSize: { w: 6, h: 6 },
198
+ schema: "gauge",
199
+ builtin: true,
200
+ });
201
+
202
+ export const TablePanel = makePanelClass<"table", table.Options, table.FieldConfig>({
203
+ type: "table",
204
+ className: "TablePanel",
205
+ description: "Table: query results as rows and columns",
206
+ defaultSize: { w: 12, h: 8 },
207
+ schema: "table",
208
+ builtin: true,
209
+ });
210
+
211
+ export const LogsPanel = makePanelClass<"logs", logs.Options, Record<string, unknown>>({
212
+ type: "logs",
213
+ className: "LogsPanel",
214
+ description: "Logs: log lines from a logs datasource such as Loki",
215
+ defaultSize: { w: 24, h: 10 },
216
+ schema: "logs",
217
+ builtin: true,
218
+ });
219
+
220
+ export const TracesPanel = makePanelClass<"traces", Record<string, unknown>, Record<string, unknown>>({
221
+ type: "traces",
222
+ className: "TracesPanel",
223
+ description: "Traces: one trace as a span timeline, from a tracing datasource such as Tempo",
224
+ defaultSize: { w: 24, h: 12 },
225
+ builtin: true,
226
+ });
227
+
228
+ export const HeatmapPanel = makePanelClass<"heatmap", heatmap.Options, heatmap.FieldConfig>({
229
+ type: "heatmap",
230
+ className: "HeatmapPanel",
231
+ description: "Heatmap: a distribution over time, e.g. histogram buckets",
232
+ defaultSize: { w: 12, h: 8 },
233
+ schema: "heatmap",
234
+ builtin: true,
235
+ });
236
+
237
+ export const TextPanel = makePanelClass<"text", text.Options, Record<string, unknown>>({
238
+ type: "text",
239
+ className: "TextPanel",
240
+ description: "Text: markdown or HTML, no queries",
241
+ defaultSize: { w: 24, h: 3 },
242
+ schema: "text",
243
+ builtin: true,
244
+ });
245
+
246
+ // ── Rows ────────────────────────────────────────────────────────
247
+
248
+ export interface RowProps {
249
+ title: string;
250
+ /** Collapsed rows keep their panels inside the row until opened. */
251
+ collapsed?: boolean;
252
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
253
+ panels?: PanelEntity<any, any>[];
254
+ /** Repeat the row once per value of this variable. */
255
+ repeat?: VariableEntity | string;
256
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
257
+ datasource?: DatasourceInput<any>;
258
+ id?: number;
259
+ }
260
+
261
+ export interface RowEntity extends Declarable {
262
+ readonly props: RowProps;
263
+ }
264
+
265
+ const RowBase = createProperty(ROW_TYPE, "grafana") as unknown as (this: object, props: Record<string, unknown>) => void;
266
+
267
+ /** A full-width row header; the panels listed in it are placed below it. */
268
+ export const Row = function (this: object, props: RowProps) {
269
+ RowBase.call(this, props as unknown as Record<string, unknown>);
270
+ } as unknown as new (props: RowProps) => RowEntity;
271
+ Object.defineProperty(Row, "name", { value: "Row" });
272
+
273
+ export function isRowEntity(value: unknown): value is RowEntity {
274
+ return typeof value === "object" && value !== null && (value as Declarable).entityType === ROW_TYPE && (value as Declarable).lexicon === "grafana";
275
+ }
package/src/pin.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The Grafana schemas this lexicon's types are generated from.
3
+ *
4
+ * Grafana defines dashboards, panel options and datasource queries as CUE
5
+ * kinds. `grafana/grafana-foundation-sdk` runs them through cog and
6
+ * publishes, beside its builder libraries, one JSON Schema per kind under
7
+ * `jsonschema/`. This lexicon vendors the files it uses into `src/spec/schemas/`
8
+ * (exact bytes, at the commit below) and generates `src/schema/*.gen.ts`
9
+ * from them. Nothing is fetched at build or test time.
10
+ *
11
+ * Bumping the pin: change `ref` and `commit`, run `just fetch-schemas`
12
+ * (downloads and prints the new digests), paste the digests here, run
13
+ * `npm run generate`, and review the diff of `src/schema/`.
14
+ */
15
+
16
+ export interface GrafanaSchemaPin {
17
+ /** Where the schema files come from. */
18
+ source: string;
19
+ /** The tag the commit is published under. */
20
+ ref: string;
21
+ /** The exact commit the vendored files were taken from. */
22
+ commit: string;
23
+ /** The Grafana kind registry version cog generated the schemas from. */
24
+ kindRegistry: string;
25
+ /** sha256 of each vendored file, keyed by schema name (`src/spec/schemas/<name>.jsonschema.json`). */
26
+ files: Readonly<Record<SchemaName, string>>;
27
+ }
28
+
29
+ export const SCHEMA_NAMES = [
30
+ "dashboard",
31
+ "timeseries",
32
+ "stat",
33
+ "gauge",
34
+ "table",
35
+ "logs",
36
+ "heatmap",
37
+ "text",
38
+ "prometheus",
39
+ "tempo",
40
+ "loki",
41
+ ] as const;
42
+
43
+ export type SchemaName = (typeof SCHEMA_NAMES)[number];
44
+
45
+ export const GRAFANA_SCHEMA_PIN: GrafanaSchemaPin = Object.freeze({
46
+ source: "github.com/grafana/grafana-foundation-sdk/jsonschema",
47
+ ref: "v0.0.20",
48
+ commit: "d9e3417a1a40a0b42379cedbd270411e599fa492",
49
+ kindRegistry: "v11.6.x",
50
+ files: Object.freeze({
51
+ dashboard: "67451cd5b288f97e3f68eccd52ffa4f75151fc87c669b8cd93c051056b294265",
52
+ timeseries: "77ba36d66bbfcb363eeb5e6957813ca4b4cbc5a5bc4e8a12034d4c3b8cbbbde4",
53
+ stat: "da547c8b1e52883a77e876f34d90d6034266ca8488a5601900ca26fc01d74b9e",
54
+ gauge: "154f065b05b36c84ff1b372edc2c7ef5a5dde32adec77daddf6bf9fab726eb96",
55
+ table: "eeaf780eb1be6fcd2ff0c098a1ba6eb051a8a37a0b2ea8db7c4d3bcdc5db6b97",
56
+ logs: "0254600ef9720bd2c906628304a7c362c0182b4bff2fd7d91352d95a269b2dda",
57
+ heatmap: "1e1905eef14ada48c121374ecad41412380b317e8183771d99a7504753d5005c",
58
+ text: "db6d2e3e2c576d1c78ac2e08930f60dbc22bb7ee21abd0cd9204be49f0dbce09",
59
+ prometheus: "726fc97eeb1e37791dbbf988c3cf40de17bb8e623926e6979c4a3f947cb1af87",
60
+ tempo: "21aec4c333c9b8e5e9228e85a6225c76161975abb54002e6f12e919e3b4e54ae",
61
+ loki: "03b81d6b952e3d31785c4b52637e2b7b1c170cc2ea8ec7ec671c21599508e31b",
62
+ }),
63
+ });
64
+
65
+ /** The raw URL of one schema file at the pinned commit. */
66
+ export function schemaUrl(name: SchemaName, pin: GrafanaSchemaPin = GRAFANA_SCHEMA_PIN): string {
67
+ return `https://raw.githubusercontent.com/grafana/grafana-foundation-sdk/${pin.commit}/jsonschema/${name}.jsonschema.json`;
68
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isLexiconPlugin } from "@intentius/chant/lexicon";
3
+ import { grafanaPlugin } from "./plugin";
4
+ import { grafanaAuditCatalog } from "./lint/audit-catalog";
5
+
6
+ describe("grafana plugin", () => {
7
+ it("is a valid LexiconPlugin", () => {
8
+ expect(isLexiconPlugin(grafanaPlugin)).toBe(true);
9
+ });
10
+
11
+ it("is named grafana and serializes under the GRAF prefix", () => {
12
+ expect(grafanaPlugin.name).toBe("grafana");
13
+ expect(grafanaPlugin.serializer.name).toBe("grafana");
14
+ expect(grafanaPlugin.serializer.rulePrefix).toBe("GRAF");
15
+ });
16
+
17
+ it("ships lint rules and post-synth checks, all under the GRAF prefix", () => {
18
+ const ids = [...grafanaPlugin.lintRules!().map((r) => r.id), ...grafanaPlugin.postSynthChecks!().map((c) => c.id)];
19
+ expect(ids).toEqual(["GRAF001", "GRAF002", "GRAF101", "GRAF102", "GRAF103", "GRAF104", "GRAF105", "GRAF106", "GRAF107"]);
20
+ });
21
+
22
+ it("catalogues every rule and check for chant audit, and nothing else", () => {
23
+ const ids = [...grafanaPlugin.lintRules!().map((r) => r.id), ...grafanaPlugin.postSynthChecks!().map((c) => c.id)];
24
+ expect(Object.keys(grafanaAuditCatalog).sort()).toEqual([...ids].sort());
25
+ for (const check of grafanaPlugin.postSynthChecks!()) expect(grafanaAuditCatalog[check.id].yamlBased).toBe(true);
26
+ });
27
+
28
+ it("detects dashboards and provisioning files and nothing else", () => {
29
+ expect(grafanaPlugin.detectTemplate!({ panels: [], schemaVersion: 41, title: "x" })).toBe(true);
30
+ expect(grafanaPlugin.detectTemplate!({ apiVersion: 1, datasources: [] })).toBe(true);
31
+ expect(grafanaPlugin.detectTemplate!({ apiVersion: 1, providers: [] })).toBe(true);
32
+ expect(grafanaPlugin.detectTemplate!({ apiVersion: "v1", kind: "ConfigMap" })).toBe(false);
33
+ expect(grafanaPlugin.detectTemplate!({ receivers: {}, service: { pipelines: {} } })).toBe(false);
34
+ });
35
+
36
+ it("loads its skills with content", () => {
37
+ const skills = grafanaPlugin.skills!();
38
+ expect(skills.map((s) => s.name)).toEqual(["chant-grafana", "chant-grafana-provisioning"]);
39
+ for (const s of skills) expect(s.content.length).toBeGreaterThan(200);
40
+ });
41
+
42
+ it("registers namespaced MCP contributions", async () => {
43
+ expect(grafanaPlugin.mcpTools!().map((t) => t.name)).toEqual(["grafana:diff"]);
44
+ const [resource] = grafanaPlugin.mcpResources!();
45
+ expect(resource.uri).toBe("grafana:resource-catalog");
46
+ const body = JSON.parse(await resource.handler());
47
+ expect(body.pin.commit).toMatch(/^[0-9a-f]{40}$/);
48
+ expect(body.entities.map((e: { className: string }) => e.className)).toContain("TimeSeriesPanel");
49
+ });
50
+ });
package/src/plugin.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { LexiconPlugin } from "@intentius/chant/lexicon";
2
+ import type { CompletionContext, HoverContext } from "@intentius/chant/lsp/types";
3
+ import type { McpResourceContribution } from "@intentius/chant/mcp/types";
4
+ import { createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
5
+ import { grafanaSerializer } from "./serializer";
6
+ import { rules } from "./lint/rules";
7
+ import { postSynthChecks } from "./lint/post-synth";
8
+ import { grafanaAuditCatalog } from "./lint/audit-catalog";
9
+ import { completions } from "./lsp/completions";
10
+ import { hover } from "./lsp/hover";
11
+ import { detectTemplate } from "./detect";
12
+ import { grafanaSkills } from "./skill-defs";
13
+ import { BUILTIN_CATALOG } from "./catalog";
14
+ import { GRAFANA_SCHEMA_PIN } from "./pin";
15
+ import { compositeCatalog } from "./composites/catalog";
16
+
17
+ const catalogResource: McpResourceContribution = {
18
+ uri: "grafana:resource-catalog",
19
+ name: "Grafana entity catalog",
20
+ description: "The dashboard, datasource, panel, query and variable classes this lexicon types, with the Grafana schema pin they follow",
21
+ mimeType: "application/json",
22
+ async handler(): Promise<string> {
23
+ return JSON.stringify({ pin: GRAFANA_SCHEMA_PIN, entities: BUILTIN_CATALOG });
24
+ },
25
+ };
26
+
27
+ /**
28
+ * Grafana lexicon plugin.
29
+ *
30
+ * Dashboards, panels, typed Prometheus, Tempo and Loki queries, variables
31
+ * and datasources, serialized to dashboard JSON and Grafana's provisioning
32
+ * files. Panel options and query models are generated from Grafana's own
33
+ * schemas at `GRAFANA_SCHEMA_PIN`; `definePanel` and `defineQuery` add
34
+ * plugins chant doesn't ship.
35
+ */
36
+ export const grafanaPlugin: LexiconPlugin = {
37
+ name: "grafana",
38
+ serializer: grafanaSerializer,
39
+
40
+ // ── Required lifecycle methods ────────────────────────────────
41
+
42
+ async generate(options?: { verbose?: boolean }): Promise<void> {
43
+ const { generate, writeGeneratedFiles } = await import("./codegen/generate");
44
+ writeGeneratedFiles(await generate(options));
45
+ },
46
+
47
+ async validate(_options?: { verbose?: boolean }): Promise<void> {
48
+ const { validate } = await import("./validate");
49
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
50
+ printValidationResult(await validate());
51
+ },
52
+
53
+ async coverage(_options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
54
+ const byKind = new Map<string, string[]>();
55
+ for (const c of BUILTIN_CATALOG) byKind.set(c.kind, [...(byKind.get(c.kind) ?? []), c.pluginId ?? c.className]);
56
+ console.error(`grafana: ${BUILTIN_CATALOG.length} entity classes, typed against ${GRAFANA_SCHEMA_PIN.source} ${GRAFANA_SCHEMA_PIN.ref}`);
57
+ for (const [kind, names] of byKind) console.error(` ${kind}: ${names.join(", ")}`);
58
+ console.error(" other panel and datasource plugins: definePanel, defineQuery");
59
+ },
60
+
61
+ async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
62
+ const { packageLexicon } = await import("./codegen/package");
63
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
64
+ const { join, dirname } = await import("path");
65
+ const { fileURLToPath } = await import("url");
66
+ const { spec, stats } = await packageLexicon(options);
67
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
68
+ writeBundleSpec(spec, join(pkgDir, "dist"));
69
+ console.error(`Packaged ${stats.resources} entities, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
70
+ },
71
+
72
+ // ── Optional extensions ────────────────────────────────────
73
+
74
+ lintRules() {
75
+ return rules;
76
+ },
77
+
78
+ postSynthChecks() {
79
+ return postSynthChecks;
80
+ },
81
+
82
+ auditCatalog() {
83
+ return grafanaAuditCatalog;
84
+ },
85
+
86
+ skills: grafanaSkills,
87
+
88
+ composites() {
89
+ return compositeCatalog;
90
+ },
91
+
92
+ mcpTools() {
93
+ return [createDiffTool(grafanaSerializer, "Compare current Grafana dashboard and provisioning output against the previous build", "grafana")];
94
+ },
95
+
96
+ mcpResources() {
97
+ return [catalogResource];
98
+ },
99
+
100
+ detectTemplate(data: unknown) {
101
+ return detectTemplate(data);
102
+ },
103
+
104
+ completionProvider(ctx: CompletionContext) {
105
+ return completions(ctx);
106
+ },
107
+
108
+ hoverProvider(ctx: HoverContext) {
109
+ return hover(ctx);
110
+ },
111
+
112
+ async docs(options?: { verbose?: boolean }) {
113
+ const { generateDocs } = await import("./codegen/docs");
114
+ return generateDocs(options);
115
+ },
116
+ };