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