@intentius/chant-lexicon-grafana 0.97.0 → 0.99.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 +15 -5
- package/dist/actions/grafana.d.ts +24 -0
- package/dist/actions/grafana.d.ts.map +1 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/alerting-build.d.ts +128 -0
- package/dist/alerting-build.d.ts.map +1 -0
- package/dist/alerting.d.ts +273 -0
- package/dist/alerting.d.ts.map +1 -0
- package/dist/api/apply.d.ts +156 -0
- package/dist/api/apply.d.ts.map +1 -0
- package/dist/api/bind.d.ts +32 -0
- package/dist/api/bind.d.ts.map +1 -0
- package/dist/api/client.d.ts +76 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/converge.d.ts +33 -0
- package/dist/api/converge.d.ts.map +1 -0
- package/dist/api/dashboards.d.ts +102 -0
- package/dist/api/dashboards.d.ts.map +1 -0
- package/dist/api/datasources.d.ts +26 -0
- package/dist/api/datasources.d.ts.map +1 -0
- package/dist/api/fake-grafana-writes.d.ts +41 -0
- package/dist/api/fake-grafana-writes.d.ts.map +1 -0
- package/dist/api/fake-grafana.d.ts +61 -0
- package/dist/api/fake-grafana.d.ts.map +1 -0
- package/dist/api/folders.d.ts +110 -0
- package/dist/api/folders.d.ts.map +1 -0
- package/dist/api/library-panels.d.ts +52 -0
- package/dist/api/library-panels.d.ts.map +1 -0
- package/dist/build.d.ts +30 -2
- 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/schema-types.d.ts +3 -3
- package/dist/codegen/schema-types.d.ts.map +1 -1
- package/dist/composites/agent-dashboard.d.ts.map +1 -1
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/index.d.ts +4 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/red-dashboard.d.ts +21 -2
- package/dist/composites/red-dashboard.d.ts.map +1 -1
- package/dist/composites/shared.d.ts +7 -0
- package/dist/composites/shared.d.ts.map +1 -1
- package/dist/composites/slo-alert-rules.d.ts +92 -0
- package/dist/composites/slo-alert-rules.d.ts.map +1 -0
- package/dist/config.d.ts +111 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/contact-point-secrets.d.ts +17 -0
- package/dist/contact-point-secrets.d.ts.map +1 -0
- package/dist/dashboard.d.ts +16 -2
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/datasource-refs.d.ts +95 -0
- package/dist/datasource-refs.d.ts.map +1 -0
- package/dist/datasource.d.ts +29 -0
- package/dist/datasource.d.ts.map +1 -1
- package/dist/deep-observe-hooks.d.ts +64 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +42 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +49 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/detect.d.ts +28 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/export-resources.d.ts +38 -0
- package/dist/export-resources.d.ts.map +1 -0
- package/dist/import/alerting.d.ts +40 -0
- package/dist/import/alerting.d.ts.map +1 -0
- package/dist/import/edits.d.ts +47 -0
- package/dist/import/edits.d.ts.map +1 -0
- package/dist/import/embedded.d.ts +14 -0
- package/dist/import/embedded.d.ts.map +1 -0
- package/dist/import/generator.d.ts +60 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/live-export.d.ts +108 -0
- package/dist/import/live-export.d.ts.map +1 -0
- package/dist/import/mappings.d.ts +83 -0
- package/dist/import/mappings.d.ts.map +1 -0
- package/dist/import/model.d.ts +98 -0
- package/dist/import/model.d.ts.map +1 -0
- package/dist/import/normalize-alerting.d.ts +26 -0
- package/dist/import/normalize-alerting.d.ts.map +1 -0
- package/dist/import/normalize.d.ts +74 -0
- package/dist/import/normalize.d.ts.map +1 -0
- package/dist/import/parser.d.ts +110 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/import/report.d.ts +26 -0
- package/dist/import/report.d.ts.map +1 -0
- package/dist/import/testdata/fixtures.d.ts +35 -0
- package/dist/import/testdata/fixtures.d.ts.map +1 -0
- package/dist/import/v2.d.ts +72 -0
- package/dist/import/v2.d.ts.map +1 -0
- package/dist/index.d.ts +26 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +18 -10
- package/dist/k8s.d.ts +107 -0
- package/dist/k8s.d.ts.map +1 -0
- package/dist/lint/audit-catalog.d.ts +2 -1
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/graf101.d.ts +2 -2
- package/dist/lint/post-synth/graf102.d.ts +1 -1
- package/dist/lint/post-synth/graf107.d.ts +1 -1
- package/dist/lint/post-synth/graf108.d.ts +8 -0
- package/dist/lint/post-synth/graf108.d.ts.map +1 -0
- package/dist/lint/post-synth/graf109.d.ts +8 -0
- package/dist/lint/post-synth/graf109.d.ts.map +1 -0
- package/dist/lint/post-synth/graf111.d.ts +8 -0
- package/dist/lint/post-synth/graf111.d.ts.map +1 -0
- package/dist/lint/post-synth/graf112.d.ts +8 -0
- package/dist/lint/post-synth/graf112.d.ts.map +1 -0
- package/dist/lint/post-synth/graf113.d.ts +8 -0
- package/dist/lint/post-synth/graf113.d.ts.map +1 -0
- package/dist/lint/post-synth/graf114.d.ts +8 -0
- package/dist/lint/post-synth/graf114.d.ts.map +1 -0
- package/dist/lint/post-synth/graf115.d.ts +8 -0
- package/dist/lint/post-synth/graf115.d.ts.map +1 -0
- package/dist/lint/post-synth/grafana-helpers.d.ts +7 -5
- 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/lint/rules/literal-secret.d.ts +7 -3
- package/dist/lint/rules/literal-secret.d.ts.map +1 -1
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/members.d.ts +27 -0
- package/dist/members.d.ts.map +1 -0
- package/dist/meta.json +135 -0
- package/dist/okf/index.md +36 -2
- package/dist/okf/rules/GRAF002.md +3 -2
- package/dist/okf/rules/GRAF101.md +3 -2
- package/dist/okf/rules/GRAF102.md +1 -0
- package/dist/okf/rules/GRAF108.md +11 -0
- package/dist/okf/rules/GRAF109.md +15 -0
- package/dist/okf/rules/GRAF111.md +11 -0
- package/dist/okf/rules/GRAF112.md +16 -0
- package/dist/okf/rules/GRAF113.md +16 -0
- package/dist/okf/rules/GRAF114.md +11 -0
- package/dist/okf/rules/GRAF115.md +11 -0
- package/dist/okf/types/AlertListPanel.md +9 -0
- package/dist/okf/types/AlertQuery.md +9 -0
- package/dist/okf/types/AlertRule.md +9 -0
- package/dist/okf/types/AlertRuleGroup.md +9 -0
- package/dist/okf/types/BarChartPanel.md +9 -0
- package/dist/okf/types/BarGaugePanel.md +9 -0
- package/dist/okf/types/CanvasPanel.md +9 -0
- package/dist/okf/types/ClassicConditionsExpression.md +9 -0
- package/dist/okf/types/ContactPoint.md +14 -0
- package/dist/okf/types/Dashboard.md +1 -0
- package/dist/okf/types/Datasource.md +3 -2
- package/dist/okf/types/ExternalDatasource.md +15 -0
- package/dist/okf/types/FlameGraphPanel.md +9 -0
- package/dist/okf/types/GeomapPanel.md +9 -0
- package/dist/okf/types/HistogramPanel.md +9 -0
- package/dist/okf/types/MathExpression.md +9 -0
- package/dist/okf/types/MuteTiming.md +13 -0
- package/dist/okf/types/NodeGraphPanel.md +9 -0
- package/dist/okf/types/NotificationPolicy.md +9 -0
- package/dist/okf/types/NotificationTemplate.md +9 -0
- package/dist/okf/types/PieChartPanel.md +9 -0
- package/dist/okf/types/ReduceExpression.md +9 -0
- package/dist/okf/types/ResampleExpression.md +9 -0
- package/dist/okf/types/SqlExpression.md +9 -0
- package/dist/okf/types/StateTimelinePanel.md +9 -0
- package/dist/okf/types/StatusHistoryPanel.md +9 -0
- package/dist/okf/types/ThresholdExpression.md +9 -0
- package/dist/okf/types/TrendPanel.md +9 -0
- package/dist/okf/types/XYChartPanel.md +9 -0
- package/dist/op/activities/grafana-apply.d.ts +75 -0
- package/dist/op/activities/grafana-apply.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +9 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/builders.d.ts +24 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/ownership.d.ts +67 -0
- package/dist/ownership.d.ts.map +1 -0
- package/dist/panel-options.d.ts +65 -0
- package/dist/panel-options.d.ts.map +1 -0
- package/dist/panels.d.ts +46 -9
- package/dist/panels.d.ts.map +1 -1
- package/dist/pin.d.ts +29 -7
- package/dist/pin.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-check.d.ts +51 -0
- package/dist/promql-check.d.ts.map +1 -0
- package/dist/query.d.ts +9 -8
- package/dist/query.d.ts.map +1 -1
- package/dist/rules/graf101.ts +3 -3
- package/dist/rules/graf102.ts +1 -1
- package/dist/rules/graf107.ts +1 -1
- package/dist/rules/graf108.ts +17 -0
- package/dist/rules/graf109.ts +17 -0
- package/dist/rules/graf111.ts +17 -0
- package/dist/rules/graf112.ts +17 -0
- package/dist/rules/graf113.ts +17 -0
- package/dist/rules/graf114.ts +17 -0
- package/dist/rules/graf115.ts +17 -0
- package/dist/rules/grafana-helpers.ts +15 -8
- package/dist/rules/literal-secret.ts +60 -6
- package/dist/schema/barchart.gen.d.ts +198 -0
- package/dist/schema/barchart.gen.d.ts.map +1 -0
- package/dist/schema/bargauge.gen.d.ts +97 -0
- package/dist/schema/bargauge.gen.d.ts.map +1 -0
- package/dist/schema/canvas.gen.d.ts +152 -0
- package/dist/schema/canvas.gen.d.ts.map +1 -0
- package/dist/schema/dashboard.gen.d.ts +196 -21
- package/dist/schema/dashboard.gen.d.ts.map +1 -1
- package/dist/schema/expr.gen.d.ts +606 -0
- package/dist/schema/expr.gen.d.ts.map +1 -0
- package/dist/schema/gauge.gen.d.ts.map +1 -1
- package/dist/schema/geomap.gen.d.ts +100 -0
- package/dist/schema/geomap.gen.d.ts.map +1 -0
- package/dist/schema/heatmap.gen.d.ts.map +1 -1
- package/dist/schema/histogram.gen.d.ts +132 -0
- package/dist/schema/histogram.gen.d.ts.map +1 -0
- package/dist/schema/index.d.ts +12 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/logs.gen.d.ts +1 -0
- package/dist/schema/logs.gen.d.ts.map +1 -1
- package/dist/schema/loki.gen.d.ts.map +1 -1
- package/dist/schema/nodegraph.gen.d.ts +49 -0
- package/dist/schema/nodegraph.gen.d.ts.map +1 -0
- package/dist/schema/piechart.gen.d.ts +127 -0
- package/dist/schema/piechart.gen.d.ts.map +1 -0
- package/dist/schema/prometheus.gen.d.ts.map +1 -1
- package/dist/schema/stat.gen.d.ts.map +1 -1
- package/dist/schema/statetimeline.gen.d.ts +124 -0
- package/dist/schema/statetimeline.gen.d.ts.map +1 -0
- package/dist/schema/statushistory.gen.d.ts +114 -0
- package/dist/schema/statushistory.gen.d.ts.map +1 -0
- package/dist/schema/table.gen.d.ts +22 -3
- package/dist/schema/table.gen.d.ts.map +1 -1
- package/dist/schema/tempo.gen.d.ts.map +1 -1
- package/dist/schema/text.gen.d.ts.map +1 -1
- package/dist/schema/timeseries.gen.d.ts +5 -1
- package/dist/schema/timeseries.gen.d.ts.map +1 -1
- package/dist/schema/trend.gen.d.ts +180 -0
- package/dist/schema/trend.gen.d.ts.map +1 -0
- package/dist/schema/xychart.gen.d.ts +145 -0
- package/dist/schema/xychart.gen.d.ts.map +1 -0
- package/dist/schema-validate.d.ts +30 -4
- package/dist/schema-validate.d.ts.map +1 -1
- package/dist/serializer.d.ts +11 -3
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-grafana-alerting.md +49 -0
- package/dist/skills/chant-grafana-provisioning.md +3 -3
- package/dist/skills/chant-grafana.md +12 -6
- package/dist/spec/fetch-units-cli.d.ts +3 -0
- package/dist/spec/fetch-units-cli.d.ts.map +1 -0
- package/dist/spec/overlay.d.ts +43 -0
- package/dist/spec/overlay.d.ts.map +1 -0
- package/dist/spec/schemas.d.ts +13 -7
- package/dist/spec/schemas.d.ts.map +1 -1
- package/dist/spec/units.d.ts +35 -0
- package/dist/spec/units.d.ts.map +1 -0
- package/dist/spec/units.gen.d.ts +2 -0
- package/dist/spec/units.gen.d.ts.map +1 -0
- package/dist/transformations.d.ts +459 -0
- package/dist/transformations.d.ts.map +1 -0
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/validate-alerting.d.ts +65 -0
- package/dist/validate-alerting.d.ts.map +1 -0
- package/dist/validate-output.d.ts +49 -9
- package/dist/validate-output.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/variables.d.ts +7 -2
- package/dist/variables.d.ts.map +1 -1
- package/package.json +12 -4
- package/src/actions/actions.test.ts +24 -0
- package/src/actions/grafana.ts +40 -0
- package/src/actions/index.ts +1 -0
- package/src/alerting-build.ts +376 -0
- package/src/alerting.e2e.test.ts +212 -0
- package/src/alerting.test.ts +272 -0
- package/src/alerting.ts +435 -0
- package/src/api/apply.test.ts +238 -0
- package/src/api/apply.ts +384 -0
- package/src/api/bind.ts +58 -0
- package/src/api/client.ts +148 -0
- package/src/api/converge.ts +63 -0
- package/src/api/dashboards.ts +247 -0
- package/src/api/datasources.ts +33 -0
- package/src/api/fake-grafana-writes.ts +201 -0
- package/src/api/fake-grafana.ts +186 -0
- package/src/api/folders.ts +265 -0
- package/src/api/library-panels.ts +101 -0
- package/src/build.test.ts +210 -5
- package/src/build.ts +180 -43
- package/src/catalog.ts +86 -2
- package/src/codegen/docs.ts +42 -9
- package/src/codegen/schema-types.ts +16 -6
- package/src/composites/agent-dashboard.ts +6 -3
- package/src/composites/catalog.test.ts +2 -2
- package/src/composites/catalog.ts +82 -0
- package/src/composites/composites.test.ts +73 -9
- package/src/composites/index.ts +4 -2
- package/src/composites/queries.e2e.test.ts +374 -0
- package/src/composites/red-dashboard.ts +83 -12
- package/src/composites/shared.ts +11 -0
- package/src/composites/slo-alert-rules.ts +149 -0
- package/src/config.test.ts +61 -0
- package/src/config.ts +138 -0
- package/src/contact-point-secrets.ts +63 -0
- package/src/dashboard.ts +16 -2
- package/src/datasource-refs.test.ts +56 -0
- package/src/datasource-refs.ts +189 -0
- package/src/datasource.ts +52 -0
- package/src/deep-observe-hooks.ts +252 -0
- package/src/deep-observe.test.ts +265 -0
- package/src/deep-observe.ts +164 -0
- package/src/describe-resources.test.ts +255 -0
- package/src/describe-resources.ts +209 -0
- package/src/detect.ts +79 -2
- package/src/export-resources.test.ts +129 -0
- package/src/export-resources.ts +84 -0
- package/src/generated/lexicon-grafana.json +135 -0
- package/src/import/alerting-roundtrip.test.ts +161 -0
- package/src/import/alerting.ts +500 -0
- package/src/import/edits.ts +95 -0
- package/src/import/embedded.test.ts +45 -0
- package/src/import/embedded.ts +47 -0
- package/src/import/generated-types.e2e.test.ts +118 -0
- package/src/import/generator.test.ts +210 -0
- package/src/import/generator.ts +462 -0
- package/src/import/live-export.ts +264 -0
- package/src/import/mappings.ts +264 -0
- package/src/import/model.ts +110 -0
- package/src/import/normalize-alerting.ts +101 -0
- package/src/import/normalize.test.ts +117 -0
- package/src/import/normalize.ts +364 -0
- package/src/import/parser.test.ts +440 -0
- package/src/import/parser.ts +1236 -0
- package/src/import/report.ts +62 -0
- package/src/import/roundtrip.test.ts +468 -0
- package/src/import/testdata/fixtures.ts +129 -0
- package/src/import/v2.test.ts +292 -0
- package/src/import/v2.ts +803 -0
- package/src/import.e2e.test.ts +31 -84
- package/src/import.test.ts +9 -3
- package/src/index.ts +190 -5
- package/src/k8s.test.ts +98 -0
- package/src/k8s.ts +197 -0
- package/src/lint/audit-catalog.ts +60 -3
- package/src/lint/core-lint.test.ts +43 -0
- package/src/lint/post-synth/graf101.ts +3 -3
- package/src/lint/post-synth/graf102.ts +1 -1
- package/src/lint/post-synth/graf107.ts +1 -1
- package/src/lint/post-synth/graf108.ts +17 -0
- package/src/lint/post-synth/graf109.ts +17 -0
- package/src/lint/post-synth/graf111.ts +17 -0
- package/src/lint/post-synth/graf112.ts +17 -0
- package/src/lint/post-synth/graf113.ts +17 -0
- package/src/lint/post-synth/graf114.ts +17 -0
- package/src/lint/post-synth/graf115.ts +17 -0
- package/src/lint/post-synth/grafana-helpers.ts +15 -8
- package/src/lint/post-synth/index.ts +14 -0
- package/src/lint/post-synth/post-synth.test.ts +235 -11
- package/src/lint/rules/literal-secret.ts +60 -6
- package/src/lint/rules/rules.test.ts +27 -0
- package/src/lsp/completions.test.ts +5 -0
- package/src/lsp/hover.test.ts +7 -0
- package/src/lsp/hover.ts +8 -1
- package/src/members.ts +68 -0
- package/src/observe.e2e.test.ts +255 -0
- package/src/op/activities/grafana-apply.e2e.test.ts +175 -0
- package/src/op/activities/grafana-apply.test.ts +229 -0
- package/src/op/activities/grafana-apply.ts +171 -0
- package/src/op/activities/index.ts +8 -0
- package/src/op/builders.test.ts +40 -0
- package/src/op/builders.ts +34 -0
- package/src/ownership.ts +103 -0
- package/src/panel-options.ts +66 -0
- package/src/panels.test.ts +51 -0
- package/src/panels.ts +148 -10
- package/src/pin.ts +57 -6
- package/src/plugin.test.ts +5 -2
- package/src/plugin.ts +66 -0
- package/src/promql-check.test.ts +108 -0
- package/src/promql-check.ts +189 -0
- package/src/query.ts +13 -8
- package/src/schema/barchart.gen.ts +221 -0
- package/src/schema/bargauge.gen.ts +111 -0
- package/src/schema/canvas.gen.ts +176 -0
- package/src/schema/dashboard.gen.ts +206 -22
- package/src/schema/expr.gen.ts +618 -0
- package/src/schema/gauge.gen.ts +2 -1
- package/src/schema/geomap.gen.ts +112 -0
- package/src/schema/heatmap.gen.ts +2 -1
- package/src/schema/histogram.gen.ts +151 -0
- package/src/schema/index.ts +12 -0
- package/src/schema/logs.gen.ts +3 -1
- package/src/schema/loki.gen.ts +2 -1
- package/src/schema/nodegraph.gen.ts +58 -0
- package/src/schema/piechart.gen.ts +146 -0
- package/src/schema/prometheus.gen.ts +2 -1
- package/src/schema/stat.gen.ts +2 -1
- package/src/schema/statetimeline.gen.ts +143 -0
- package/src/schema/statushistory.gen.ts +132 -0
- package/src/schema/table.gen.ts +27 -4
- package/src/schema/tempo.gen.ts +2 -1
- package/src/schema/text.gen.ts +2 -1
- package/src/schema/timeseries.gen.ts +7 -2
- package/src/schema/trend.gen.ts +208 -0
- package/src/schema/xychart.gen.ts +167 -0
- package/src/schema-overlay.test.ts +282 -0
- package/src/schema-validate.ts +141 -50
- package/src/schema.test.ts +4 -2
- package/src/serializer.test.ts +13 -1
- package/src/serializer.ts +11 -3
- package/src/skill-defs.ts +17 -0
- package/src/skills/chant-grafana-alerting.md +49 -0
- package/src/skills/chant-grafana-provisioning.md +3 -3
- package/src/skills/chant-grafana.md +12 -6
- package/src/spec/fetch-cli.ts +3 -3
- package/src/spec/fetch-units-cli.ts +37 -0
- package/src/spec/overlay/canvas.overlay.json +142 -0
- package/src/spec/overlay/dashboard.overlay.json +378 -0
- package/src/spec/overlay/expr.overlay.json +14 -0
- package/src/spec/overlay/geomap.overlay.json +35 -0
- package/src/spec/overlay/logs.overlay.json +14 -0
- package/src/spec/overlay/nodegraph.overlay.json +31 -0
- package/src/spec/overlay/piechart.overlay.json +17 -0
- package/src/spec/overlay/table.overlay.json +84 -0
- package/src/spec/overlay/timeseries.overlay.json +21 -0
- package/src/spec/overlay/trend.overlay.json +17 -0
- package/src/spec/overlay/xychart.overlay.json +16 -0
- package/src/spec/overlay.ts +105 -0
- package/src/spec/schemas/barchart.jsonschema.json +419 -0
- package/src/spec/schemas/bargauge.jsonschema.json +224 -0
- package/src/spec/schemas/canvas.jsonschema.json +356 -0
- package/src/spec/schemas/dashboardv2.jsonschema.json +3387 -0
- package/src/spec/schemas/expr.jsonschema.json +987 -0
- package/src/spec/schemas/geomap.jsonschema.json +220 -0
- package/src/spec/schemas/histogram.jsonschema.json +308 -0
- package/src/spec/schemas/nodegraph.jsonschema.json +77 -0
- package/src/spec/schemas/piechart.jsonschema.json +275 -0
- package/src/spec/schemas/statetimeline.jsonschema.json +324 -0
- package/src/spec/schemas/statushistory.jsonschema.json +292 -0
- package/src/spec/schemas/trend.jsonschema.json +449 -0
- package/src/spec/schemas/xychart.jsonschema.json +428 -0
- package/src/spec/schemas.ts +22 -10
- package/src/spec/units.gen.ts +281 -0
- package/src/spec/units.ts +77 -0
- package/src/transformations.test.ts +65 -0
- package/src/transformations.ts +574 -0
- package/src/util.ts +8 -0
- package/src/validate-alerting.test.ts +255 -0
- package/src/validate-alerting.ts +428 -0
- package/src/validate-output.ts +332 -101
- package/src/validate.ts +14 -0
- package/src/variables.ts +7 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Grafana that remembers writes, in memory, for the applier's tests
|
|
3
|
+
* (#2948). The read-only fake (./fake-grafana.ts) serves a fixed state for
|
|
4
|
+
* observe and export; this one serves the routes the applier writes to and
|
|
5
|
+
* keeps what it was sent, so a second apply sees the first one's result.
|
|
6
|
+
*
|
|
7
|
+
* It behaves as Grafana 12.4.11 and 13.2.2 did when the applier was checked
|
|
8
|
+
* against them (../op/activities/grafana-apply.e2e.test.ts): `POST` to an
|
|
9
|
+
* existing name is a 409, `PUT` needs no `resourceVersion`, a stored
|
|
10
|
+
* dashboard gets the built-in annotation and Grafana's defaults
|
|
11
|
+
* (`storedDashboard`), a folder with anything in it cannot be deleted over
|
|
12
|
+
* `/apis`, and a library element update needs its `version`. With
|
|
13
|
+
* `api: "legacy"` it is a Grafana 11: no `/apis` groups, dashboards over
|
|
14
|
+
* `/api/dashboards/db` and `/api/dashboards/uid`, folders over `/api/folders`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { GrafanaHttp, GrafanaResponse } from "./client";
|
|
18
|
+
import { storedDashboard } from "./fake-grafana";
|
|
19
|
+
|
|
20
|
+
type Json = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
function isObject(v: unknown): v is Json {
|
|
23
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StoredResource {
|
|
27
|
+
spec: Json;
|
|
28
|
+
labels: Record<string, string>;
|
|
29
|
+
annotations: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WritableGrafanaState {
|
|
33
|
+
/** `v1` (13.x), `v1beta1` (12.4), or `legacy` (11.x). */
|
|
34
|
+
api: "v1" | "v1beta1" | "legacy";
|
|
35
|
+
dashboards: Record<string, StoredResource>;
|
|
36
|
+
/** Folders by uid; `spec.title` is the title and the `grafana.app/folder` annotation the parent. */
|
|
37
|
+
folders: Record<string, StoredResource>;
|
|
38
|
+
libraryElements: Record<string, Json>;
|
|
39
|
+
namespace?: string;
|
|
40
|
+
/** Answer every request with this status (a refused token: 401). */
|
|
41
|
+
status?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function emptyGrafana(api: WritableGrafanaState["api"]): WritableGrafanaState {
|
|
45
|
+
return { api, dashboards: {}, folders: {}, libraryElements: {} };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What Grafana fills in on a dashboard it stores, beyond `storedDashboard`. */
|
|
49
|
+
const DASHBOARD_DEFAULTS: Json = { editable: true, fiscalYearStartMonth: 0, graphTooltip: 0, links: [], templating: { list: [] }, timepicker: {}, timezone: "", weekStart: "" };
|
|
50
|
+
|
|
51
|
+
function store(spec: Json): Json {
|
|
52
|
+
return { ...DASHBOARD_DEFAULTS, ...storedDashboard(spec) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ok = (json: unknown, status = 200): GrafanaResponse => ({ status, json });
|
|
56
|
+
const notFound: GrafanaResponse = { status: 404, json: { message: "not found" } };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A {@link GrafanaHttp} over `state`, which it changes as it is written to.
|
|
60
|
+
* Every request is recorded in `calls` as `METHOD path`.
|
|
61
|
+
*/
|
|
62
|
+
export function writableGrafana(state: WritableGrafanaState, calls: string[] = []): GrafanaHttp {
|
|
63
|
+
const ns = state.namespace ?? "default";
|
|
64
|
+
const apis = state.api !== "legacy";
|
|
65
|
+
const dashRoute = new RegExp(`^/apis/dashboard\\.grafana\\.app/${state.api}/namespaces/${ns}/dashboards(?:/([^/]+))?$`);
|
|
66
|
+
const folderRoute = new RegExp(`^/apis/folder\\.grafana\\.app/${state.api}/namespaces/${ns}/folders(?:/([^/]+))?$`);
|
|
67
|
+
|
|
68
|
+
const resource = (kind: string, group: string, uid: string, r: StoredResource): Json => ({
|
|
69
|
+
apiVersion: `${group}/${state.api}`,
|
|
70
|
+
kind,
|
|
71
|
+
metadata: { name: uid, namespace: ns, resourceVersion: "1", labels: { "grafana.app/deprecatedInternalID": "1", ...r.labels }, annotations: r.annotations },
|
|
72
|
+
spec: r.spec,
|
|
73
|
+
...(kind === "Dashboard" ? { status: { conversion: { failed: false, storedVersion: "v0alpha1" } } } : {}),
|
|
74
|
+
});
|
|
75
|
+
const metadataOf = (body: unknown): { name: string; labels: Record<string, string>; annotations: Record<string, string> } => {
|
|
76
|
+
const m = isObject(body) && isObject(body.metadata) ? body.metadata : {};
|
|
77
|
+
const strings = (v: unknown) => (isObject(v) ? (Object.fromEntries(Object.entries(v).filter(([, x]) => typeof x === "string")) as Record<string, string>) : {});
|
|
78
|
+
const labels = strings(m.labels);
|
|
79
|
+
delete labels["grafana.app/deprecatedInternalID"];
|
|
80
|
+
return { name: typeof m.name === "string" ? m.name : "", labels, annotations: strings(m.annotations) };
|
|
81
|
+
};
|
|
82
|
+
const parentOf = (r: StoredResource) => r.annotations["grafana.app/folder"] ?? "";
|
|
83
|
+
const counts = (uid: string) => ({
|
|
84
|
+
dashboards: Object.values(state.dashboards).filter((d) => parentOf(d) === uid).length,
|
|
85
|
+
folders: Object.values(state.folders).filter((f) => parentOf(f) === uid).length,
|
|
86
|
+
librarypanels: Object.values(state.libraryElements).filter((e) => e.folderUid === uid).length,
|
|
87
|
+
alertrules: 0,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return async (method, path, body) => {
|
|
91
|
+
calls.push(`${method} ${path}`);
|
|
92
|
+
if (state.status !== undefined) return { status: state.status, json: { message: "refused" } };
|
|
93
|
+
const [route] = path.split("?");
|
|
94
|
+
|
|
95
|
+
if (route === "/apis/dashboard.grafana.app") {
|
|
96
|
+
if (!apis) return notFound;
|
|
97
|
+
const versions = state.api === "v1" ? ["v2", "v0alpha1", "v1", "v1beta1"] : ["v1beta1", "v0alpha1", "v2beta1"];
|
|
98
|
+
return ok({ kind: "APIGroup", versions: versions.map((version) => ({ version })) });
|
|
99
|
+
}
|
|
100
|
+
if (route === "/apis/folder.grafana.app") {
|
|
101
|
+
if (!apis) return notFound;
|
|
102
|
+
return ok({ kind: "APIGroup", versions: (state.api === "v1" ? ["v1", "v1beta1"] : ["v1beta1"]).map((version) => ({ version })) });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [re, kind, group, table] of [
|
|
106
|
+
[dashRoute, "Dashboard", "dashboard.grafana.app", state.dashboards],
|
|
107
|
+
[folderRoute, "Folder", "folder.grafana.app", state.folders],
|
|
108
|
+
] as const) {
|
|
109
|
+
const m = apis ? re.exec(route) : null;
|
|
110
|
+
if (!m) continue;
|
|
111
|
+
const uid = m[1] ? decodeURIComponent(m[1]) : undefined;
|
|
112
|
+
if (!uid) {
|
|
113
|
+
if (method === "GET") return ok({ items: Object.entries(table).map(([u, r]) => resource(kind, group, u, r)), metadata: {} });
|
|
114
|
+
if (method === "POST") {
|
|
115
|
+
const meta = metadataOf(body);
|
|
116
|
+
if (table[meta.name]) return { status: 409, json: { message: "already exists" } };
|
|
117
|
+
const spec = isObject(body) && isObject(body.spec) ? body.spec : {};
|
|
118
|
+
table[meta.name] = { spec: kind === "Dashboard" ? store(spec) : spec, labels: meta.labels, annotations: meta.annotations };
|
|
119
|
+
return ok(resource(kind, group, meta.name, table[meta.name]), 201);
|
|
120
|
+
}
|
|
121
|
+
return { status: 405, json: {} };
|
|
122
|
+
}
|
|
123
|
+
const r = table[uid];
|
|
124
|
+
if (method === "GET") return r ? ok(resource(kind, group, uid, r)) : notFound;
|
|
125
|
+
if (method === "PUT") {
|
|
126
|
+
const meta = metadataOf(body);
|
|
127
|
+
const spec = isObject(body) && isObject(body.spec) ? body.spec : {};
|
|
128
|
+
table[uid] = { spec: kind === "Dashboard" ? store(spec) : spec, labels: meta.labels, annotations: meta.annotations };
|
|
129
|
+
return ok(resource(kind, group, uid, table[uid]), r ? 200 : 201);
|
|
130
|
+
}
|
|
131
|
+
if (method === "DELETE") {
|
|
132
|
+
if (!r) return notFound;
|
|
133
|
+
if (kind === "Folder" && Object.values(counts(uid)).some((n) => n > 0)) return { status: 400, json: { message: "Folder cannot be deleted: folder is not empty" } };
|
|
134
|
+
delete table[uid];
|
|
135
|
+
return ok({ status: "Success" });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const counted = /^\/api\/folders\/([^/]+)\/counts$/.exec(route);
|
|
140
|
+
if (counted) return state.folders[decodeURIComponent(counted[1])] ? ok(counts(decodeURIComponent(counted[1]))) : notFound;
|
|
141
|
+
|
|
142
|
+
// Legacy dashboards and folders (Grafana 11; 12.4 and 13.2 still serve them).
|
|
143
|
+
const legacyDash = /^\/api\/dashboards\/uid\/([^/]+)$/.exec(route);
|
|
144
|
+
if (legacyDash && method === "GET") {
|
|
145
|
+
const uid = decodeURIComponent(legacyDash[1]);
|
|
146
|
+
const d = state.dashboards[uid];
|
|
147
|
+
if (!d) return notFound;
|
|
148
|
+
return ok({ dashboard: { ...d.spec, uid, id: 7, version: 3 }, meta: { folderUid: parentOf(d), provisioned: false } });
|
|
149
|
+
}
|
|
150
|
+
if (route === "/api/dashboards/db" && method === "POST" && isObject(body) && isObject(body.dashboard)) {
|
|
151
|
+
const { uid, id: _id, version: _v, ...spec } = body.dashboard;
|
|
152
|
+
const folderUid = typeof body.folderUid === "string" ? body.folderUid : "";
|
|
153
|
+
const prior = state.dashboards[String(uid)];
|
|
154
|
+
state.dashboards[String(uid)] = { spec, labels: prior?.labels ?? {}, annotations: folderUid ? { "grafana.app/folder": folderUid } : {} };
|
|
155
|
+
return ok({ status: "success", uid, version: 1 });
|
|
156
|
+
}
|
|
157
|
+
if (route === "/api/search") return ok(Object.keys(state.dashboards).map((uid) => ({ uid, type: "dash-db" })));
|
|
158
|
+
const legacyFolder = /^\/api\/folders\/([^/]+)(\/move)?$/.exec(route);
|
|
159
|
+
if (legacyFolder) {
|
|
160
|
+
const uid = decodeURIComponent(legacyFolder[1]);
|
|
161
|
+
const f = state.folders[uid];
|
|
162
|
+
if (!f) return notFound;
|
|
163
|
+
if (legacyFolder[2] && method === "POST" && isObject(body)) {
|
|
164
|
+
f.annotations = typeof body.parentUid === "string" && body.parentUid ? { "grafana.app/folder": body.parentUid } : {};
|
|
165
|
+
return ok({ uid });
|
|
166
|
+
}
|
|
167
|
+
if (method === "GET") return ok({ uid, title: f.spec.title, version: 1, ...(parentOf(f) ? { parentUid: parentOf(f) } : {}) });
|
|
168
|
+
if (method === "PUT" && isObject(body)) {
|
|
169
|
+
f.spec = { title: body.title };
|
|
170
|
+
return ok({ uid, title: body.title });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (route === "/api/folders" && method === "POST" && isObject(body)) {
|
|
174
|
+
const uid = String(body.uid);
|
|
175
|
+
if (state.folders[uid]) return { status: 409, json: { message: "a folder with that uid already exists" } };
|
|
176
|
+
state.folders[uid] = { spec: { title: body.title }, labels: {}, annotations: typeof body.parentUid === "string" && body.parentUid ? { "grafana.app/folder": body.parentUid } : {} };
|
|
177
|
+
return ok({ uid, title: body.title });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Library elements: the same on every version.
|
|
181
|
+
if (route === "/api/library-elements" && method === "POST" && isObject(body)) {
|
|
182
|
+
const uid = String(body.uid);
|
|
183
|
+
if (state.libraryElements[uid]) return { status: 400, json: { message: "library element with that name or UID already exists" } };
|
|
184
|
+
state.libraryElements[uid] = { uid, name: body.name, kind: body.kind, model: body.model, folderUid: body.folderUid ?? "", version: 1 };
|
|
185
|
+
return ok({ result: state.libraryElements[uid] });
|
|
186
|
+
}
|
|
187
|
+
const lib = /^\/api\/library-elements\/([^/]+)$/.exec(route);
|
|
188
|
+
if (lib) {
|
|
189
|
+
const uid = decodeURIComponent(lib[1]);
|
|
190
|
+
const el = state.libraryElements[uid];
|
|
191
|
+
if (!el) return notFound;
|
|
192
|
+
if (method === "GET") return ok({ result: el });
|
|
193
|
+
if (method === "PATCH" && isObject(body)) {
|
|
194
|
+
if (body.version !== el.version) return { status: 400, json: { message: "bad request data" } };
|
|
195
|
+
state.libraryElements[uid] = { ...el, name: body.name, model: body.model, folderUid: body.folderUid ?? "", version: (el.version as number) + 1 };
|
|
196
|
+
return ok({ result: state.libraryElements[uid] });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return notFound;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Grafana API in memory, for the observe and export tests (#2946).
|
|
3
|
+
*
|
|
4
|
+
* Serves the routes the readers use, from a state the test sets up, in the
|
|
5
|
+
* shapes Grafana 12.4.11 and 13.2.2 return (checked against both with the
|
|
6
|
+
* e2e in ../observe.e2e.test.ts): the API group discovery, dashboard
|
|
7
|
+
* resources under `/apis`, the legacy `/api/dashboards/uid` and
|
|
8
|
+
* `/api/search`, folders, and datasources. `storedDashboard` does to a built
|
|
9
|
+
* dashboard what Grafana does when it stores one.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { GrafanaHttp, GrafanaResponse } from "./client";
|
|
13
|
+
|
|
14
|
+
type Json = Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
export interface FakeDashboard {
|
|
17
|
+
/** The classic JSON, as it would be stored (see {@link storedDashboard}). */
|
|
18
|
+
spec: Json;
|
|
19
|
+
labels?: Record<string, string>;
|
|
20
|
+
annotations?: Record<string, string>;
|
|
21
|
+
/** The version it is stored at: `v0alpha1` for a classic dashboard, `v2beta1` for one saved as v2. */
|
|
22
|
+
storedVersion?: string;
|
|
23
|
+
/**
|
|
24
|
+
* For a dashboard stored as v2, its v2 spec, served at the v2 versions the
|
|
25
|
+
* server has (`spec` is then the classic read's down-conversion). Without
|
|
26
|
+
* it the v2 routes answer 404.
|
|
27
|
+
*/
|
|
28
|
+
v2?: Json;
|
|
29
|
+
folderUid?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface FakeGrafanaState {
|
|
33
|
+
/** `v1` (13.x), `v1beta1` (12.x), or `legacy` for a server with no `/apis/dashboard.grafana.app` (11.x). */
|
|
34
|
+
api: "v1" | "v1beta1" | "legacy";
|
|
35
|
+
dashboards: Record<string, FakeDashboard>;
|
|
36
|
+
datasources?: Record<string, Json>;
|
|
37
|
+
folders?: Record<string, string>;
|
|
38
|
+
/** Answer every request with this status (a refused token: 401). */
|
|
39
|
+
status?: number;
|
|
40
|
+
namespace?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const BUILTIN_ANNOTATION = {
|
|
44
|
+
builtIn: 1,
|
|
45
|
+
datasource: { type: "grafana", uid: "-- Grafana --" },
|
|
46
|
+
enable: true,
|
|
47
|
+
hide: true,
|
|
48
|
+
iconColor: "rgba(0, 211, 255, 1)",
|
|
49
|
+
name: "Annotations & Alerts",
|
|
50
|
+
type: "dashboard",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function isObject(v: unknown): v is Json {
|
|
54
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dropNulls(v: unknown): unknown {
|
|
58
|
+
if (Array.isArray(v)) return v.map(dropNulls);
|
|
59
|
+
if (isObject(v)) return Object.fromEntries(Object.entries(v).filter(([, x]) => x !== null).map(([k, x]) => [k, dropNulls(x)]));
|
|
60
|
+
return v;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function storePanel(p: Json): Json {
|
|
64
|
+
const out: Json = { ...p };
|
|
65
|
+
if (isObject(out.options) && Object.keys(out.options).length === 0) delete out.options;
|
|
66
|
+
const fc = out.fieldConfig;
|
|
67
|
+
if (isObject(fc) && isObject(fc.defaults) && Object.keys(fc.defaults).length === 0 && Array.isArray(fc.overrides) && fc.overrides.length === 0) delete out.fieldConfig;
|
|
68
|
+
if (Array.isArray(out.panels)) out.panels = out.panels.map((c) => (isObject(c) ? storePanel(c) : c));
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A built dashboard as Grafana hands it back through `/apis` (measured on
|
|
74
|
+
* 12.4.11 and 13.2.2): the built-in annotation added, empty `options` and
|
|
75
|
+
* `fieldConfig` dropped, `null` values dropped, and the uid moved to the
|
|
76
|
+
* resource's name. The legacy route (`/api/dashboards/uid`) serves the
|
|
77
|
+
* built JSON plus `id` and `version`; the fake serves this model on both,
|
|
78
|
+
* which is the harder case for the legacy path.
|
|
79
|
+
*/
|
|
80
|
+
export function storedDashboard(built: Json): Json {
|
|
81
|
+
const out = dropNulls(built) as Json;
|
|
82
|
+
const annotations = isObject(out.annotations) && Array.isArray(out.annotations.list) ? out.annotations.list : [];
|
|
83
|
+
out.annotations = { list: [BUILTIN_ANNOTATION, ...annotations] };
|
|
84
|
+
if (Array.isArray(out.panels)) out.panels = out.panels.map((p) => (isObject(p) ? storePanel(p) : p));
|
|
85
|
+
delete out.uid;
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const ok = (json: unknown): GrafanaResponse => ({ status: 200, json });
|
|
90
|
+
const notFound: GrafanaResponse = { status: 404, json: { message: "not found" } };
|
|
91
|
+
|
|
92
|
+
/** A {@link GrafanaHttp} over `state`, recording every request path in `calls`. */
|
|
93
|
+
export function fakeGrafana(state: FakeGrafanaState, calls: string[] = []): GrafanaHttp {
|
|
94
|
+
const ns = state.namespace ?? "default";
|
|
95
|
+
const resource = (uid: string, d: FakeDashboard): Json => ({
|
|
96
|
+
apiVersion: `dashboard.grafana.app/${state.api}`,
|
|
97
|
+
kind: "Dashboard",
|
|
98
|
+
metadata: {
|
|
99
|
+
name: uid,
|
|
100
|
+
namespace: ns,
|
|
101
|
+
resourceVersion: "1",
|
|
102
|
+
generation: 1,
|
|
103
|
+
labels: { "grafana.app/deprecatedInternalID": "1", ...(d.labels ?? {}) },
|
|
104
|
+
annotations: { ...(d.folderUid ? { "grafana.app/folder": d.folderUid } : {}), ...(d.annotations ?? {}) },
|
|
105
|
+
},
|
|
106
|
+
spec: d.spec,
|
|
107
|
+
status: { conversion: { failed: false, storedVersion: d.storedVersion ?? "v0alpha1" } },
|
|
108
|
+
});
|
|
109
|
+
return async (method, path) => {
|
|
110
|
+
calls.push(`${method} ${path}`);
|
|
111
|
+
if (state.status !== undefined) return { status: state.status, json: { message: "refused" } };
|
|
112
|
+
const [route, query] = path.split("?");
|
|
113
|
+
if (route === "/apis/dashboard.grafana.app") {
|
|
114
|
+
if (state.api === "legacy") return notFound;
|
|
115
|
+
const versions = state.api === "v1" ? ["v2", "v2beta1", "v2alpha1", "v0alpha1", "v1", "v1beta1"] : ["v1beta1", "v0alpha1", "v2beta1", "v2alpha1"];
|
|
116
|
+
return ok({ kind: "APIGroup", name: "dashboard.grafana.app", versions: versions.map((version) => ({ groupVersion: `dashboard.grafana.app/${version}`, version })) });
|
|
117
|
+
}
|
|
118
|
+
const list = new RegExp(`^/apis/dashboard\\.grafana\\.app/${state.api}/namespaces/${ns}/dashboards$`).exec(route);
|
|
119
|
+
if (list && state.api !== "legacy") return ok({ items: Object.entries(state.dashboards).map(([uid, d]) => resource(uid, d)), metadata: {} });
|
|
120
|
+
const v2 = new RegExp(`^/apis/dashboard\\.grafana\\.app/(v2|v2beta1)/namespaces/${ns}/dashboards/([^/]+)$`).exec(route);
|
|
121
|
+
if (v2 && state.api !== "legacy" && (state.api === "v1" || v2[1] === "v2beta1")) {
|
|
122
|
+
const uid = decodeURIComponent(v2[2]);
|
|
123
|
+
const d = state.dashboards[uid];
|
|
124
|
+
if (!d?.v2) return notFound;
|
|
125
|
+
const r = resource(uid, d);
|
|
126
|
+
return ok({ ...r, apiVersion: `dashboard.grafana.app/${v2[1]}`, spec: d.v2, status: {} });
|
|
127
|
+
}
|
|
128
|
+
const one = new RegExp(`^/apis/dashboard\\.grafana\\.app/${state.api}/namespaces/${ns}/dashboards/([^/]+)$`).exec(route);
|
|
129
|
+
if (one && state.api !== "legacy") {
|
|
130
|
+
const d = state.dashboards[decodeURIComponent(one[1])];
|
|
131
|
+
return d ? ok(resource(decodeURIComponent(one[1]), d)) : notFound;
|
|
132
|
+
}
|
|
133
|
+
const legacy = /^\/api\/dashboards\/uid\/([^/]+)$/.exec(route);
|
|
134
|
+
if (legacy) {
|
|
135
|
+
const uid = decodeURIComponent(legacy[1]);
|
|
136
|
+
const d = state.dashboards[uid];
|
|
137
|
+
if (!d) return notFound;
|
|
138
|
+
const folderTitle = d.folderUid ? state.folders?.[d.folderUid] : undefined;
|
|
139
|
+
return ok({
|
|
140
|
+
dashboard: { ...d.spec, uid, id: 7, version: 3 },
|
|
141
|
+
meta: { provisioned: true, folderUid: d.folderUid ?? "", folderTitle: folderTitle ?? "Dashboards", version: 3 },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
if (route === "/api/search") {
|
|
145
|
+
const page = Number(new URLSearchParams(query ?? "").get("page") ?? "1");
|
|
146
|
+
return ok(page === 1 ? Object.keys(state.dashboards).map((uid) => ({ uid, type: "dash-db" })) : []);
|
|
147
|
+
}
|
|
148
|
+
const folder = /^\/api\/folders\/([^/]+)$/.exec(route);
|
|
149
|
+
if (folder) {
|
|
150
|
+
const title = state.folders?.[decodeURIComponent(folder[1])];
|
|
151
|
+
return title ? ok({ uid: folder[1], title }) : notFound;
|
|
152
|
+
}
|
|
153
|
+
if (route === "/api/datasources") return ok(Object.values(state.datasources ?? {}).map(({ secureJsonFields: _s, ...rest }) => rest));
|
|
154
|
+
const ds = /^\/api\/datasources\/uid\/([^/]+)$/.exec(route);
|
|
155
|
+
if (ds) {
|
|
156
|
+
const d = state.datasources?.[decodeURIComponent(ds[1])];
|
|
157
|
+
return d ? ok(d) : notFound;
|
|
158
|
+
}
|
|
159
|
+
return notFound;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A datasource as `GET /api/datasources/uid/<uid>` returns one provisioned from a file. */
|
|
164
|
+
export function liveDatasource(ds: { name: string; type: string; uid: string; url?: string; isDefault?: boolean; jsonData?: Json; secure?: string[]; readOnly?: boolean }): Json {
|
|
165
|
+
return {
|
|
166
|
+
id: 2,
|
|
167
|
+
uid: ds.uid,
|
|
168
|
+
orgId: 1,
|
|
169
|
+
name: ds.name,
|
|
170
|
+
type: ds.type,
|
|
171
|
+
typeLogoUrl: `public/plugins/${ds.type}/img/logo.svg`,
|
|
172
|
+
access: "proxy",
|
|
173
|
+
url: ds.url ?? "",
|
|
174
|
+
user: "",
|
|
175
|
+
database: "",
|
|
176
|
+
basicAuth: false,
|
|
177
|
+
basicAuthUser: "",
|
|
178
|
+
withCredentials: false,
|
|
179
|
+
isDefault: ds.isDefault ?? false,
|
|
180
|
+
jsonData: ds.jsonData ?? {},
|
|
181
|
+
secureJsonFields: Object.fromEntries((ds.secure ?? []).map((k) => [k, true])),
|
|
182
|
+
version: 1,
|
|
183
|
+
readOnly: ds.readOnly ?? true,
|
|
184
|
+
apiVersion: "",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folders, for the API applier (#2948). Every folder decision the applier
|
|
3
|
+
* makes is in this module: which folders a build needs, the uid each gets,
|
|
4
|
+
* how one is read, written, listed and deleted, and on which API.
|
|
5
|
+
*
|
|
6
|
+
* ## What the lexicon models today
|
|
7
|
+
*
|
|
8
|
+
* A dashboard names its folder by title (`Dashboard.folder`), and a title
|
|
9
|
+
* holds no nesting (the build flattens `/` in it). So the applier derives
|
|
10
|
+
* one root-level folder per distinct title, with a stable uid made from the
|
|
11
|
+
* title ({@link folderUidFor}), and a second apply finds it again by that
|
|
12
|
+
* uid. {@link FolderPlan} already carries `parentUid`, and the writers
|
|
13
|
+
* already send it, so #2953 (folder uid and nesting on the declaration) only
|
|
14
|
+
* has to change {@link foldersForDashboards}.
|
|
15
|
+
*
|
|
16
|
+
* ## Which API
|
|
17
|
+
*
|
|
18
|
+
* Grafana 12.4 serves `folder.grafana.app/v1beta1` and 13.x also serves
|
|
19
|
+
* `v1`, both keeping `metadata.labels` as written (checked against 12.4.11
|
|
20
|
+
* and 13.2.2), so a folder carries chant's ownership labels the same way a
|
|
21
|
+
* dashboard does. The parent is the `grafana.app/folder` annotation, as on a
|
|
22
|
+
* dashboard. A server without the group (Grafana 11) is written over
|
|
23
|
+
* `/api/folders`, which has `parentUid` but no labels: a folder written
|
|
24
|
+
* there cannot be told apart from anyone else's, so it is never pruned.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { GrafanaApiError, type GrafanaClient } from "./client";
|
|
28
|
+
import { slugUid } from "../util";
|
|
29
|
+
|
|
30
|
+
type Json = Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
function isObject(v: unknown): v is Json {
|
|
33
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stringMap(v: unknown): Record<string, string> {
|
|
37
|
+
if (!isObject(v)) return {};
|
|
38
|
+
return Object.fromEntries(Object.entries(v).filter(([, x]) => typeof x === "string")) as Record<string, string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const FOLDER_GROUP = "folder.grafana.app";
|
|
42
|
+
|
|
43
|
+
/** Folder API versions chant writes, newest first. */
|
|
44
|
+
export const FOLDER_VERSIONS: readonly string[] = ["v1", "v1beta1"];
|
|
45
|
+
|
|
46
|
+
/** The annotation that names a folder's (or a dashboard's) parent folder on `/apis`. */
|
|
47
|
+
export const FOLDER_ANNOTATION = "grafana.app/folder";
|
|
48
|
+
|
|
49
|
+
/** How this server serves folders. */
|
|
50
|
+
export type FolderApi = { readonly kind: "apis"; readonly version: string } | { readonly kind: "legacy" };
|
|
51
|
+
|
|
52
|
+
/** One folder a build needs. */
|
|
53
|
+
export interface FolderPlan {
|
|
54
|
+
readonly uid: string;
|
|
55
|
+
readonly title: string;
|
|
56
|
+
/** The parent folder's uid; absent for a root-level folder. */
|
|
57
|
+
readonly parentUid?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A folder as Grafana holds it. */
|
|
61
|
+
export interface LiveFolder {
|
|
62
|
+
readonly uid: string;
|
|
63
|
+
readonly title: string;
|
|
64
|
+
readonly parentUid?: string;
|
|
65
|
+
/** `metadata.labels`; empty on the legacy API. */
|
|
66
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
67
|
+
readonly via: "apis" | "legacy";
|
|
68
|
+
/** The request path it lives at. */
|
|
69
|
+
readonly address: string;
|
|
70
|
+
/** The legacy folder's `version`, which its update needs. */
|
|
71
|
+
readonly version?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The uid chant gives the folder a dashboard names by title. Stable, so a
|
|
76
|
+
* second apply finds the folder the first one made; Grafana's own limit is
|
|
77
|
+
* 40 characters, which `slugUid` keeps to.
|
|
78
|
+
*/
|
|
79
|
+
export function folderUidFor(title: string): string {
|
|
80
|
+
return slugUid(title);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The folders a set of dashboards needs, one per distinct title, sorted by
|
|
85
|
+
* uid. The one place a dashboard's folder becomes a folder plan.
|
|
86
|
+
*/
|
|
87
|
+
export function foldersForDashboards(dashboards: Iterable<{ folder?: string }>): FolderPlan[] {
|
|
88
|
+
const byUid = new Map<string, FolderPlan>();
|
|
89
|
+
for (const d of dashboards) {
|
|
90
|
+
if (!d.folder) continue;
|
|
91
|
+
const uid = folderUidFor(d.folder);
|
|
92
|
+
const seen = byUid.get(uid);
|
|
93
|
+
if (seen && seen.title !== d.folder) {
|
|
94
|
+
throw new Error(`grafana apply: the folders "${seen.title}" and "${d.folder}" would both get the uid "${uid}"; rename one`);
|
|
95
|
+
}
|
|
96
|
+
byUid.set(uid, { uid, title: d.folder });
|
|
97
|
+
}
|
|
98
|
+
return [...byUid.values()].sort((a, b) => a.uid.localeCompare(b.uid));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The folder API the server behind `client` serves, discovered once per client. */
|
|
102
|
+
export function folderApi(client: GrafanaClient): Promise<FolderApi> {
|
|
103
|
+
return client.once("folder-api", async () => {
|
|
104
|
+
const group = await client.get<{ versions?: Array<{ version?: string }> }>(`/apis/${FOLDER_GROUP}`);
|
|
105
|
+
const served = new Set((group?.versions ?? []).map((v) => v.version).filter((v): v is string => typeof v === "string"));
|
|
106
|
+
const version = FOLDER_VERSIONS.find((v) => served.has(v));
|
|
107
|
+
return version ? { kind: "apis", version } : { kind: "legacy" };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collection(api: Extract<FolderApi, { kind: "apis" }>, namespace: string): string {
|
|
112
|
+
return `/apis/${FOLDER_GROUP}/${api.version}/namespaces/${encodeURIComponent(namespace)}/folders`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The request path for one folder on this API. */
|
|
116
|
+
export function folderPath(api: FolderApi, namespace: string, uid: string): string {
|
|
117
|
+
return api.kind === "apis" ? `${collection(api, namespace)}/${encodeURIComponent(uid)}` : `/api/folders/${encodeURIComponent(uid)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function fromResource(resource: Json, address: string): LiveFolder {
|
|
121
|
+
const metadata = isObject(resource.metadata) ? resource.metadata : {};
|
|
122
|
+
const spec = isObject(resource.spec) ? resource.spec : {};
|
|
123
|
+
const parentUid = stringMap(metadata.annotations)[FOLDER_ANNOTATION];
|
|
124
|
+
return {
|
|
125
|
+
uid: typeof metadata.name === "string" ? metadata.name : "",
|
|
126
|
+
title: typeof spec.title === "string" ? spec.title : "",
|
|
127
|
+
...(parentUid ? { parentUid } : {}),
|
|
128
|
+
labels: stringMap(metadata.labels),
|
|
129
|
+
via: "apis",
|
|
130
|
+
address,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fromLegacy(body: Json, address: string): LiveFolder {
|
|
135
|
+
return {
|
|
136
|
+
uid: typeof body.uid === "string" ? body.uid : "",
|
|
137
|
+
title: typeof body.title === "string" ? body.title : "",
|
|
138
|
+
...(typeof body.parentUid === "string" && body.parentUid !== "" ? { parentUid: body.parentUid } : {}),
|
|
139
|
+
labels: {},
|
|
140
|
+
via: "legacy",
|
|
141
|
+
address,
|
|
142
|
+
...(typeof body.version === "number" ? { version: body.version } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Read one folder; undefined when there is none with that uid. */
|
|
147
|
+
export async function readFolder(client: GrafanaClient, uid: string): Promise<LiveFolder | undefined> {
|
|
148
|
+
const api = await folderApi(client);
|
|
149
|
+
const address = folderPath(api, client.namespace, uid);
|
|
150
|
+
const body = await client.get<Json>(address);
|
|
151
|
+
if (body === undefined) return undefined;
|
|
152
|
+
return api.kind === "apis" ? fromResource(body, address) : fromLegacy(body, address);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Every folder in the organisation over `/apis`, paged. The legacy API has no labels to list by, so it is not listed. */
|
|
156
|
+
export async function listFolders(client: GrafanaClient): Promise<LiveFolder[]> {
|
|
157
|
+
const api = await folderApi(client);
|
|
158
|
+
if (api.kind !== "apis") return [];
|
|
159
|
+
const base = collection(api, client.namespace);
|
|
160
|
+
const out: LiveFolder[] = [];
|
|
161
|
+
let next: string | undefined;
|
|
162
|
+
do {
|
|
163
|
+
const page = await client.get<{ items?: unknown[]; metadata?: { continue?: string } }>(
|
|
164
|
+
`${base}?limit=500${next ? `&continue=${encodeURIComponent(next)}` : ""}`,
|
|
165
|
+
);
|
|
166
|
+
for (const item of page?.items ?? []) {
|
|
167
|
+
if (!isObject(item)) continue;
|
|
168
|
+
const name = isObject(item.metadata) && typeof item.metadata.name === "string" ? item.metadata.name : "";
|
|
169
|
+
if (name) out.push(fromResource(item, `${base}/${encodeURIComponent(name)}`));
|
|
170
|
+
}
|
|
171
|
+
next = page?.metadata?.continue || undefined;
|
|
172
|
+
} while (next);
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Send one write; throws {@link GrafanaApiError} on anything but a 2xx. */
|
|
177
|
+
export async function send(client: GrafanaClient, method: string, path: string, body?: unknown): Promise<unknown> {
|
|
178
|
+
const res = await client.http(method, path, body);
|
|
179
|
+
if (res.status < 200 || res.status >= 300) throw new GrafanaApiError(res.status, method, path, res.json);
|
|
180
|
+
return res.json;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function sameLabels(live: Readonly<Record<string, string>>, want: Readonly<Record<string, string>>): boolean {
|
|
184
|
+
return Object.entries(want).every(([k, v]) => live[k] === v);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create or update one folder so it matches `plan`, with chant's `labels`
|
|
189
|
+
* on it where the API keeps labels. Unchanged when title, parent and labels
|
|
190
|
+
* already match; nothing is written then.
|
|
191
|
+
*/
|
|
192
|
+
export async function ensureFolder(
|
|
193
|
+
client: GrafanaClient,
|
|
194
|
+
plan: FolderPlan,
|
|
195
|
+
labels: Readonly<Record<string, string>>,
|
|
196
|
+
): Promise<{ action: "created" | "updated" | "unchanged"; address: string; via: "apis" | "legacy" }> {
|
|
197
|
+
const api = await folderApi(client);
|
|
198
|
+
const address = folderPath(api, client.namespace, plan.uid);
|
|
199
|
+
const live = await readFolder(client, plan.uid);
|
|
200
|
+
const sameParent = (live?.parentUid ?? "") === (plan.parentUid ?? "");
|
|
201
|
+
if (api.kind === "apis") {
|
|
202
|
+
if (live && live.title === plan.title && sameParent && sameLabels(live.labels, labels)) return { action: "unchanged", address, via: "apis" };
|
|
203
|
+
// A folder's labels are merged, so a label another tool put there stays.
|
|
204
|
+
const metadata = {
|
|
205
|
+
name: plan.uid,
|
|
206
|
+
labels: { ...(live?.labels ?? {}), ...labels },
|
|
207
|
+
...(plan.parentUid ? { annotations: { [FOLDER_ANNOTATION]: plan.parentUid } } : {}),
|
|
208
|
+
};
|
|
209
|
+
const resource = { apiVersion: `${FOLDER_GROUP}/${api.version}`, kind: "Folder", metadata, spec: { title: plan.title } };
|
|
210
|
+
if (live) {
|
|
211
|
+
await send(client, "PUT", address, resource);
|
|
212
|
+
return { action: "updated", address, via: "apis" };
|
|
213
|
+
}
|
|
214
|
+
await send(client, "POST", collection(api, client.namespace), resource);
|
|
215
|
+
return { action: "created", address, via: "apis" };
|
|
216
|
+
}
|
|
217
|
+
if (live) {
|
|
218
|
+
if (live.title === plan.title && sameParent) return { action: "unchanged", address, via: "legacy" };
|
|
219
|
+
if (live.title !== plan.title) await send(client, "PUT", address, { title: plan.title, ...(live.version !== undefined ? { version: live.version } : {}), overwrite: true });
|
|
220
|
+
if (!sameParent) await send(client, "POST", `${address}/move`, { parentUid: plan.parentUid ?? "" });
|
|
221
|
+
return { action: "updated", address, via: "legacy" };
|
|
222
|
+
}
|
|
223
|
+
await send(client, "POST", "/api/folders", { uid: plan.uid, title: plan.title, ...(plan.parentUid ? { parentUid: plan.parentUid } : {}) });
|
|
224
|
+
return { action: "created", address, via: "legacy" };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* How many things a folder holds (dashboards, folders, library panels, alert
|
|
229
|
+
* rules), from `GET /api/folders/<uid>/counts`. Grafana 12.4 and 13.2 name
|
|
230
|
+
* the keys differently, so every numeric value is summed.
|
|
231
|
+
*/
|
|
232
|
+
export async function folderContentCount(client: GrafanaClient, uid: string): Promise<number> {
|
|
233
|
+
const counts = await client.get<Json>(`/api/folders/${encodeURIComponent(uid)}/counts`);
|
|
234
|
+
if (!counts) return 0;
|
|
235
|
+
return Object.values(counts).reduce<number>((n, v) => n + (typeof v === "number" ? v : 0), 0);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Delete one folder over `/apis`. Only ever called for a folder that holds
|
|
240
|
+
* nothing: Grafana 12.4 and 13.2 refuse to delete a folder with anything in
|
|
241
|
+
* it over `/apis`, and the applier checks first, so a foreign dashboard in a
|
|
242
|
+
* folder chant made is never removed with it. False when it was already gone.
|
|
243
|
+
*/
|
|
244
|
+
export async function deleteFolder(client: GrafanaClient, folder: LiveFolder): Promise<boolean> {
|
|
245
|
+
if (folder.via !== "apis") throw new Error(`grafana apply: folder "${folder.uid}" was read over /api/folders, whose delete removes what the folder holds; chant does not delete it`);
|
|
246
|
+
const res = await client.http("DELETE", folder.address);
|
|
247
|
+
if (res.status === 404) return false;
|
|
248
|
+
if (res.status < 200 || res.status >= 300) throw new GrafanaApiError(res.status, "DELETE", folder.address, res.json);
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Order folders so a child comes before its parent: the order to delete
|
|
254
|
+
* them in. Parents outside the set count as depth 0.
|
|
255
|
+
*/
|
|
256
|
+
export function childrenFirst(folders: readonly LiveFolder[]): LiveFolder[] {
|
|
257
|
+
const byUid = new Map(folders.map((f) => [f.uid, f]));
|
|
258
|
+
const depth = (f: LiveFolder, seen = new Set<string>()): number => {
|
|
259
|
+
if (!f.parentUid || seen.has(f.uid)) return 0;
|
|
260
|
+
seen.add(f.uid);
|
|
261
|
+
const parent = byUid.get(f.parentUid);
|
|
262
|
+
return parent ? 1 + depth(parent, seen) : 1;
|
|
263
|
+
};
|
|
264
|
+
return [...folders].sort((a, b) => depth(b) - depth(a) || a.uid.localeCompare(b.uid));
|
|
265
|
+
}
|