@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.
- package/README.md +50 -0
- package/dist/build.d.ts +116 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/catalog.d.ts +26 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +8 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +21 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +7 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/codegen/schema-types.d.ts +41 -0
- package/dist/codegen/schema-types.d.ts.map +1 -0
- package/dist/composites/agent-dashboard.d.ts +70 -0
- package/dist/composites/agent-dashboard.d.ts.map +1 -0
- package/dist/composites/catalog.d.ts +3 -0
- package/dist/composites/catalog.d.ts.map +1 -0
- package/dist/composites/index.d.ts +12 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/red-dashboard.d.ts +58 -0
- package/dist/composites/red-dashboard.d.ts.map +1 -0
- package/dist/composites/shared.d.ts +68 -0
- package/dist/composites/shared.d.ts.map +1 -0
- package/dist/composites/slo-dashboard.d.ts +62 -0
- package/dist/composites/slo-dashboard.d.ts.map +1 -0
- package/dist/dashboard.d.ts +84 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/datasource.d.ts +67 -0
- package/dist/datasource.d.ts.map +1 -0
- package/dist/detect.d.ts +20 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/integrity.json +22 -0
- package/dist/lint/audit-catalog.d.ts +13 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/graf101.d.ts +8 -0
- package/dist/lint/post-synth/graf101.d.ts.map +1 -0
- package/dist/lint/post-synth/graf102.d.ts +8 -0
- package/dist/lint/post-synth/graf102.d.ts.map +1 -0
- package/dist/lint/post-synth/graf103.d.ts +8 -0
- package/dist/lint/post-synth/graf103.d.ts.map +1 -0
- package/dist/lint/post-synth/graf104.d.ts +8 -0
- package/dist/lint/post-synth/graf104.d.ts.map +1 -0
- package/dist/lint/post-synth/graf105.d.ts +8 -0
- package/dist/lint/post-synth/graf105.d.ts.map +1 -0
- package/dist/lint/post-synth/graf106.d.ts +8 -0
- package/dist/lint/post-synth/graf106.d.ts.map +1 -0
- package/dist/lint/post-synth/graf107.d.ts +8 -0
- package/dist/lint/post-synth/graf107.d.ts.map +1 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts +16 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/rules/grafana-ast.d.ts +21 -0
- package/dist/lint/rules/grafana-ast.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +6 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/literal-secret.d.ts +12 -0
- package/dist/lint/rules/literal-secret.d.ts.map +1 -0
- package/dist/lint/rules/uid-syntax.d.ts +12 -0
- package/dist/lint/rules/uid-syntax.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +4 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +4 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/meta.json +107 -0
- package/dist/okf/index.md +39 -0
- package/dist/okf/rules/GRAF001.md +16 -0
- package/dist/okf/rules/GRAF002.md +15 -0
- package/dist/okf/rules/GRAF101.md +15 -0
- package/dist/okf/rules/GRAF102.md +15 -0
- package/dist/okf/rules/GRAF103.md +11 -0
- package/dist/okf/rules/GRAF104.md +15 -0
- package/dist/okf/rules/GRAF105.md +11 -0
- package/dist/okf/rules/GRAF106.md +11 -0
- package/dist/okf/rules/GRAF107.md +11 -0
- package/dist/okf/types/ConstantVariable.md +9 -0
- package/dist/okf/types/CustomVariable.md +9 -0
- package/dist/okf/types/Dashboard.md +14 -0
- package/dist/okf/types/DashboardProvider.md +9 -0
- package/dist/okf/types/Datasource.md +16 -0
- package/dist/okf/types/DatasourceVariable.md +9 -0
- package/dist/okf/types/GaugePanel.md +9 -0
- package/dist/okf/types/HeatmapPanel.md +9 -0
- package/dist/okf/types/IntervalVariable.md +9 -0
- package/dist/okf/types/LogsPanel.md +9 -0
- package/dist/okf/types/LokiQuery.md +9 -0
- package/dist/okf/types/PromQuery.md +9 -0
- package/dist/okf/types/QueryVariable.md +9 -0
- package/dist/okf/types/Row.md +9 -0
- package/dist/okf/types/StatPanel.md +9 -0
- package/dist/okf/types/TablePanel.md +9 -0
- package/dist/okf/types/TempoQuery.md +9 -0
- package/dist/okf/types/TextPanel.md +9 -0
- package/dist/okf/types/TextboxVariable.md +9 -0
- package/dist/okf/types/TimeSeriesPanel.md +9 -0
- package/dist/okf/types/TracesPanel.md +9 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/panels.d.ts +138 -0
- package/dist/panels.d.ts.map +1 -0
- package/dist/pin.d.ts +32 -0
- package/dist/pin.d.ts.map +1 -0
- package/dist/plugin.d.ts +12 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/query.d.ts +81 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/rules/graf101.ts +17 -0
- package/dist/rules/graf102.ts +17 -0
- package/dist/rules/graf103.ts +17 -0
- package/dist/rules/graf104.ts +17 -0
- package/dist/rules/graf105.ts +17 -0
- package/dist/rules/graf106.ts +17 -0
- package/dist/rules/graf107.ts +17 -0
- package/dist/rules/grafana-ast.ts +70 -0
- package/dist/rules/grafana-helpers.ts +68 -0
- package/dist/rules/literal-secret.ts +48 -0
- package/dist/rules/uid-syntax.ts +56 -0
- package/dist/schema/dashboard.gen.d.ts +1231 -0
- package/dist/schema/dashboard.gen.d.ts.map +1 -0
- package/dist/schema/gauge.gen.d.ts +57 -0
- package/dist/schema/gauge.gen.d.ts.map +1 -0
- package/dist/schema/heatmap.gen.d.ts +295 -0
- package/dist/schema/heatmap.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +18 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/logs.gen.d.ts +29 -0
- package/dist/schema/logs.gen.d.ts.map +1 -0
- package/dist/schema/loki.gen.d.ts +69 -0
- package/dist/schema/loki.gen.d.ts.map +1 -0
- package/dist/schema/prometheus.gen.d.ts +169 -0
- package/dist/schema/prometheus.gen.d.ts.map +1 -0
- package/dist/schema/stat.gen.d.ts +75 -0
- package/dist/schema/stat.gen.d.ts.map +1 -0
- package/dist/schema/table.gen.d.ts +295 -0
- package/dist/schema/table.gen.d.ts.map +1 -0
- package/dist/schema/tempo.gen.d.ts +136 -0
- package/dist/schema/tempo.gen.d.ts.map +1 -0
- package/dist/schema/text.gen.d.ts +16 -0
- package/dist/schema/text.gen.d.ts.map +1 -0
- package/dist/schema/timeseries.gen.d.ts +179 -0
- package/dist/schema/timeseries.gen.d.ts.map +1 -0
- package/dist/schema-validate.d.ts +20 -0
- package/dist/schema-validate.d.ts.map +1 -0
- package/dist/serializer.d.ts +22 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/skill-defs.d.ts +3 -0
- package/dist/skill-defs.d.ts.map +1 -0
- package/dist/skills/chant-grafana-provisioning.md +42 -0
- package/dist/skills/chant-grafana.md +93 -0
- package/dist/spec/fetch-cli.d.ts +3 -0
- package/dist/spec/fetch-cli.d.ts.map +1 -0
- package/dist/spec/schemas.d.ts +25 -0
- package/dist/spec/schemas.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/util.d.ts +17 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate-output.d.ts +42 -0
- package/dist/validate-output.d.ts.map +1 -0
- package/dist/validate.d.ts +12 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/variables.d.ts +100 -0
- package/dist/variables.d.ts.map +1 -0
- package/package.json +79 -0
- package/src/build.test.ts +210 -0
- package/src/build.ts +573 -0
- package/src/catalog.ts +116 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +97 -0
- package/src/codegen/generate-cli.ts +8 -0
- package/src/codegen/generate.ts +83 -0
- package/src/codegen/package.ts +41 -0
- package/src/codegen/schema-types.ts +173 -0
- package/src/composites/agent-dashboard.ts +235 -0
- package/src/composites/catalog.test.ts +37 -0
- package/src/composites/catalog.ts +223 -0
- package/src/composites/composites.test.ts +314 -0
- package/src/composites/index.ts +12 -0
- package/src/composites/red-dashboard.ts +159 -0
- package/src/composites/shared.ts +94 -0
- package/src/composites/slo-dashboard.ts +177 -0
- package/src/dashboard.ts +123 -0
- package/src/datasource.ts +93 -0
- package/src/detect.ts +30 -0
- package/src/generated/lexicon-grafana.json +107 -0
- package/src/import.e2e.test.ts +147 -0
- package/src/import.test.ts +45 -0
- package/src/index.ts +178 -0
- package/src/lint/audit-catalog.ts +87 -0
- package/src/lint/post-synth/graf101.ts +17 -0
- package/src/lint/post-synth/graf102.ts +17 -0
- package/src/lint/post-synth/graf103.ts +17 -0
- package/src/lint/post-synth/graf104.ts +17 -0
- package/src/lint/post-synth/graf105.ts +17 -0
- package/src/lint/post-synth/graf106.ts +17 -0
- package/src/lint/post-synth/graf107.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +68 -0
- package/src/lint/post-synth/index.ts +19 -0
- package/src/lint/post-synth/post-synth.test.ts +231 -0
- package/src/lint/rules/grafana-ast.ts +70 -0
- package/src/lint/rules/index.ts +9 -0
- package/src/lint/rules/literal-secret.ts +48 -0
- package/src/lint/rules/rules.test.ts +70 -0
- package/src/lint/rules/uid-syntax.ts +56 -0
- package/src/lsp/completions.test.ts +31 -0
- package/src/lsp/completions.ts +15 -0
- package/src/lsp/hover.test.ts +25 -0
- package/src/lsp/hover.ts +28 -0
- package/src/package-cli.ts +17 -0
- package/src/panels.ts +275 -0
- package/src/pin.ts +68 -0
- package/src/plugin.test.ts +50 -0
- package/src/plugin.ts +116 -0
- package/src/query.ts +160 -0
- package/src/schema/dashboard.gen.ts +1289 -0
- package/src/schema/gauge.gen.ts +64 -0
- package/src/schema/heatmap.gen.ts +321 -0
- package/src/schema/index.ts +17 -0
- package/src/schema/logs.gen.ts +34 -0
- package/src/schema/loki.gen.ts +77 -0
- package/src/schema/prometheus.gen.ts +181 -0
- package/src/schema/stat.gen.ts +86 -0
- package/src/schema/table.gen.ts +334 -0
- package/src/schema/tempo.gen.ts +146 -0
- package/src/schema/text.gen.ts +22 -0
- package/src/schema/timeseries.gen.ts +208 -0
- package/src/schema-validate.ts +143 -0
- package/src/schema.test.ts +76 -0
- package/src/serializer.test.ts +170 -0
- package/src/serializer.ts +37 -0
- package/src/skill-defs.ts +31 -0
- package/src/skills/chant-grafana-provisioning.md +42 -0
- package/src/skills/chant-grafana.md +93 -0
- package/src/spec/fetch-cli.ts +27 -0
- package/src/spec/schemas/dashboard.jsonschema.json +1901 -0
- package/src/spec/schemas/gauge.jsonschema.json +114 -0
- package/src/spec/schemas/heatmap.jsonschema.json +480 -0
- package/src/spec/schemas/logs.jsonschema.json +94 -0
- package/src/spec/schemas/loki.jsonschema.json +118 -0
- package/src/spec/schemas/prometheus.jsonschema.json +276 -0
- package/src/spec/schemas/stat.jsonschema.json +163 -0
- package/src/spec/schemas/table.jsonschema.json +751 -0
- package/src/spec/schemas/tempo.jsonschema.json +225 -0
- package/src/spec/schemas/text.jsonschema.json +72 -0
- package/src/spec/schemas/timeseries.jsonschema.json +471 -0
- package/src/spec/schemas.ts +56 -0
- package/src/util.ts +43 -0
- package/src/validate-cli.ts +7 -0
- package/src/validate-output.ts +380 -0
- package/src/validate.ts +108 -0
- package/src/variables.ts +161 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The checks behind GRAF101-GRAF107, as plain functions over built Grafana
|
|
3
|
+
* output: dashboard JSON documents and provisioned datasources. The
|
|
4
|
+
* post-synth checks run them over a build; anything else holding the same
|
|
5
|
+
* JSON (a test, another lexicon embedding dashboards) can call them directly.
|
|
6
|
+
*
|
|
7
|
+
* Checks that join a dashboard against datasources (GRAF101, GRAF102) only
|
|
8
|
+
* see the datasources passed in. In a build that is the build root being
|
|
9
|
+
* built (chant #1939): with no datasource declared in it they report
|
|
10
|
+
* nothing, and a datasource declared in another build root looks undeclared.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ProvisionedDatasource } from "./build";
|
|
14
|
+
import { GRID_COLUMNS } from "./build";
|
|
15
|
+
import { BUILTIN_DATASOURCE_UIDS } from "./datasource";
|
|
16
|
+
import { isBuiltinVariable } from "./variables";
|
|
17
|
+
import { isValidUid } from "./util";
|
|
18
|
+
import { validateDashboardSchema } from "./schema-validate";
|
|
19
|
+
|
|
20
|
+
export type GrafanaIssueCode = "GRAF101" | "GRAF102" | "GRAF103" | "GRAF104" | "GRAF105" | "GRAF106" | "GRAF107";
|
|
21
|
+
|
|
22
|
+
export interface GrafanaIssue {
|
|
23
|
+
code: GrafanaIssueCode;
|
|
24
|
+
severity: "error" | "warning";
|
|
25
|
+
message: string;
|
|
26
|
+
/** The dashboard uid or datasource name the issue is about. */
|
|
27
|
+
entity?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DashboardDoc {
|
|
31
|
+
/** Where it came from, e.g. its output file. */
|
|
32
|
+
source?: string;
|
|
33
|
+
json: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GrafanaArtifacts {
|
|
37
|
+
dashboards: DashboardDoc[];
|
|
38
|
+
datasources: ProvisionedDatasource[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Json = Record<string, unknown>;
|
|
42
|
+
interface Ref {
|
|
43
|
+
type?: string;
|
|
44
|
+
uid?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface PanelInfo {
|
|
48
|
+
panel: Json;
|
|
49
|
+
/** Panels inside a collapsed row are laid out in their own group. */
|
|
50
|
+
group: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function panelsOf(dashboard: Json): PanelInfo[] {
|
|
54
|
+
const out: PanelInfo[] = [];
|
|
55
|
+
const top = Array.isArray(dashboard.panels) ? (dashboard.panels as Json[]) : [];
|
|
56
|
+
for (const p of top) {
|
|
57
|
+
if (!p || typeof p !== "object") continue;
|
|
58
|
+
out.push({ panel: p, group: "" });
|
|
59
|
+
if (p.type === "row" && Array.isArray(p.panels)) {
|
|
60
|
+
for (const c of p.panels as Json[]) if (c && typeof c === "object") out.push({ panel: c, group: `row ${String(p.id)}` });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function describePanel(panel: Json): string {
|
|
67
|
+
const title = typeof panel.title === "string" && panel.title ? `"${panel.title}"` : "(untitled)";
|
|
68
|
+
return `panel ${title} (id ${String(panel.id ?? "?")})`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function dashName(d: Json): string {
|
|
72
|
+
return `Dashboard "${String(d.title ?? d.uid ?? "?")}"`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function variablesOf(d: Json): Json[] {
|
|
76
|
+
const list = (d.templating as { list?: unknown } | undefined)?.list;
|
|
77
|
+
return Array.isArray(list) ? (list as Json[]).filter((v) => v && typeof v === "object") : [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const VAR_REF = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::[^}]*)?\}|\[\[([A-Za-z_][A-Za-z0-9_]*)(?::[^\]]*)?\]\]|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
81
|
+
|
|
82
|
+
/** Every variable name referenced in a string. */
|
|
83
|
+
export function variableReferences(text: string): string[] {
|
|
84
|
+
const out: string[] = [];
|
|
85
|
+
for (const m of text.matchAll(VAR_REF)) out.push(m[1] ?? m[2] ?? m[3]);
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isVariableUid(uid: string | undefined): boolean {
|
|
90
|
+
return typeof uid === "string" && uid.includes("$");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stringsIn(value: unknown, skip: ReadonlySet<string>, out: string[] = []): string[] {
|
|
94
|
+
if (typeof value === "string") out.push(value);
|
|
95
|
+
else if (Array.isArray(value)) for (const v of value) stringsIn(v, skip, out);
|
|
96
|
+
else if (value && typeof value === "object") {
|
|
97
|
+
for (const [k, v] of Object.entries(value)) if (!skip.has(k)) stringsIn(v, skip, out);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const TARGET_SKIP = new Set(["refId", "datasource"]);
|
|
103
|
+
|
|
104
|
+
/** Every datasource ref a panel makes, with where it is made. */
|
|
105
|
+
function panelRefs(panel: Json): Array<{ ref: Ref; where: string }> {
|
|
106
|
+
const out: Array<{ ref: Ref; where: string }> = [];
|
|
107
|
+
const panelRef = panel.datasource as Ref | undefined;
|
|
108
|
+
if (panelRef && typeof panelRef === "object") out.push({ ref: panelRef, where: describePanel(panel) });
|
|
109
|
+
const targets = Array.isArray(panel.targets) ? (panel.targets as Json[]) : [];
|
|
110
|
+
for (const t of targets) {
|
|
111
|
+
const r = t?.datasource as Ref | undefined;
|
|
112
|
+
if (r && typeof r === "object") out.push({ ref: r, where: `${describePanel(panel)} query ${String(t.refId ?? "?")}` });
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isPseudoRef(ref: Ref): boolean {
|
|
118
|
+
return ref.type === "datasource" || ref.type === "grafana" || (ref.uid !== undefined && BUILTIN_DATASOURCE_UIDS.has(ref.uid));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── GRAF101, GRAF102: datasource references ─────────────────────
|
|
122
|
+
|
|
123
|
+
export function checkDatasourceRefs(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
124
|
+
const issues: GrafanaIssue[] = [];
|
|
125
|
+
if (a.datasources.length === 0) return issues; // declared elsewhere, or not at all: see #1939
|
|
126
|
+
const byUid = new Map(a.datasources.map((d) => [d.uid, d]));
|
|
127
|
+
for (const { json: d } of a.dashboards) {
|
|
128
|
+
const uid = String(d.uid ?? "");
|
|
129
|
+
const vars = new Map(variablesOf(d).map((v) => [String(v.name), v]));
|
|
130
|
+
const refs: Array<{ ref: Ref; where: string }> = [];
|
|
131
|
+
for (const { panel } of panelsOf(d)) {
|
|
132
|
+
if (panel.type === "row") continue;
|
|
133
|
+
refs.push(...panelRefs(panel));
|
|
134
|
+
const targets = Array.isArray(panel.targets) ? (panel.targets as Json[]) : [];
|
|
135
|
+
if (targets.length > 0 && !panel.datasource && targets.every((t) => !t?.datasource)) {
|
|
136
|
+
issues.push({
|
|
137
|
+
code: "GRAF101",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
message: `${dashName(d)} ${describePanel(panel)} has queries but no datasource; Grafana will send them to whichever datasource is its default.`,
|
|
140
|
+
entity: uid,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const v of vars.values()) {
|
|
145
|
+
if (v.type === "query" && v.datasource && typeof v.datasource === "object") refs.push({ ref: v.datasource as Ref, where: `variable "${String(v.name)}"` });
|
|
146
|
+
}
|
|
147
|
+
for (const { ref, where } of refs) {
|
|
148
|
+
if (isPseudoRef(ref) || ref.uid === undefined) continue;
|
|
149
|
+
if (isVariableUid(ref.uid)) {
|
|
150
|
+
const [name] = variableReferences(ref.uid);
|
|
151
|
+
const v = name ? vars.get(name) : undefined;
|
|
152
|
+
if (v && v.type === "datasource" && ref.type && v.query && v.query !== ref.type) {
|
|
153
|
+
issues.push({
|
|
154
|
+
code: "GRAF102",
|
|
155
|
+
severity: "error",
|
|
156
|
+
message: `${dashName(d)} ${where} expects a ${ref.type} datasource, but variable "${name}" chooses among ${String(v.query)} datasources.`,
|
|
157
|
+
entity: uid,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const declared = byUid.get(ref.uid);
|
|
163
|
+
if (!declared) {
|
|
164
|
+
issues.push({
|
|
165
|
+
code: "GRAF101",
|
|
166
|
+
severity: "error",
|
|
167
|
+
message: `${dashName(d)} ${where} uses datasource uid "${ref.uid}"${ref.type ? ` (${ref.type})` : ""}, which no declared Datasource has. Declared: ${[...byUid.keys()].map((k) => `"${k}"`).join(", ")}.`,
|
|
168
|
+
entity: uid,
|
|
169
|
+
});
|
|
170
|
+
} else if (ref.type && ref.type !== declared.type) {
|
|
171
|
+
issues.push({
|
|
172
|
+
code: "GRAF102",
|
|
173
|
+
severity: "error",
|
|
174
|
+
message: `${dashName(d)} ${where} sends a ${ref.type} query to datasource "${declared.name}", which is ${declared.type}.`,
|
|
175
|
+
entity: uid,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return issues;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── GRAF103: variables ──────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
export function checkVariables(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
186
|
+
const issues: GrafanaIssue[] = [];
|
|
187
|
+
for (const { json: d } of a.dashboards) {
|
|
188
|
+
const uid = String(d.uid ?? "");
|
|
189
|
+
const declared = new Set(variablesOf(d).map((v) => String(v.name)));
|
|
190
|
+
const report = (name: string, where: string) => {
|
|
191
|
+
if (declared.has(name) || isBuiltinVariable(name)) return;
|
|
192
|
+
issues.push({
|
|
193
|
+
code: "GRAF103",
|
|
194
|
+
severity: "error",
|
|
195
|
+
message: `${dashName(d)} ${where} uses $${name}, which is not one of its variables${declared.size > 0 ? ` (${[...declared].map((n) => `$${n}`).join(", ")})` : ""}.`,
|
|
196
|
+
entity: uid,
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
for (const { panel } of panelsOf(d)) {
|
|
200
|
+
const where = panel.type === "row" ? `row "${String(panel.title ?? "")}"` : describePanel(panel);
|
|
201
|
+
if (typeof panel.repeat === "string" && panel.repeat) report(panel.repeat, `${where} repeat`);
|
|
202
|
+
if (typeof panel.title === "string") for (const n of new Set(variableReferences(panel.title))) report(n, `${where} title`);
|
|
203
|
+
for (const { ref, where: w } of panelRefs(panel)) {
|
|
204
|
+
if (isVariableUid(ref.uid)) for (const n of variableReferences(ref.uid!)) report(n, `${w} datasource`);
|
|
205
|
+
}
|
|
206
|
+
const targets = Array.isArray(panel.targets) ? (panel.targets as Json[]) : [];
|
|
207
|
+
for (const t of targets) {
|
|
208
|
+
const names = new Set(stringsIn(t, TARGET_SKIP).flatMap(variableReferences));
|
|
209
|
+
for (const n of names) report(n, `${where} query ${String(t?.refId ?? "?")}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const v of variablesOf(d)) {
|
|
213
|
+
if (v.type !== "query") continue;
|
|
214
|
+
const names = new Set(stringsIn(v.query, new Set()).flatMap(variableReferences));
|
|
215
|
+
for (const n of names) if (n !== v.name) report(n, `variable "${String(v.name)}" query`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return issues;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── GRAF104: duplicates ─────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
function dupes<T>(items: T[], key: (t: T) => string | undefined): Map<string, number> {
|
|
224
|
+
const counts = new Map<string, number>();
|
|
225
|
+
for (const i of items) {
|
|
226
|
+
const k = key(i);
|
|
227
|
+
if (k !== undefined) counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
228
|
+
}
|
|
229
|
+
return new Map([...counts].filter(([, n]) => n > 1));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function checkDuplicates(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
233
|
+
const issues: GrafanaIssue[] = [];
|
|
234
|
+
const push = (message: string, entity?: string) => issues.push({ code: "GRAF104", severity: "error", message, entity });
|
|
235
|
+
for (const [uid, n] of dupes(a.dashboards, (d) => (typeof d.json.uid === "string" ? d.json.uid : undefined))) {
|
|
236
|
+
push(`${n} dashboards share the uid "${uid}"; Grafana keeps one of them.`, uid);
|
|
237
|
+
}
|
|
238
|
+
for (const [uid, n] of dupes(a.datasources, (d) => d.uid)) push(`${n} datasources share the uid "${uid}".`, uid);
|
|
239
|
+
for (const [name, n] of dupes(a.datasources, (d) => d.name)) push(`${n} datasources share the name "${name}"; Grafana needs names to be unique.`, name);
|
|
240
|
+
for (const { json: d } of a.dashboards) {
|
|
241
|
+
const uid = String(d.uid ?? "");
|
|
242
|
+
const panels = panelsOf(d).map((p) => p.panel);
|
|
243
|
+
for (const [id, n] of dupes(panels, (p) => (typeof p.id === "number" ? String(p.id) : undefined))) {
|
|
244
|
+
push(`${dashName(d)} has ${n} panels with id ${id}.`, uid);
|
|
245
|
+
}
|
|
246
|
+
for (const [name, n] of dupes(variablesOf(d), (v) => (typeof v.name === "string" ? v.name : undefined))) {
|
|
247
|
+
push(`${dashName(d)} declares the variable "${name}" ${n} times.`, uid);
|
|
248
|
+
}
|
|
249
|
+
for (const p of panels) {
|
|
250
|
+
const targets = Array.isArray(p.targets) ? (p.targets as Json[]) : [];
|
|
251
|
+
for (const [refId, n] of dupes(targets, (t) => (typeof t?.refId === "string" ? t.refId : undefined))) {
|
|
252
|
+
push(`${dashName(d)} ${describePanel(p)} has ${n} queries with refId "${refId}".`, uid);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return issues;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── GRAF105: grid ───────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
interface Box {
|
|
262
|
+
x: number;
|
|
263
|
+
y: number;
|
|
264
|
+
w: number;
|
|
265
|
+
h: number;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function boxOf(panel: Json): Box | undefined {
|
|
269
|
+
const g = panel.gridPos as Partial<Box> | undefined;
|
|
270
|
+
if (!g || typeof g !== "object") return undefined;
|
|
271
|
+
if ([g.x, g.y, g.w, g.h].some((v) => typeof v !== "number")) return undefined;
|
|
272
|
+
return g as Box;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function overlaps(a: Box, b: Box): boolean {
|
|
276
|
+
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function checkGrid(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
280
|
+
const issues: GrafanaIssue[] = [];
|
|
281
|
+
for (const { json: d } of a.dashboards) {
|
|
282
|
+
const uid = String(d.uid ?? "");
|
|
283
|
+
const groups = new Map<string, Array<{ panel: Json; box: Box }>>();
|
|
284
|
+
for (const { panel, group } of panelsOf(d)) {
|
|
285
|
+
const box = boxOf(panel);
|
|
286
|
+
if (!box) continue;
|
|
287
|
+
if (box.x < 0 || box.y < 0 || box.w < 1 || box.h < 1 || box.x + box.w > GRID_COLUMNS) {
|
|
288
|
+
issues.push({
|
|
289
|
+
code: "GRAF105",
|
|
290
|
+
severity: "error",
|
|
291
|
+
message: `${dashName(d)} ${describePanel(panel)} at x=${box.x} y=${box.y} w=${box.w} h=${box.h} does not fit Grafana's ${GRID_COLUMNS}-column grid.`,
|
|
292
|
+
entity: uid,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
groups.set(group, [...(groups.get(group) ?? []), { panel, box }]);
|
|
296
|
+
}
|
|
297
|
+
for (const members of groups.values()) {
|
|
298
|
+
for (let i = 0; i < members.length; i++) {
|
|
299
|
+
for (let j = i + 1; j < members.length; j++) {
|
|
300
|
+
if (!overlaps(members[i].box, members[j].box)) continue;
|
|
301
|
+
issues.push({
|
|
302
|
+
code: "GRAF105",
|
|
303
|
+
severity: "warning",
|
|
304
|
+
message: `${dashName(d)} ${describePanel(members[i].panel)} overlaps ${describePanel(members[j].panel)}; Grafana will move one of them.`,
|
|
305
|
+
entity: uid,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return issues;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── GRAF106: uids and titles Grafana rejects ────────────────────
|
|
315
|
+
|
|
316
|
+
export function checkIdentity(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
317
|
+
const issues: GrafanaIssue[] = [];
|
|
318
|
+
for (const { json: d } of a.dashboards) {
|
|
319
|
+
const uid = d.uid;
|
|
320
|
+
if (typeof uid !== "string" || !isValidUid(uid)) {
|
|
321
|
+
issues.push({
|
|
322
|
+
code: "GRAF106",
|
|
323
|
+
severity: "error",
|
|
324
|
+
message: `${dashName(d)} has uid ${JSON.stringify(uid)}; Grafana accepts 1-40 letters, digits, "-" and "_".`,
|
|
325
|
+
entity: typeof uid === "string" ? uid : undefined,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (typeof d.title !== "string" || d.title.trim() === "") {
|
|
329
|
+
issues.push({ code: "GRAF106", severity: "error", message: `Dashboard ${JSON.stringify(uid)} has no title; Grafana refuses to save it.`, entity: String(uid) });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const ds of a.datasources) {
|
|
333
|
+
if (!isValidUid(ds.uid)) {
|
|
334
|
+
issues.push({
|
|
335
|
+
code: "GRAF106",
|
|
336
|
+
severity: "error",
|
|
337
|
+
message: `Datasource "${ds.name}" has uid "${ds.uid}"; Grafana accepts 1-40 letters, digits, "-" and "_".`,
|
|
338
|
+
entity: ds.name,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return issues;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── GRAF107: the pinned schemas ─────────────────────────────────
|
|
346
|
+
|
|
347
|
+
export function checkSchema(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
348
|
+
const issues: GrafanaIssue[] = [];
|
|
349
|
+
for (const { json: d } of a.dashboards) {
|
|
350
|
+
for (const p of validateDashboardSchema(d)) {
|
|
351
|
+
issues.push({
|
|
352
|
+
code: "GRAF107",
|
|
353
|
+
severity: "error",
|
|
354
|
+
message: `${dashName(d)} ${p.path}: ${p.message} (Grafana schema).`,
|
|
355
|
+
entity: String(d.uid ?? ""),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return issues;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const BY_CODE: Record<GrafanaIssueCode, (a: GrafanaArtifacts) => GrafanaIssue[]> = {
|
|
363
|
+
GRAF101: (a) => checkDatasourceRefs(a).filter((i) => i.code === "GRAF101"),
|
|
364
|
+
GRAF102: (a) => checkDatasourceRefs(a).filter((i) => i.code === "GRAF102"),
|
|
365
|
+
GRAF103: checkVariables,
|
|
366
|
+
GRAF104: checkDuplicates,
|
|
367
|
+
GRAF105: checkGrid,
|
|
368
|
+
GRAF106: checkIdentity,
|
|
369
|
+
GRAF107: checkSchema,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/** The issues one check finds. */
|
|
373
|
+
export function issuesFor(code: GrafanaIssueCode, a: GrafanaArtifacts): GrafanaIssue[] {
|
|
374
|
+
return BY_CODE[code](a);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Every issue every check finds, in code order. */
|
|
378
|
+
export function validateGrafanaOutput(a: GrafanaArtifacts): GrafanaIssue[] {
|
|
379
|
+
return (Object.keys(BY_CODE) as GrafanaIssueCode[]).flatMap((c) => issuesFor(c, a));
|
|
380
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate the grafana lexicon's own artifacts: the vendored schemas match
|
|
3
|
+
* their pinned digests, the committed schema types match what generate would
|
|
4
|
+
* write, every required class is in the registry, and every built-in panel
|
|
5
|
+
* renders to a dashboard the pinned schema accepts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync } from "fs";
|
|
9
|
+
import { dirname, join } from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
|
|
12
|
+
import { BUILTIN_CATALOG, lexiconRegistry } from "./catalog";
|
|
13
|
+
import { digestMismatches } from "./spec/schemas";
|
|
14
|
+
import { schemaModules } from "./codegen/generate";
|
|
15
|
+
import * as panels from "./panels";
|
|
16
|
+
import { Dashboard } from "./dashboard";
|
|
17
|
+
import { renderDashboard } from "./build";
|
|
18
|
+
import { validateDashboardSchema } from "./schema-validate";
|
|
19
|
+
|
|
20
|
+
export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
|
|
21
|
+
|
|
22
|
+
/** Every entity class the package must keep exporting. */
|
|
23
|
+
export const REQUIRED_NAMES = [
|
|
24
|
+
"Dashboard",
|
|
25
|
+
"Datasource",
|
|
26
|
+
"DashboardProvider",
|
|
27
|
+
"Row",
|
|
28
|
+
"TimeSeriesPanel",
|
|
29
|
+
"StatPanel",
|
|
30
|
+
"GaugePanel",
|
|
31
|
+
"TablePanel",
|
|
32
|
+
"LogsPanel",
|
|
33
|
+
"TracesPanel",
|
|
34
|
+
"HeatmapPanel",
|
|
35
|
+
"TextPanel",
|
|
36
|
+
"PromQuery",
|
|
37
|
+
"TempoQuery",
|
|
38
|
+
"LokiQuery",
|
|
39
|
+
"QueryVariable",
|
|
40
|
+
"CustomVariable",
|
|
41
|
+
"IntervalVariable",
|
|
42
|
+
"DatasourceVariable",
|
|
43
|
+
"ConstantVariable",
|
|
44
|
+
"TextboxVariable",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
48
|
+
|
|
49
|
+
export async function validate(): Promise<ValidateResult> {
|
|
50
|
+
const checks: ValidateCheck[] = [];
|
|
51
|
+
const registry = lexiconRegistry();
|
|
52
|
+
|
|
53
|
+
const missing = REQUIRED_NAMES.filter((n) => !(n in registry));
|
|
54
|
+
checks.push(
|
|
55
|
+
missing.length === 0
|
|
56
|
+
? { name: "required-names", ok: true }
|
|
57
|
+
: { name: "required-names", ok: false, error: `Missing required names: ${missing.join(", ")}` },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const bad = digestMismatches();
|
|
61
|
+
checks.push(
|
|
62
|
+
bad.length === 0
|
|
63
|
+
? { name: "schemas-match-pin", ok: true }
|
|
64
|
+
: { name: "schemas-match-pin", ok: false, error: `Vendored schemas differ from GRAFANA_SCHEMA_PIN: ${bad.map((b) => b.name).join(", ")}` },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const stale = Object.entries(schemaModules())
|
|
68
|
+
.filter(([rel, source]) => {
|
|
69
|
+
try {
|
|
70
|
+
return readFileSync(join(pkgDir, rel), "utf-8") !== source;
|
|
71
|
+
} catch {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.map(([rel]) => rel);
|
|
76
|
+
checks.push(
|
|
77
|
+
stale.length === 0
|
|
78
|
+
? { name: "schema-types-in-sync", ok: true }
|
|
79
|
+
: { name: "schema-types-in-sync", ok: false, error: `Run npm run generate: stale ${stale.join(", ")}` },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const classes = (Object.entries(panels) as Array<[string, unknown]>).filter(
|
|
83
|
+
(e): e is [string, panels.PanelClass] => typeof e[1] === "function" && "definition" in (e[1] as object),
|
|
84
|
+
);
|
|
85
|
+
const broken: string[] = [];
|
|
86
|
+
for (const [name, Cls] of classes) {
|
|
87
|
+
try {
|
|
88
|
+
const json = renderDashboard(new Dashboard({ title: name, uid: "validate", panels: [new Cls({ title: name })] }));
|
|
89
|
+
const problems = validateDashboardSchema(json as unknown as Record<string, unknown>);
|
|
90
|
+
if (problems.length > 0) broken.push(`${name} (${problems[0].path}: ${problems[0].message})`);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
broken.push(`${name} (${err instanceof Error ? err.message : String(err)})`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
checks.push(
|
|
96
|
+
broken.length === 0
|
|
97
|
+
? { name: "panels-match-schema", ok: true }
|
|
98
|
+
: { name: "panels-match-schema", ok: false, error: `Panels whose dashboard fails the schema: ${broken.join(", ")}` },
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
checks.push(
|
|
102
|
+
BUILTIN_CATALOG.length === Object.keys(registry).length
|
|
103
|
+
? { name: "catalog-matches-registry", ok: true }
|
|
104
|
+
: { name: "catalog-matches-registry", ok: false, error: "catalog and registry disagree" },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return { success: checks.every((c) => c.ok), checks };
|
|
108
|
+
}
|
package/src/variables.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard variables (Grafana's "templating"): query, custom, interval,
|
|
3
|
+
* datasource, constant and textbox.
|
|
4
|
+
*
|
|
5
|
+
* A variable is declared on its own and listed in a dashboard's
|
|
6
|
+
* `variables`. Queries reference it as `$name` or `${name}` in their
|
|
7
|
+
* expression; GRAF103 checks every such reference names a declared
|
|
8
|
+
* variable. A `DatasourceVariable` can stand in for a datasource on a panel
|
|
9
|
+
* or query, and the reference becomes `${name}`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createProperty } from "@intentius/chant/runtime";
|
|
13
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
14
|
+
import type { DatasourceEntity, DatasourceRef } from "./datasource";
|
|
15
|
+
import type { VariableOption } from "./schema/dashboard.gen";
|
|
16
|
+
|
|
17
|
+
export type VariableKind = "query" | "custom" | "interval" | "datasource" | "constant" | "textbox";
|
|
18
|
+
|
|
19
|
+
/** Where the variable shows: with its label, without it, or not at all. */
|
|
20
|
+
export type VariableHide = "label" | "valueOnly" | "hidden";
|
|
21
|
+
|
|
22
|
+
interface CommonVariableProps {
|
|
23
|
+
/** The name queries use, `$name`. Letters, digits and `_`. */
|
|
24
|
+
name: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
hide?: VariableHide;
|
|
28
|
+
/** Keep the value out of the URL. */
|
|
29
|
+
skipUrlSync?: boolean;
|
|
30
|
+
/** The value selected when the dashboard loads. */
|
|
31
|
+
current?: VariableOption;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface MultiValueProps {
|
|
35
|
+
multi?: boolean;
|
|
36
|
+
includeAll?: boolean;
|
|
37
|
+
/** What `All` expands to, e.g. `.*` for a regex matcher. */
|
|
38
|
+
allValue?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A variable anything can hold before its datasource is known: its own datasource ref or a datasource variable. */
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
+
export type VariableDatasource = DatasourceEntity<any> | DatasourceRef | DatasourceVariableEntity<any>;
|
|
44
|
+
|
|
45
|
+
export interface QueryVariableProps extends CommonVariableProps, MultiValueProps {
|
|
46
|
+
datasource: VariableDatasource;
|
|
47
|
+
/** The datasource's variable query, e.g. `label_values(up, job)` for Prometheus. */
|
|
48
|
+
query: string;
|
|
49
|
+
regex?: string;
|
|
50
|
+
/** When to re-run the query. Defaults to on dashboard load. */
|
|
51
|
+
refresh?: "never" | "onLoad" | "onTimeRangeChange";
|
|
52
|
+
/** Grafana's `VariableSort` (0 disabled, 1 alphabetical asc, 2 desc, 3 numerical asc, …). */
|
|
53
|
+
sort?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CustomVariableProps extends CommonVariableProps, MultiValueProps {
|
|
57
|
+
values: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface IntervalVariableProps extends CommonVariableProps {
|
|
61
|
+
/** e.g. `["1m", "5m", "1h"]`. */
|
|
62
|
+
values: string[];
|
|
63
|
+
auto?: boolean;
|
|
64
|
+
autoCount?: number;
|
|
65
|
+
autoMin?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface DatasourceVariableProps<T extends string = string> extends CommonVariableProps, MultiValueProps {
|
|
69
|
+
/** The datasource plugin id to choose among, e.g. `prometheus`. */
|
|
70
|
+
pluginType: T;
|
|
71
|
+
/** Only datasources whose name matches. */
|
|
72
|
+
regex?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ConstantVariableProps extends Omit<CommonVariableProps, "hide"> {
|
|
76
|
+
value: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface TextboxVariableProps extends CommonVariableProps {
|
|
80
|
+
value?: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface VariableEntity<P = CommonVariableProps> extends Declarable {
|
|
84
|
+
readonly props: P;
|
|
85
|
+
readonly variableKind: VariableKind;
|
|
86
|
+
/** The variable name, `$name` in queries. */
|
|
87
|
+
readonly variableName: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface DatasourceVariableEntity<T extends string = string> extends VariableEntity<DatasourceVariableProps<T>> {
|
|
91
|
+
readonly variableKind: "datasource";
|
|
92
|
+
readonly pluginType: T;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const VARIABLE_TYPE_PREFIX = "Grafana::Variable::";
|
|
96
|
+
|
|
97
|
+
function variableClass<P extends CommonVariableProps | ConstantVariableProps>(kind: VariableKind, className: string) {
|
|
98
|
+
const Base = createProperty(`${VARIABLE_TYPE_PREFIX}${kind}`, "grafana") as unknown as (this: object, props: Record<string, unknown>) => void;
|
|
99
|
+
const Cls = function (this: object, props: P) {
|
|
100
|
+
Base.call(this, props as unknown as Record<string, unknown>);
|
|
101
|
+
Object.defineProperty(this, "variableKind", { value: kind, enumerable: false });
|
|
102
|
+
Object.defineProperty(this, "variableName", { value: props.name, enumerable: false });
|
|
103
|
+
if (kind === "datasource") {
|
|
104
|
+
Object.defineProperty(this, "pluginType", { value: (props as unknown as DatasourceVariableProps).pluginType, enumerable: false });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
Object.defineProperty(Cls, "name", { value: className });
|
|
108
|
+
return Cls;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A variable whose values come from a datasource query. */
|
|
112
|
+
export const QueryVariable = variableClass<QueryVariableProps>("query", "QueryVariable") as unknown as new (
|
|
113
|
+
props: QueryVariableProps,
|
|
114
|
+
) => VariableEntity<QueryVariableProps>;
|
|
115
|
+
|
|
116
|
+
/** A variable with a fixed list of values. */
|
|
117
|
+
export const CustomVariable = variableClass<CustomVariableProps>("custom", "CustomVariable") as unknown as new (
|
|
118
|
+
props: CustomVariableProps,
|
|
119
|
+
) => VariableEntity<CustomVariableProps>;
|
|
120
|
+
|
|
121
|
+
/** A time interval to choose, used as `$name` in range selectors. */
|
|
122
|
+
export const IntervalVariable = variableClass<IntervalVariableProps>("interval", "IntervalVariable") as unknown as new (
|
|
123
|
+
props: IntervalVariableProps,
|
|
124
|
+
) => VariableEntity<IntervalVariableProps>;
|
|
125
|
+
|
|
126
|
+
/** A choice of datasource of one plugin type. Usable wherever a datasource is. */
|
|
127
|
+
export const DatasourceVariable = variableClass<DatasourceVariableProps>("datasource", "DatasourceVariable") as unknown as {
|
|
128
|
+
new <T extends string>(props: DatasourceVariableProps<T>): DatasourceVariableEntity<T>;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** A hidden, fixed value. */
|
|
132
|
+
export const ConstantVariable = variableClass<ConstantVariableProps>("constant", "ConstantVariable") as unknown as new (
|
|
133
|
+
props: ConstantVariableProps,
|
|
134
|
+
) => VariableEntity<ConstantVariableProps>;
|
|
135
|
+
|
|
136
|
+
/** A free-text box. */
|
|
137
|
+
export const TextboxVariable = variableClass<TextboxVariableProps>("textbox", "TextboxVariable") as unknown as new (
|
|
138
|
+
props: TextboxVariableProps,
|
|
139
|
+
) => VariableEntity<TextboxVariableProps>;
|
|
140
|
+
|
|
141
|
+
export function isVariableEntity(value: unknown): value is VariableEntity {
|
|
142
|
+
return (
|
|
143
|
+
typeof value === "object" &&
|
|
144
|
+
value !== null &&
|
|
145
|
+
(value as Declarable).lexicon === "grafana" &&
|
|
146
|
+
typeof (value as Declarable).entityType === "string" &&
|
|
147
|
+
(value as Declarable).entityType.startsWith(VARIABLE_TYPE_PREFIX)
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isDatasourceVariable(value: unknown): value is DatasourceVariableEntity {
|
|
152
|
+
return isVariableEntity(value) && value.variableKind === "datasource";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Variable names Grafana defines itself; queries may use them without declaring anything. */
|
|
156
|
+
export function isBuiltinVariable(name: string): boolean {
|
|
157
|
+
return name.startsWith("__") || name === "timeFilter" || name === "interval" || name === "interval_ms";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A valid variable name: what `$name` can match. */
|
|
161
|
+
export const VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|