@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,1236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard JSON (and Grafana's provisioning files) -> `TemplateIR`, for
|
|
3
|
+
* `chant import`.
|
|
4
|
+
*
|
|
5
|
+
* A dashboard's parts refer to each other: panels name datasources and
|
|
6
|
+
* variables, rows hold panels, the dashboard lists them all. So the IR
|
|
7
|
+
* carries one resource per dashboard, of type `Grafana::Dashboard`, whose
|
|
8
|
+
* properties are a `Plan` (./model.ts): the declarations the dashboard
|
|
9
|
+
* becomes and the references between them. The generator lays the plan out
|
|
10
|
+
* in modules, and says so with `ownsLayout` (#2964), so core writes its
|
|
11
|
+
* files as they are.
|
|
12
|
+
*
|
|
13
|
+
* What each JSON key becomes is decided by the tables in ./mappings.ts.
|
|
14
|
+
* Whatever cannot be carried is named in `warnings` and recorded as an
|
|
15
|
+
* edit (./edits.ts) in the resource's metadata; a key left out at the value
|
|
16
|
+
* Grafana assumes anyway (./normalize.ts) is an edit with no warning.
|
|
17
|
+
*
|
|
18
|
+
* Accepted input:
|
|
19
|
+
*
|
|
20
|
+
* - classic dashboard JSON, as the UI exports it, with or without "Export
|
|
21
|
+
* for sharing externally" (`__inputs`, `__requires`, `__elements`);
|
|
22
|
+
* - the same wrapped in a `dashboard.grafana.app` v0/v1 resource (`spec`),
|
|
23
|
+
* or in the `{ dashboard, meta }` of `GET /api/dashboards/uid/<uid>`;
|
|
24
|
+
* - a v2 dashboard (`dashboard.grafana.app/v2` or `v2beta1`, or a bare v2
|
|
25
|
+
* spec), read into classic JSON by ./v2.ts, with a warning for each thing
|
|
26
|
+
* v2 says that the classic model cannot (#2947);
|
|
27
|
+
* - a datasource, dashboard or alerting provisioning file (./alerting.ts).
|
|
28
|
+
*
|
|
29
|
+
* A v0/v1 resource that Grafana stores as v2 (`status.conversion` says so)
|
|
30
|
+
* is refused unless the caller opts in (`acceptLossyV1`): it is a lossy
|
|
31
|
+
* down-conversion that looks like any other v1 dashboard. A dashboard saved
|
|
32
|
+
* before Grafana 5.0 (panels inside a top-level `rows` list) is recognised
|
|
33
|
+
* and reported, and nothing is imported from it.
|
|
34
|
+
*
|
|
35
|
+
* Deliberate changes, each with a warning:
|
|
36
|
+
*
|
|
37
|
+
* - An `__inputs` datasource (`${DS_PROMETHEUS}`) becomes a
|
|
38
|
+
* `DatasourceVariable` of the same name, so every `${DS_PROMETHEUS}` in
|
|
39
|
+
* the dashboard still resolves, now to whatever the variable selects.
|
|
40
|
+
* - An `__inputs` constant is replaced by its value, as Grafana's import
|
|
41
|
+
* dialog would.
|
|
42
|
+
* - The stored copy's `id`, `version` and `iteration` are Grafana's
|
|
43
|
+
* bookkeeping and are dropped without a warning.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import * as jsYaml from "js-yaml";
|
|
47
|
+
import type { TemplateIR, TemplateParser, ResourceIR } from "@intentius/chant/import/parser";
|
|
48
|
+
import { DASHBOARD_SCHEMA_VERSION } from "../schema/dashboard.gen";
|
|
49
|
+
import { BUILTIN_DATASOURCE_UIDS } from "../datasource";
|
|
50
|
+
import {
|
|
51
|
+
looksLikeDashboard,
|
|
52
|
+
looksLikeAlertingProvisioning,
|
|
53
|
+
looksLikeDashboardApiResponse,
|
|
54
|
+
looksLikeDashboardProvisioning,
|
|
55
|
+
looksLikeDashboardResource,
|
|
56
|
+
looksLikeDatasourceProvisioning,
|
|
57
|
+
looksLikeLegacyRowsDashboard,
|
|
58
|
+
looksLikeV2Dashboard,
|
|
59
|
+
} from "../detect";
|
|
60
|
+
import { slugUid } from "../util";
|
|
61
|
+
import { untypedTransformationReason } from "../transformations";
|
|
62
|
+
import { pointer, type ImportEdit } from "./edits";
|
|
63
|
+
import { NO_PROP, Report, list } from "./report";
|
|
64
|
+
import { planAlertingProvisioning } from "./alerting";
|
|
65
|
+
import { lossyV1Read, readV2Dashboard } from "./v2";
|
|
66
|
+
import {
|
|
67
|
+
COMMON_VARIABLE_KEYS,
|
|
68
|
+
DASHBOARD_FIELDS,
|
|
69
|
+
DEFAULT_DATASOURCE_TYPE,
|
|
70
|
+
GRAPH_TOOLTIP,
|
|
71
|
+
PANEL_FIELDS,
|
|
72
|
+
ROW_FIELDS,
|
|
73
|
+
VARIABLE_HIDE,
|
|
74
|
+
VARIABLE_MAPPINGS,
|
|
75
|
+
builtinPanelFor,
|
|
76
|
+
builtinQueryFor,
|
|
77
|
+
type VariableContext,
|
|
78
|
+
} from "./mappings";
|
|
79
|
+
import { callValue, declRef, type CustomClass, type Declaration, type DeclRef, type ModuleSpec, type Plan } from "./model";
|
|
80
|
+
import {
|
|
81
|
+
BOOKKEEPING_KEYS,
|
|
82
|
+
DASHBOARD_DEFAULTS,
|
|
83
|
+
MIXED_UID,
|
|
84
|
+
PANEL_DEFAULTS,
|
|
85
|
+
ROW_DEFAULTS,
|
|
86
|
+
canonicalVariableUid,
|
|
87
|
+
deepEqual,
|
|
88
|
+
isBuiltinAnnotation,
|
|
89
|
+
isDefault,
|
|
90
|
+
isObject,
|
|
91
|
+
variableDefaults,
|
|
92
|
+
} from "./normalize";
|
|
93
|
+
|
|
94
|
+
type Json = Record<string, unknown>;
|
|
95
|
+
|
|
96
|
+
/** The IR resource type for one dashboard. */
|
|
97
|
+
export const DASHBOARD_RESOURCE_TYPE = "Grafana::Dashboard";
|
|
98
|
+
/** The IR resource type for a datasource or dashboard provisioning file. */
|
|
99
|
+
export const PROVISIONING_RESOURCE_TYPE = "Grafana::Provisioning";
|
|
100
|
+
|
|
101
|
+
/** `properties` of a `Grafana::Dashboard` or `Grafana::Provisioning` resource. */
|
|
102
|
+
export interface PlanResourceProperties {
|
|
103
|
+
plan: Plan;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `metadata` of a `Grafana::Dashboard` resource. */
|
|
107
|
+
export interface DashboardResourceMetadata {
|
|
108
|
+
/** The dashboard JSON the plan was made from (unwrapped from a resource or API response). */
|
|
109
|
+
source: Json;
|
|
110
|
+
/** What the importer did to it: apply these to `source` to get what the rebuilt dashboard should equal. */
|
|
111
|
+
edits: ImportEdit[];
|
|
112
|
+
/** For a v2 dashboard, the v2 JSON as given; `source` is then its classic form (see ./v2.ts). */
|
|
113
|
+
v2?: Json;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Options for reading dashboard JSON. */
|
|
117
|
+
export interface GrafanaImportOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Import a `dashboard.grafana.app` v0/v1 resource even when Grafana stores
|
|
120
|
+
* the dashboard as v2. That read has already lost what v1 cannot hold (tabs,
|
|
121
|
+
* auto grids, conditional rendering); prefer importing the v2 read. Off by
|
|
122
|
+
* default: such a resource is refused, with a warning saying why.
|
|
123
|
+
*/
|
|
124
|
+
acceptLossyV1?: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `metadata` of an alerting provisioning file's resource. */
|
|
128
|
+
export interface AlertingResourceMetadata {
|
|
129
|
+
/** The file as parsed. */
|
|
130
|
+
source: Json;
|
|
131
|
+
/** What the importer did to it: apply these to `source`, then `normalizeAlerting` both sides, to compare with the rebuilt file. */
|
|
132
|
+
edits: ImportEdit[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The panel plugin id format `definePanel` accepts. */
|
|
136
|
+
const PLUGIN_ID = /^[a-z0-9][a-z0-9-_]*$/;
|
|
137
|
+
|
|
138
|
+
const VARIABLE_UID = /^(?:\$\{([A-Za-z_][A-Za-z0-9_]*)(?::[^}]*)?\}|\$([A-Za-z_][A-Za-z0-9_]*)|\[\[([A-Za-z_][A-Za-z0-9_]*)\]\])$/;
|
|
139
|
+
|
|
140
|
+
/** Class names the package exports, which a class the import declares must not take. */
|
|
141
|
+
const PACKAGE_CLASS_NAMES = new Set([
|
|
142
|
+
"Dashboard",
|
|
143
|
+
"DashboardProvider",
|
|
144
|
+
"Datasource",
|
|
145
|
+
"Row",
|
|
146
|
+
"QueryVariable",
|
|
147
|
+
"CustomVariable",
|
|
148
|
+
"IntervalVariable",
|
|
149
|
+
"DatasourceVariable",
|
|
150
|
+
"ConstantVariable",
|
|
151
|
+
"TextboxVariable",
|
|
152
|
+
"PromQuery",
|
|
153
|
+
"TempoQuery",
|
|
154
|
+
"LokiQuery",
|
|
155
|
+
"TimeSeriesPanel",
|
|
156
|
+
"StatPanel",
|
|
157
|
+
"GaugePanel",
|
|
158
|
+
"TablePanel",
|
|
159
|
+
"LogsPanel",
|
|
160
|
+
"TracesPanel",
|
|
161
|
+
"HeatmapPanel",
|
|
162
|
+
"TextPanel",
|
|
163
|
+
"BarChartPanel",
|
|
164
|
+
"BarGaugePanel",
|
|
165
|
+
"PieChartPanel",
|
|
166
|
+
"StateTimelinePanel",
|
|
167
|
+
"StatusHistoryPanel",
|
|
168
|
+
"HistogramPanel",
|
|
169
|
+
"NodeGraphPanel",
|
|
170
|
+
"XYChartPanel",
|
|
171
|
+
"TrendPanel",
|
|
172
|
+
"CanvasPanel",
|
|
173
|
+
"GeomapPanel",
|
|
174
|
+
"FlameGraphPanel",
|
|
175
|
+
"AlertListPanel",
|
|
176
|
+
"AlertRuleGroup",
|
|
177
|
+
"AlertRule",
|
|
178
|
+
"AlertQuery",
|
|
179
|
+
"ReduceExpression",
|
|
180
|
+
"MathExpression",
|
|
181
|
+
"ThresholdExpression",
|
|
182
|
+
"ResampleExpression",
|
|
183
|
+
"ClassicConditionsExpression",
|
|
184
|
+
"SqlExpression",
|
|
185
|
+
"ContactPoint",
|
|
186
|
+
"NotificationPolicy",
|
|
187
|
+
"MuteTiming",
|
|
188
|
+
"NotificationTemplate",
|
|
189
|
+
"ExternalDatasource",
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
function words(text: string): string[] {
|
|
193
|
+
return text.split(/[^A-Za-z0-9]+/).filter((w) => w !== "");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function pascal(text: string): string {
|
|
197
|
+
return words(text)
|
|
198
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
199
|
+
.join("");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function describePanel(p: Json): string {
|
|
203
|
+
const title = typeof p.title === "string" && p.title !== "" ? `"${p.title}"` : "(untitled)";
|
|
204
|
+
return `panel ${title}${p.id !== undefined ? ` (id ${String(p.id)})` : ""}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
/** Why a Mixed datasource anywhere but on a panel is not carried. */
|
|
210
|
+
const MIXED_ELSEWHERE = '(it is "-- Mixed --", which only a panel can have: it means each query names its own datasource)';
|
|
211
|
+
|
|
212
|
+
/** Grafana's grid width, and what the dashboard schema says a missing `gridPos` key is. */
|
|
213
|
+
const GRID_COLUMNS = 24;
|
|
214
|
+
const GRID_POS_DEFAULTS = { h: 9, w: 12, x: 0, y: 0 } as const;
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
/** A resolved datasource reference: the prop value, the plugin type when known, and a key for comparing two. */
|
|
218
|
+
interface ResolvedDs {
|
|
219
|
+
value: DeclRef;
|
|
220
|
+
type?: string;
|
|
221
|
+
key: string;
|
|
222
|
+
/** The `{ type, uid }` the build writes for it. */
|
|
223
|
+
written: { type: string; uid: string };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
type Resolution = ResolvedDs | "mixed" | undefined;
|
|
227
|
+
|
|
228
|
+
interface DatasourceVariableInfo {
|
|
229
|
+
declId: string;
|
|
230
|
+
pluginType: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The state of one dashboard's conversion. */
|
|
234
|
+
class DashboardConverter {
|
|
235
|
+
readonly declarations: Declaration[] = [];
|
|
236
|
+
readonly customClasses = new Map<string, CustomClass>();
|
|
237
|
+
private readonly modules = new Map<string, ModuleSpec>();
|
|
238
|
+
private readonly refDecls = new Map<string, string>();
|
|
239
|
+
/** Datasource variables (and `__inputs` datasources) by name. */
|
|
240
|
+
private readonly datasourceVariables = new Map<string, DatasourceVariableInfo>();
|
|
241
|
+
/** Every declared variable by name, for `repeat`. */
|
|
242
|
+
private readonly variables = new Map<string, string>();
|
|
243
|
+
private panelCount = 0;
|
|
244
|
+
private rowCount = 0;
|
|
245
|
+
private legacyPanels = 0;
|
|
246
|
+
private readonly legacyTypes = new Set<string>();
|
|
247
|
+
private readonly takenModuleFiles = new Set<string>();
|
|
248
|
+
|
|
249
|
+
constructor(
|
|
250
|
+
private readonly d: Json,
|
|
251
|
+
private readonly report: Report,
|
|
252
|
+
) {}
|
|
253
|
+
|
|
254
|
+
// ── modules and custom classes ──────────────────────────────────
|
|
255
|
+
|
|
256
|
+
private module(key: string, file: string, summary: string): string {
|
|
257
|
+
if (!this.modules.has(key)) {
|
|
258
|
+
let f = file;
|
|
259
|
+
for (let n = 2; this.takenModuleFiles.has(f); n++) f = `${file}-${n}`;
|
|
260
|
+
this.takenModuleFiles.add(f);
|
|
261
|
+
this.modules.set(key, { key, file: f, summary });
|
|
262
|
+
}
|
|
263
|
+
return key;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private customClassName(base: string): string {
|
|
267
|
+
const taken = new Set([...PACKAGE_CLASS_NAMES, ...[...this.customClasses.values()].map((c) => c.className)]);
|
|
268
|
+
let name = base;
|
|
269
|
+
for (let n = 2; taken.has(name); n++) name = `${base}${n}`;
|
|
270
|
+
return name;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** The class for a panel of this type: a built-in, or a `definePanel` the import declares. */
|
|
274
|
+
private panelClass(type: string): { className: string; customClass?: string } {
|
|
275
|
+
const builtin = builtinPanelFor(type);
|
|
276
|
+
if (builtin) return { className: builtin.className };
|
|
277
|
+
const id = `panel-class:${type}`;
|
|
278
|
+
let custom = this.customClasses.get(id);
|
|
279
|
+
if (!custom) {
|
|
280
|
+
this.module("plugins", "plugins", "Panel and query classes for plugins chant has no class for");
|
|
281
|
+
const className = this.customClassName(`${/^[0-9]/.test(type) ? "Plugin" : ""}${pascal(type) || "Custom"}Panel`);
|
|
282
|
+
custom = {
|
|
283
|
+
id,
|
|
284
|
+
className,
|
|
285
|
+
factory: "definePanel",
|
|
286
|
+
definition: { type, className, defaultSize: { w: 12, h: 8 } },
|
|
287
|
+
comment: [
|
|
288
|
+
`// The "${type}" panel is not one chant has a class for, so its options are carried as data`,
|
|
289
|
+
"// and are not type-checked. Give definePanel an options type to check them.",
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
this.customClasses.set(id, custom);
|
|
293
|
+
}
|
|
294
|
+
return { className: custom.className, customClass: id };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** The class for a query to a datasource of this type: a built-in, or a `defineQuery` the import declares. */
|
|
298
|
+
private queryClass(datasourceType: string): { className: string; customClass?: string } {
|
|
299
|
+
const builtin = builtinQueryFor(datasourceType);
|
|
300
|
+
if (builtin) return { className: builtin.className };
|
|
301
|
+
const id = `query-class:${datasourceType}`;
|
|
302
|
+
let custom = this.customClasses.get(id);
|
|
303
|
+
if (!custom) {
|
|
304
|
+
this.module("plugins", "plugins", "Panel and query classes for plugins chant has no class for");
|
|
305
|
+
const isDefault = datasourceType === DEFAULT_DATASOURCE_TYPE;
|
|
306
|
+
const className = this.customClassName(isDefault ? "DefaultDatasourceQuery" : `${pascal(datasourceType) || "Custom"}Query`);
|
|
307
|
+
custom = {
|
|
308
|
+
id,
|
|
309
|
+
className,
|
|
310
|
+
factory: "defineQuery",
|
|
311
|
+
definition: { datasourceType, className },
|
|
312
|
+
comment: isDefault
|
|
313
|
+
? [
|
|
314
|
+
"// Queries that name no datasource, on panels that name none: Grafana sends them to its",
|
|
315
|
+
"// default datasource. The query model is carried as data and is not type-checked.",
|
|
316
|
+
]
|
|
317
|
+
: [
|
|
318
|
+
`// Queries to "${datasourceType}" datasources, which chant has no query class for, so the`,
|
|
319
|
+
"// query model is carried as data and is not type-checked. Give defineQuery a model type to check it.",
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
this.customClasses.set(id, custom);
|
|
323
|
+
}
|
|
324
|
+
return { className: custom.className, customClass: id };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private add(decl: Declaration): string {
|
|
328
|
+
this.declarations.push(decl);
|
|
329
|
+
return decl.id;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── datasources ─────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
/** Datasources the dashboard references by uid, declared as `ExternalDatasource`s; exported so the build sees them. */
|
|
335
|
+
readonly externals: string[] = [];
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The declaration for a datasource referenced by uid: an
|
|
339
|
+
* `ExternalDatasource` (it exists in Grafana, outside this dashboard), or,
|
|
340
|
+
* for one of Grafana's own pseudo-datasources, a `DatasourceRef` const.
|
|
341
|
+
*/
|
|
342
|
+
private refDecl(type: string, uid: string): ResolvedDs {
|
|
343
|
+
const key = `${type}\u0000${uid}`;
|
|
344
|
+
let id = this.refDecls.get(key);
|
|
345
|
+
if (!id) {
|
|
346
|
+
id = `datasource:${type}:${uid}`;
|
|
347
|
+
this.module("datasources", "datasources", "Datasources the panels and queries refer to, by the uid they have in Grafana");
|
|
348
|
+
const pseudo = BUILTIN_DATASOURCE_UIDS.has(uid) || type === "datasource" || type === "grafana" || uid.includes("$");
|
|
349
|
+
if (pseudo) {
|
|
350
|
+
const hint = uid.includes("$") ? `${type} datasource` : `${uid.replace(/-/g, " ")} datasource`;
|
|
351
|
+
this.add({
|
|
352
|
+
id,
|
|
353
|
+
kind: "value",
|
|
354
|
+
value: { type, uid },
|
|
355
|
+
type: { text: `DatasourceRef<${JSON.stringify(type)}>`, imports: ["DatasourceRef"] },
|
|
356
|
+
name: hint,
|
|
357
|
+
module: "datasources",
|
|
358
|
+
});
|
|
359
|
+
} else {
|
|
360
|
+
const hint = /^[A-Za-z][A-Za-z0-9_-]{0,24}$/.test(uid) ? uid : `${type} datasource`;
|
|
361
|
+
this.add({ id, kind: "new", className: "ExternalDatasource", props: { type, uid }, name: hint, module: "datasources" });
|
|
362
|
+
this.externals.push(id);
|
|
363
|
+
}
|
|
364
|
+
this.refDecls.set(key, id);
|
|
365
|
+
}
|
|
366
|
+
return { value: declRef(id), type, key, written: { type, uid } };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* GRAF101 checks a datasource variable against the declared datasources of
|
|
371
|
+
* its type. When the dashboard names some datasources by uid but a
|
|
372
|
+
* datasource variable's type is not among them, declaring the others would
|
|
373
|
+
* make GRAF101 fail the build over a datasource the dashboard never names;
|
|
374
|
+
* then the uids are written as plain refs instead, and the warning says
|
|
375
|
+
* what to declare.
|
|
376
|
+
*/
|
|
377
|
+
private settleExternals(): void {
|
|
378
|
+
if (this.externals.length === 0) return;
|
|
379
|
+
const covered = new Set(this.externals.map((id) => String(this.declarations.find((d) => d.id === id)!.props!.type)));
|
|
380
|
+
const uncovered = [...this.datasourceVariables.entries()].filter(([, v]) => !covered.has(v.pluginType));
|
|
381
|
+
if (uncovered.length === 0) return;
|
|
382
|
+
const uids: string[] = [];
|
|
383
|
+
for (const id of this.externals) {
|
|
384
|
+
const i = this.declarations.findIndex((d) => d.id === id);
|
|
385
|
+
const { type, uid } = this.declarations[i].props as { type: string; uid: string };
|
|
386
|
+
uids.push(`"${uid}" (${type})`);
|
|
387
|
+
this.declarations[i] = {
|
|
388
|
+
id,
|
|
389
|
+
kind: "value",
|
|
390
|
+
value: { type, uid },
|
|
391
|
+
type: { text: `DatasourceRef<${JSON.stringify(type)}>`, imports: ["DatasourceRef"] },
|
|
392
|
+
name: this.declarations[i].name,
|
|
393
|
+
module: "datasources",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
this.externals.length = 0;
|
|
397
|
+
const vars = uncovered.map(([name, v]) => `$${name} (${v.pluginType})`);
|
|
398
|
+
this.report.warn(
|
|
399
|
+
`datasources: the dashboard names ${list(uids)} by uid, but no ${[...new Set(uncovered.map(([, v]) => v.pluginType))].join(" or ")} datasource, ` +
|
|
400
|
+
`which ${list(vars)} ${vars.length === 1 ? "chooses" : "choose"} among. They are written as plain { type, uid } refs rather than ExternalDatasources, ` +
|
|
401
|
+
"so GRAF101 and GRAF102 do not check them; declare an ExternalDatasource for each, and one of each variable's type, to have them checked.",
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* A `datasource` value as a prop: a declared datasource variable, or a
|
|
407
|
+
* `DatasourceRef` const. Records the edit when it is written differently
|
|
408
|
+
* or cannot be carried.
|
|
409
|
+
*/
|
|
410
|
+
resolveDatasource(value: unknown, path: string, subject: string): Resolution {
|
|
411
|
+
if (value === undefined || value === null) return undefined;
|
|
412
|
+
let ref: { type?: string; uid?: string };
|
|
413
|
+
if (typeof value === "string") {
|
|
414
|
+
// A reference by name, from before Grafana 8.3.
|
|
415
|
+
if (value === "" || value === "default") {
|
|
416
|
+
this.report.drop(path, subject, "datasource");
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
if (value === MIXED_UID) return "mixed";
|
|
420
|
+
if (value === "-- Grafana --" || value === "grafana") ref = { type: "grafana", uid: "-- Grafana --" };
|
|
421
|
+
else if (value === "-- Dashboard --") ref = { type: "datasource", uid: "-- Dashboard --" };
|
|
422
|
+
else if (VARIABLE_UID.test(value)) ref = { uid: value };
|
|
423
|
+
else {
|
|
424
|
+
this.report.drop(path, subject, "datasource", `(it names the datasource "${value}" rather than its uid, from before Grafana 8.3)`);
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
} else if (isObject(value)) {
|
|
428
|
+
ref = { type: typeof value.type === "string" ? value.type : undefined, uid: typeof value.uid === "string" ? value.uid : undefined };
|
|
429
|
+
const extra = Object.keys(value).filter((k) => k !== "type" && k !== "uid" && value[k] !== undefined);
|
|
430
|
+
if (extra.length > 0) {
|
|
431
|
+
this.report.drop(path, subject, "datasource", `(it has ${list(extra)} besides type and uid)`);
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
this.report.drop(path, subject, "datasource", "(it is not a datasource reference)");
|
|
436
|
+
return undefined;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (ref.uid === undefined || ref.uid === "") {
|
|
440
|
+
this.report.drop(path, subject, "datasource", `(it names no uid${ref.type ? `, only the type ${ref.type}` : ""})`);
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
if (ref.uid === MIXED_UID) return "mixed";
|
|
444
|
+
|
|
445
|
+
const variable = VARIABLE_UID.exec(ref.uid);
|
|
446
|
+
if (variable) {
|
|
447
|
+
const name = variable[1] ?? variable[2] ?? variable[3];
|
|
448
|
+
const dsVar = this.datasourceVariables.get(name);
|
|
449
|
+
if (dsVar && (ref.type === undefined || ref.type === dsVar.pluginType)) {
|
|
450
|
+
const written = { type: dsVar.pluginType, uid: `\${${name}}` };
|
|
451
|
+
const resolved: ResolvedDs = { value: declRef(dsVar.declId), type: dsVar.pluginType, key: `var\u0000${name}`, written };
|
|
452
|
+
if (!deepEqual(value, written) && !(isObject(value) && value.type === written.type && canonicalVariableUid(String(value.uid)) === written.uid)) {
|
|
453
|
+
this.report.replace(path, written, subject, "datasource");
|
|
454
|
+
}
|
|
455
|
+
return resolved;
|
|
456
|
+
}
|
|
457
|
+
if (ref.type === undefined) {
|
|
458
|
+
this.report.drop(path, subject, "datasource", `(it refers to $${name}, which is not a datasource variable of this dashboard)`);
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
const resolved = this.refDecl(ref.type, canonicalVariableUid(ref.uid));
|
|
462
|
+
if (typeof value === "string") this.report.replace(path, { type: ref.type, uid: canonicalVariableUid(ref.uid) }, subject, "datasource");
|
|
463
|
+
return resolved;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (ref.type === undefined) {
|
|
467
|
+
const pseudo: Record<string, string> = { "-- Grafana --": "grafana", grafana: "grafana", "-- Dashboard --": "datasource" };
|
|
468
|
+
if (pseudo[ref.uid]) ref.type = pseudo[ref.uid];
|
|
469
|
+
else {
|
|
470
|
+
this.report.drop(path, subject, "datasource", `(it names the uid "${ref.uid}" but not the datasource's type)`);
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const resolved = this.refDecl(ref.type, ref.uid);
|
|
475
|
+
if (!isObject(value) || value.type !== ref.type || value.uid !== ref.uid) {
|
|
476
|
+
this.report.replace(path, { type: ref.type, uid: ref.uid }, subject, "datasource");
|
|
477
|
+
}
|
|
478
|
+
return resolved;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── __inputs ────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
/** Datasource inputs become datasource variables, declared ahead of the dashboard's own. */
|
|
484
|
+
inputVariables(templateNames: Set<string>): string[] {
|
|
485
|
+
const ids: string[] = [];
|
|
486
|
+
const inputs = Array.isArray(this.d.__inputs) ? this.d.__inputs : [];
|
|
487
|
+
const made: string[] = [];
|
|
488
|
+
inputs.forEach((input, i) => {
|
|
489
|
+
if (!isObject(input) || typeof input.name !== "string") return;
|
|
490
|
+
if (input.type === "constant") return;
|
|
491
|
+
if (input.type !== "datasource" || typeof input.pluginId !== "string") {
|
|
492
|
+
this.report.drop(pointer("__inputs", i), "__inputs", input.name, `(an input of type ${String(input.type)} chant has no place for)`);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (templateNames.has(input.name)) return; // the dashboard declares it itself
|
|
496
|
+
this.module("variables", "variables", "Variables, in the order the dashboard lists them");
|
|
497
|
+
const id = `variable:${input.name}`;
|
|
498
|
+
const props: Json = { name: input.name };
|
|
499
|
+
const label = typeof input.label === "string" && input.label !== "" ? input.label : undefined;
|
|
500
|
+
if (label) props.label = label;
|
|
501
|
+
props.pluginType = input.pluginId;
|
|
502
|
+
this.add({
|
|
503
|
+
id,
|
|
504
|
+
kind: "new",
|
|
505
|
+
className: "DatasourceVariable",
|
|
506
|
+
props,
|
|
507
|
+
name: input.name,
|
|
508
|
+
module: "variables",
|
|
509
|
+
comment: [`// From __inputs: the dashboard was exported for sharing, with ${input.name} for its ${input.pluginName ?? input.pluginId} datasource.`],
|
|
510
|
+
});
|
|
511
|
+
this.datasourceVariables.set(input.name, { declId: id, pluginType: input.pluginId });
|
|
512
|
+
this.variables.set(input.name, id);
|
|
513
|
+
this.report.edit({
|
|
514
|
+
op: "prependVariable",
|
|
515
|
+
value: { type: "datasource", name: input.name, ...(label ? { label } : {}), query: input.pluginId, regex: "", refresh: 1, options: [] },
|
|
516
|
+
});
|
|
517
|
+
ids.push(id);
|
|
518
|
+
made.push(`${input.name} (${input.pluginId})`);
|
|
519
|
+
});
|
|
520
|
+
if (made.length > 0) {
|
|
521
|
+
this.report.warn(
|
|
522
|
+
`__inputs: ${list(made)} ${made.length === 1 ? "becomes a datasource variable" : "become datasource variables"} of the same name, ` +
|
|
523
|
+
"so the dashboard asks for its datasources in its variable bar rather than in Grafana's import dialog",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return ids;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── variables ───────────────────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
variable(json: unknown, index: number): string | undefined {
|
|
532
|
+
const path = pointer("templating", "list", index);
|
|
533
|
+
if (!isObject(json) || typeof json.name !== "string") {
|
|
534
|
+
this.report.drop(path, "templating", `entry ${index}`, "(it is not a named variable)");
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
const name = json.name;
|
|
538
|
+
const subject = `variable "${name}"`;
|
|
539
|
+
const type = String(json.type);
|
|
540
|
+
const mapping = VARIABLE_MAPPINGS[type];
|
|
541
|
+
if (!mapping) {
|
|
542
|
+
this.report.drop(path, subject, "", `(${type}) is not carried: chant has no ${type} variable yet (#2952), so it is left out`);
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
let dropped = false;
|
|
546
|
+
const defaults = variableDefaults(type);
|
|
547
|
+
const ctx: VariableContext = {
|
|
548
|
+
json,
|
|
549
|
+
path,
|
|
550
|
+
datasource: (value, p) => {
|
|
551
|
+
const r = this.resolveDatasource(value, p, subject);
|
|
552
|
+
if (r === "mixed") this.report.drop(p, subject, "datasource", MIXED_ELSEWHERE);
|
|
553
|
+
return r === "mixed" || r === undefined ? undefined : r.value;
|
|
554
|
+
},
|
|
555
|
+
drop: (key, why) => this.report.drop(`${path}${pointer(key)}`, subject, key, why ?? NO_PROP),
|
|
556
|
+
dropVariable: (why) => {
|
|
557
|
+
dropped = true;
|
|
558
|
+
this.report.drop(path, subject, "", `is not carried: ${why}, so it is left out`);
|
|
559
|
+
},
|
|
560
|
+
replace: (key, value, why) => this.report.replace(`${path}${pointer(key)}`, value, subject, key, why),
|
|
561
|
+
};
|
|
562
|
+
const specific = mapping.convert(ctx);
|
|
563
|
+
if (dropped || specific === undefined) return undefined;
|
|
564
|
+
|
|
565
|
+
const props: Json = { name };
|
|
566
|
+
if (typeof json.label === "string" && json.label !== "") props.label = json.label;
|
|
567
|
+
if (typeof json.description === "string" && json.description !== "") props.description = json.description;
|
|
568
|
+
if (type !== "constant" && typeof json.hide === "number" && json.hide !== 0) {
|
|
569
|
+
const hide = VARIABLE_HIDE[json.hide];
|
|
570
|
+
if (hide) props.hide = hide;
|
|
571
|
+
else this.report.drop(`${path}/hide`, subject, "hide", `(${json.hide} is not a hide value Grafana has)`);
|
|
572
|
+
}
|
|
573
|
+
if (json.skipUrlSync === true) props.skipUrlSync = true;
|
|
574
|
+
Object.assign(props, specific);
|
|
575
|
+
|
|
576
|
+
const known = new Set([...COMMON_VARIABLE_KEYS, ...mapping.keys]);
|
|
577
|
+
for (const key of Object.keys(json)) {
|
|
578
|
+
if (known.has(key)) continue;
|
|
579
|
+
this.report.drop(`${path}${pointer(key)}`, subject, key, isDefault(defaults, key, json[key]) ? undefined : NO_PROP);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
this.module("variables", "variables", "Variables, in the order the dashboard lists them");
|
|
583
|
+
const id = `variable:${name}`;
|
|
584
|
+
this.add({ id, kind: "new", className: mapping.className, props, name, module: "variables" });
|
|
585
|
+
this.variables.set(name, id);
|
|
586
|
+
if (type === "datasource") {
|
|
587
|
+
const pluginType = String(props.pluginType);
|
|
588
|
+
this.datasourceVariables.set(name, { declId: id, pluginType });
|
|
589
|
+
// Since Grafana 8.3 the selected value is the datasource's uid and the text its name: a datasource that exists.
|
|
590
|
+
const cur = isObject(json.current) ? json.current : {};
|
|
591
|
+
if (typeof cur.value === "string" && cur.value !== "" && !cur.value.includes("$") && cur.value !== cur.text) {
|
|
592
|
+
this.refDecl(pluginType, cur.value);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return id;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── panels and rows ─────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* A panel's `gridPos` with every coordinate written out. A missing `x` or
|
|
602
|
+
* `y` is 0 to Grafana, as it is in the dashboard schema, but the build
|
|
603
|
+
* would place the panel itself, so the 0 is written. A missing `h` or `w`
|
|
604
|
+
* is written as the schema's default, with a warning, since the build
|
|
605
|
+
* would otherwise use the panel class's default size.
|
|
606
|
+
*/
|
|
607
|
+
private gridPos(json: Json, path: string, subject: string, props: Json): void {
|
|
608
|
+
const gp = json.gridPos;
|
|
609
|
+
if (!isObject(gp)) return;
|
|
610
|
+
const filled: Json = { ...gp };
|
|
611
|
+
const sized: string[] = [];
|
|
612
|
+
for (const [key, value] of Object.entries(GRID_POS_DEFAULTS)) {
|
|
613
|
+
if (typeof gp[key] === "number") continue;
|
|
614
|
+
filled[key] = value;
|
|
615
|
+
if (key === "h" || key === "w") sized.push(`gridPos.${key}`);
|
|
616
|
+
}
|
|
617
|
+
if (deepEqual(filled, gp)) return;
|
|
618
|
+
props.gridPos = filled;
|
|
619
|
+
const why = sized.length === 0 ? undefined : `${sized.length === 1 ? "is" : "are"} missing, so the dashboard schema's default is written (h ${GRID_POS_DEFAULTS.h}, w ${GRID_POS_DEFAULTS.w})`;
|
|
620
|
+
this.report.replace(`${path}/gridPos`, filled, subject, sized.join(" and ") || "gridPos", why);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** A panel's Mixed datasource, as a `DatasourceRef` const: the build writes it when the panel's queries alone would not make it Mixed. */
|
|
624
|
+
private mixed(value: unknown, path: string, subject: string): ResolvedDs {
|
|
625
|
+
const resolved = this.refDecl("datasource", MIXED_UID);
|
|
626
|
+
if (!deepEqual(value, resolved.written)) this.report.replace(path, resolved.written, subject, "datasource");
|
|
627
|
+
return resolved;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private copyFields(json: Json, fields: readonly string[], defaults: Readonly<Json>, props: Json): void {
|
|
631
|
+
for (const key of fields) {
|
|
632
|
+
if (!(key in json)) continue;
|
|
633
|
+
const v = json[key];
|
|
634
|
+
if (v === null || v === undefined) continue;
|
|
635
|
+
if (key !== "id" && key !== "title" && isDefault(defaults, key, v)) continue;
|
|
636
|
+
props[key] = v;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* A panel's transformations, typed (#2954): each is written as the
|
|
642
|
+
* `{ id, options }` object `Transformation` types by id, or, when its id,
|
|
643
|
+
* a key or an option key is one the types don't know, through
|
|
644
|
+
* `customTransformation(id, options, { disabled, filter, topic })` with
|
|
645
|
+
* the same JSON. Nothing is dropped.
|
|
646
|
+
*/
|
|
647
|
+
private transformations(list: unknown[], subject: string): unknown[] {
|
|
648
|
+
return list.map((t, i) => {
|
|
649
|
+
if (!isObject(t)) return t;
|
|
650
|
+
const why = untypedTransformationReason(t);
|
|
651
|
+
if (why === undefined) return t;
|
|
652
|
+
const { id, options, ...rest } = t;
|
|
653
|
+
if (typeof id !== "string") {
|
|
654
|
+
this.report.warn(`${subject}: transformation ${i + 1} has no id, so Grafana skips it; it is not carried.`);
|
|
655
|
+
return undefined;
|
|
656
|
+
}
|
|
657
|
+
if (options !== undefined && !isObject(options)) this.report.warn(`${subject}: transformation ${i + 1} (${id}) options are not an object, so they are not carried.`);
|
|
658
|
+
const common = Object.keys(rest).length > 0 ? [rest] : [];
|
|
659
|
+
this.report.warn(`${subject}: transformation ${i + 1} is written with customTransformation(), untyped: ${why}.`);
|
|
660
|
+
return callValue("customTransformation", [id, isObject(options) ? options : {}, ...common]);
|
|
661
|
+
}).filter((t) => t !== undefined);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** One panel, with its queries; returns the panel's declaration id. */
|
|
665
|
+
panel(json: unknown, path: string, module: string, rowDatasource: Resolution): string | undefined {
|
|
666
|
+
if (!isObject(json)) {
|
|
667
|
+
this.report.drop(path, "panels", "", "(an entry that is not a panel) is not carried");
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
const subject = describePanel(json);
|
|
671
|
+
if (isObject(json.libraryPanel)) {
|
|
672
|
+
const lp = json.libraryPanel;
|
|
673
|
+
this.report.drop(
|
|
674
|
+
path,
|
|
675
|
+
subject,
|
|
676
|
+
"",
|
|
677
|
+
`is a library panel ("${String(lp.name ?? lp.uid)}", uid ${String(lp.uid)}); library panels are not carried yet, so it is left out`,
|
|
678
|
+
);
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
const type = typeof json.type === "string" ? json.type : "";
|
|
682
|
+
// A built-in's id is taken as it is: core ids predate the lowercase rule (`nodeGraph`).
|
|
683
|
+
if (type === "row" || (!builtinPanelFor(type) && !PLUGIN_ID.test(type))) {
|
|
684
|
+
this.report.drop(path, subject, "", `has ${type ? `the type "${type}", which is not a panel plugin id` : "no type"}, so it is left out`);
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
const cls = this.panelClass(type);
|
|
688
|
+
const index = this.panelCount++;
|
|
689
|
+
const id = `panel:${index}`;
|
|
690
|
+
const props: Json = {};
|
|
691
|
+
this.copyFields(json, PANEL_FIELDS, PANEL_DEFAULTS, props);
|
|
692
|
+
if (Array.isArray(props.transformations)) props.transformations = this.transformations(props.transformations, subject);
|
|
693
|
+
const fc = json.fieldConfig;
|
|
694
|
+
if (isObject(fc) && deepEqual(fc, { defaults: {}, overrides: [] })) delete props.fieldConfig;
|
|
695
|
+
|
|
696
|
+
this.gridPos(json, path, subject, props);
|
|
697
|
+
|
|
698
|
+
const own = this.resolveDatasource(json.datasource, `${path}/datasource`, subject);
|
|
699
|
+
if (own === "mixed") props.datasource = this.mixed(json.datasource, `${path}/datasource`, subject).value;
|
|
700
|
+
else if (own !== undefined) props.datasource = own.value;
|
|
701
|
+
// A panel without a datasource of its own inherits its row's in the build; Grafana does not do that.
|
|
702
|
+
const inherited = own === undefined && rowDatasource !== undefined && rowDatasource !== "mixed" ? rowDatasource : undefined;
|
|
703
|
+
if (inherited && (json.datasource === undefined || json.datasource === null)) {
|
|
704
|
+
this.report.replace(
|
|
705
|
+
`${path}/datasource`,
|
|
706
|
+
inherited.written,
|
|
707
|
+
subject,
|
|
708
|
+
"datasource",
|
|
709
|
+
"is missing, and a panel in a chant Row without a datasource takes the row's, so it gets the row's datasource",
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
const panelDs: Resolution = own ?? inherited;
|
|
713
|
+
|
|
714
|
+
const queries: DeclRef[] = [];
|
|
715
|
+
const targets = Array.isArray(json.targets) ? json.targets : json.targets === undefined || json.targets === null ? [] : undefined;
|
|
716
|
+
if (targets === undefined) this.report.drop(`${path}/targets`, subject, "targets", "(it is not a list)");
|
|
717
|
+
(targets ?? []).forEach((t, i) => {
|
|
718
|
+
const tPath = `${path}/targets/${i}`;
|
|
719
|
+
if (!isObject(t)) {
|
|
720
|
+
this.report.drop(tPath, subject, `query ${i}`, "(it is not an object)");
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const tds = this.resolveDatasource(t.datasource, `${tPath}/datasource`, `${subject} query ${String(t.refId ?? i)}`);
|
|
724
|
+
if (tds === "mixed") this.report.drop(`${tPath}/datasource`, `${subject} query ${String(t.refId ?? i)}`, "datasource", MIXED_ELSEWHERE);
|
|
725
|
+
const tResolved = tds === "mixed" ? undefined : tds;
|
|
726
|
+
const effectiveType =
|
|
727
|
+
tResolved?.type ?? (panelDs !== undefined && panelDs !== "mixed" ? panelDs.type : undefined) ?? DEFAULT_DATASOURCE_TYPE;
|
|
728
|
+
const qcls = this.queryClass(effectiveType);
|
|
729
|
+
const qprops: Json = {};
|
|
730
|
+
for (const [k, v] of Object.entries(t)) {
|
|
731
|
+
if (k === "datasource") continue;
|
|
732
|
+
qprops[k] = v;
|
|
733
|
+
}
|
|
734
|
+
const panelKey = panelDs !== undefined && panelDs !== "mixed" ? panelDs.key : undefined;
|
|
735
|
+
if (tResolved !== undefined && tResolved.key !== panelKey) qprops.datasource = tResolved.value;
|
|
736
|
+
const qid = `query:${index}:${i}`;
|
|
737
|
+
this.add({
|
|
738
|
+
id: qid,
|
|
739
|
+
kind: "new",
|
|
740
|
+
className: qcls.className,
|
|
741
|
+
customClass: qcls.customClass,
|
|
742
|
+
props: qprops,
|
|
743
|
+
name: { of: id, suffix: typeof t.refId === "string" && t.refId !== "" ? t.refId : `query ${i}` },
|
|
744
|
+
module,
|
|
745
|
+
unit: id,
|
|
746
|
+
});
|
|
747
|
+
queries.push(declRef(qid));
|
|
748
|
+
});
|
|
749
|
+
if (queries.length > 0) props.targets = queries;
|
|
750
|
+
|
|
751
|
+
if (typeof json.repeat === "string" && json.repeat !== "") {
|
|
752
|
+
const v = this.variables.get(json.repeat);
|
|
753
|
+
props.repeat = v ? declRef(v) : json.repeat;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const handled = new Set([...PANEL_FIELDS, "type", "datasource", "targets", "repeat", "libraryPanel"]);
|
|
757
|
+
const extra: string[] = [];
|
|
758
|
+
for (const key of Object.keys(json)) {
|
|
759
|
+
if (handled.has(key)) continue;
|
|
760
|
+
const v = json[key];
|
|
761
|
+
if (v === null || v === undefined || isDefault(PANEL_DEFAULTS, key, v)) {
|
|
762
|
+
this.report.drop(`${path}${pointer(key)}`, subject, key);
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
extra.push(key);
|
|
766
|
+
this.report.drop(`${path}${pointer(key)}`, subject, key, NO_PROP);
|
|
767
|
+
}
|
|
768
|
+
if (extra.length >= 3 && ANGULAR_PANELS.has(type)) {
|
|
769
|
+
this.legacyPanels++;
|
|
770
|
+
this.legacyTypes.add(type);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const title = typeof json.title === "string" && json.title !== "" ? json.title : `panel ${String(json.id ?? index + 1)}`;
|
|
774
|
+
return this.add({
|
|
775
|
+
id,
|
|
776
|
+
kind: "new",
|
|
777
|
+
className: cls.className,
|
|
778
|
+
customClass: cls.customClass,
|
|
779
|
+
props,
|
|
780
|
+
name: title,
|
|
781
|
+
module,
|
|
782
|
+
unit: id,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** A row, after its panels. */
|
|
787
|
+
row(json: Json, path: string, children: Array<{ json: unknown; path: string }>, rowModule: string, ds: Resolution): string {
|
|
788
|
+
const index = this.rowCount++;
|
|
789
|
+
const id = `row:${index}`;
|
|
790
|
+
const subject = `row "${String(json.title ?? "")}"`;
|
|
791
|
+
const panels: DeclRef[] = [];
|
|
792
|
+
for (const c of children) {
|
|
793
|
+
const pid = this.panel(c.json, c.path, rowModule, ds);
|
|
794
|
+
if (pid) panels.push(declRef(pid));
|
|
795
|
+
}
|
|
796
|
+
const props: Json = { title: typeof json.title === "string" ? json.title : "" };
|
|
797
|
+
this.copyFields(json, ROW_FIELDS, ROW_DEFAULTS, props);
|
|
798
|
+
if (ds === "mixed") this.report.drop(`${path}/datasource`, subject, "datasource", MIXED_ELSEWHERE);
|
|
799
|
+
else if (ds !== undefined) props.datasource = ds.value;
|
|
800
|
+
const gp = json.gridPos;
|
|
801
|
+
if (isObject(gp) && typeof gp.y === "number") {
|
|
802
|
+
// Grafana draws a row header full width and one line high whatever its gridPos says, so only y is carried.
|
|
803
|
+
props.gridPos = { y: gp.y };
|
|
804
|
+
const written = { h: 1, w: GRID_COLUMNS, x: 0, y: gp.y };
|
|
805
|
+
if (!deepEqual(gp, written)) this.report.replace(`${path}/gridPos`, written, subject, "gridPos");
|
|
806
|
+
}
|
|
807
|
+
if (typeof json.repeat === "string" && json.repeat !== "") {
|
|
808
|
+
const v = this.variables.get(json.repeat);
|
|
809
|
+
props.repeat = v ? declRef(v) : json.repeat;
|
|
810
|
+
}
|
|
811
|
+
if (panels.length > 0) props.panels = panels;
|
|
812
|
+
const handled = new Set([...ROW_FIELDS, "type", "datasource", "repeat", "panels", "gridPos"]);
|
|
813
|
+
for (const key of Object.keys(json)) {
|
|
814
|
+
if (handled.has(key)) continue;
|
|
815
|
+
const v = json[key];
|
|
816
|
+
this.report.drop(`${path}${pointer(key)}`, subject, key, v === null || isDefault(ROW_DEFAULTS, key, v) ? undefined : NO_PROP);
|
|
817
|
+
}
|
|
818
|
+
return this.add({ id, kind: "new", className: "Row", props, name: `${String(json.title ?? "")} row`, module: rowModule, unit: id });
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** The dashboard's panels list: top-level panels and rows, each row with the panels under it. */
|
|
822
|
+
items(): DeclRef[] {
|
|
823
|
+
const panels = Array.isArray(this.d.panels) ? this.d.panels : [];
|
|
824
|
+
const out: DeclRef[] = [];
|
|
825
|
+
let i = 0;
|
|
826
|
+
const topModule = () => this.module("panels", "panels", "Panels above the first row");
|
|
827
|
+
while (i < panels.length) {
|
|
828
|
+
const p = panels[i];
|
|
829
|
+
const path = pointer("panels", i);
|
|
830
|
+
if (isObject(p) && p.type === "row") {
|
|
831
|
+
const title = typeof p.title === "string" ? p.title : "";
|
|
832
|
+
const rowModule = this.module(`row:${i}`, `row-${slugUid(title || `row ${i}`)}`, `The row "${title}" and its panels`);
|
|
833
|
+
const ds = this.resolveDatasource(p.datasource, `${path}/datasource`, `row "${title}"`);
|
|
834
|
+
const children: Array<{ json: unknown; path: string }> = [];
|
|
835
|
+
const nested = Array.isArray(p.panels) ? p.panels : [];
|
|
836
|
+
if (p.collapsed === true) {
|
|
837
|
+
nested.forEach((c, j) => children.push({ json: c, path: `${path}/panels/${j}` }));
|
|
838
|
+
i++;
|
|
839
|
+
} else {
|
|
840
|
+
if (nested.length > 0) {
|
|
841
|
+
this.report.drop(`${path}/panels`, `row "${title}"`, "panels", "(the row is expanded, so Grafana shows the panels after it instead)");
|
|
842
|
+
}
|
|
843
|
+
i++;
|
|
844
|
+
while (i < panels.length && !(isObject(panels[i]) && (panels[i] as Json).type === "row")) {
|
|
845
|
+
children.push({ json: panels[i], path: pointer("panels", i) });
|
|
846
|
+
i++;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
out.push(declRef(this.row(p, path, children, rowModule, ds)));
|
|
850
|
+
} else {
|
|
851
|
+
const pid = this.panel(p, path, topModule(), undefined);
|
|
852
|
+
if (pid) out.push(declRef(pid));
|
|
853
|
+
i++;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return out;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ── the dashboard ───────────────────────────────────────────────
|
|
860
|
+
|
|
861
|
+
convert(): Plan {
|
|
862
|
+
const d = this.d;
|
|
863
|
+
for (const key of ["__requires", "__elements"]) {
|
|
864
|
+
if (!(key in d)) continue;
|
|
865
|
+
const v = d[key];
|
|
866
|
+
const empty = v === null || (Array.isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
|
|
867
|
+
if (empty) this.report.drop(pointer(key), "dashboard", key);
|
|
868
|
+
else if (key === "__requires") this.report.drop(pointer(key), "dashboard", key, "(the list of plugins it was exported with; Grafana does not need it to load the dashboard)");
|
|
869
|
+
else this.report.drop(pointer(key), "dashboard", key, "(the library panels exported with it; library panels are not carried yet)");
|
|
870
|
+
}
|
|
871
|
+
if ("__inputs" in d) this.report.drop(pointer("__inputs"), "dashboard", "__inputs");
|
|
872
|
+
|
|
873
|
+
const templating = isObject(d.templating) && Array.isArray(d.templating.list) ? d.templating.list : [];
|
|
874
|
+
const templateNames = new Set(templating.filter(isObject).map((v) => String(v.name)));
|
|
875
|
+
const inputIds = this.inputVariables(templateNames);
|
|
876
|
+
// Datasource variables first: other variables and panels refer to them.
|
|
877
|
+
const order = templating.map((v, i) => ({ v, i })).sort((a, b) => Number(isObject(b.v) && b.v.type === "datasource") - Number(isObject(a.v) && a.v.type === "datasource"));
|
|
878
|
+
const varIds = new Map<number, string>();
|
|
879
|
+
for (const { v, i } of order) {
|
|
880
|
+
const id = this.variable(v, i);
|
|
881
|
+
if (id) varIds.set(i, id);
|
|
882
|
+
}
|
|
883
|
+
const variableRefs = [...inputIds, ...templating.map((_, i) => varIds.get(i)).filter((x): x is string => x !== undefined)].map(declRef);
|
|
884
|
+
|
|
885
|
+
const panelRefs = this.items();
|
|
886
|
+
this.settleExternals();
|
|
887
|
+
if (this.legacyPanels > 0) {
|
|
888
|
+
this.report.warn(
|
|
889
|
+
`${this.legacyPanels} ${this.legacyPanels === 1 ? "panel is an AngularJS panel" : "panels are AngularJS panels"} (${[...this.legacyTypes].join(", ")}) ` +
|
|
890
|
+
"that keep their settings as top-level keys, which are not carried. Grafana converts these panels when it loads the dashboard; " +
|
|
891
|
+
"to import them with their settings, load the dashboard in Grafana 11 or later, export it again, and import that export.",
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const props: Json = {};
|
|
896
|
+
for (const key of DASHBOARD_FIELDS) {
|
|
897
|
+
if (!(key in d)) continue;
|
|
898
|
+
const v = d[key];
|
|
899
|
+
if (v === null || v === undefined || (key !== "title" && key !== "uid" && key !== "timezone" && isDefault(DASHBOARD_DEFAULTS, key, v))) continue;
|
|
900
|
+
if (key === "refresh" && v === false) continue;
|
|
901
|
+
props[key] = v;
|
|
902
|
+
}
|
|
903
|
+
if (typeof props.title !== "string") props.title = "";
|
|
904
|
+
if (typeof d.uid !== "string" || d.uid === "") {
|
|
905
|
+
props.uid = slugUid(String(props.title) || "dashboard");
|
|
906
|
+
this.report.replace(pointer("uid"), props.uid, "dashboard", "uid", `is missing, so the dashboard is given the uid "${String(props.uid)}", from its title`);
|
|
907
|
+
}
|
|
908
|
+
// chant writes "browser" for a dashboard without a timezone; Grafana reads a missing one as "", the viewer's preference.
|
|
909
|
+
if (!("timezone" in d) || d.timezone === null) props.timezone = "";
|
|
910
|
+
if (typeof d.graphTooltip === "number" && d.graphTooltip !== 0) {
|
|
911
|
+
const tooltip = GRAPH_TOOLTIP[d.graphTooltip];
|
|
912
|
+
if (tooltip) props.graphTooltip = tooltip;
|
|
913
|
+
else this.report.drop(pointer("graphTooltip"), "dashboard", "graphTooltip", `(${d.graphTooltip} is not a value Grafana has)`);
|
|
914
|
+
}
|
|
915
|
+
if (typeof d.schemaVersion === "number") {
|
|
916
|
+
if (d.schemaVersion !== DASHBOARD_SCHEMA_VERSION) props.schemaVersion = d.schemaVersion;
|
|
917
|
+
} else {
|
|
918
|
+
this.report.replace(pointer("schemaVersion"), DASHBOARD_SCHEMA_VERSION, "dashboard", "schemaVersion", `is missing, so the dashboard is written at ${DASHBOARD_SCHEMA_VERSION}`);
|
|
919
|
+
}
|
|
920
|
+
if (variableRefs.length > 0) props.variables = variableRefs;
|
|
921
|
+
if (panelRefs.length > 0) props.panels = panelRefs;
|
|
922
|
+
|
|
923
|
+
// Annotations: the built-in one is what Grafana adds anyway; any other is not carried yet.
|
|
924
|
+
const annotations = isObject(d.annotations) && Array.isArray(d.annotations.list) ? d.annotations.list : [];
|
|
925
|
+
const others = annotations.filter((a) => !isBuiltinAnnotation(a));
|
|
926
|
+
if (others.length > 0) {
|
|
927
|
+
const names = others.map((a) => (isObject(a) && typeof a.name === "string" ? `"${a.name}"` : "(unnamed)"));
|
|
928
|
+
this.report.drop(
|
|
929
|
+
pointer("annotations"),
|
|
930
|
+
"dashboard",
|
|
931
|
+
"",
|
|
932
|
+
`has ${others.length === 1 ? "the annotation" : "the annotations"} ${list(names)}, which ${others.length === 1 ? "is" : "are"} not carried: Dashboard has no annotations yet (#2953)`,
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const handled = new Set([
|
|
937
|
+
...DASHBOARD_FIELDS,
|
|
938
|
+
...BOOKKEEPING_KEYS,
|
|
939
|
+
"panels",
|
|
940
|
+
"templating",
|
|
941
|
+
"annotations",
|
|
942
|
+
"graphTooltip",
|
|
943
|
+
"schemaVersion",
|
|
944
|
+
"__inputs",
|
|
945
|
+
"__requires",
|
|
946
|
+
"__elements",
|
|
947
|
+
]);
|
|
948
|
+
for (const key of Object.keys(d)) {
|
|
949
|
+
if (handled.has(key)) continue;
|
|
950
|
+
const v = d[key];
|
|
951
|
+
this.report.drop(pointer(key), "dashboard", key, v === null || isDefault(DASHBOARD_DEFAULTS, key, v) ? undefined : NO_PROP);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
this.module("dashboard", "dashboard", `The dashboard "${String(props.title)}"`);
|
|
955
|
+
this.add({ id: "dashboard", kind: "new", className: "Dashboard", props, name: String(props.title) || "dashboard", module: "dashboard" });
|
|
956
|
+
|
|
957
|
+
const order2 = ["plugins", "datasources", "variables", "panels"];
|
|
958
|
+
const modules = [...this.modules.values()].sort((a, b) => rank(a.key, order2) - rank(b.key, order2));
|
|
959
|
+
return {
|
|
960
|
+
directory: slugUid(typeof d.uid === "string" && d.uid !== "" ? d.uid : String(props.title) || "dashboard"),
|
|
961
|
+
modules,
|
|
962
|
+
declarations: this.declarations,
|
|
963
|
+
customClasses: [...this.customClasses.values()],
|
|
964
|
+
exports: [...this.externals, "dashboard"],
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const ANGULAR_PANELS = new Set(["graph", "singlestat", "table-old", "grafana-singlestat-panel", "grafana-piechart-panel", "grafana-worldmap-panel"]);
|
|
970
|
+
|
|
971
|
+
function rank(key: string, order: string[]): number {
|
|
972
|
+
const i = order.indexOf(key);
|
|
973
|
+
if (i !== -1) return i;
|
|
974
|
+
return key === "dashboard" ? order.length + 2 : order.length + 1;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function substituteStrings(value: unknown, from: string, to: string): unknown {
|
|
978
|
+
if (typeof value === "string") return value.split(from).join(to);
|
|
979
|
+
if (Array.isArray(value)) return value.map((v) => substituteStrings(v, from, to));
|
|
980
|
+
if (isObject(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, substituteStrings(v, from, to)]));
|
|
981
|
+
return value;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** The logical id (export name) of a dashboard's IR resource. */
|
|
985
|
+
function logicalIdFor(title: string): string {
|
|
986
|
+
const w = words(title);
|
|
987
|
+
const id = w.map((x, i) => (i === 0 ? x.charAt(0).toLowerCase() + x.slice(1) : x.charAt(0).toUpperCase() + x.slice(1))).join("");
|
|
988
|
+
return id === "" || /^[0-9]/.test(id) ? `dashboard${pascal(id)}` : id;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/** `__inputs` constants filled in, as Grafana's import dialog does: the dashboard with them substituted. */
|
|
992
|
+
function substituteConstants(dashboard: Json, report: Report): Json {
|
|
993
|
+
let d = dashboard;
|
|
994
|
+
const inputs = Array.isArray(d.__inputs) ? d.__inputs : [];
|
|
995
|
+
for (const input of inputs) {
|
|
996
|
+
if (!isObject(input) || input.type !== "constant" || typeof input.name !== "string") continue;
|
|
997
|
+
const value = typeof input.value === "string" ? input.value : "";
|
|
998
|
+
for (const from of [`\${${input.name}}`, `$${input.name}`]) {
|
|
999
|
+
report.edit({ op: "substitute", from, to: value });
|
|
1000
|
+
d = substituteStrings(d, from, value) as Json;
|
|
1001
|
+
}
|
|
1002
|
+
report.warn(`__inputs: the constant ${input.name} is written as its value ${JSON.stringify(value)}, the value Grafana's import dialog fills in`);
|
|
1003
|
+
}
|
|
1004
|
+
return d;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/** Plan one classic dashboard. */
|
|
1008
|
+
export function planDashboard(dashboard: Json): { plan: Plan; edits: ImportEdit[]; warnings: string[] } {
|
|
1009
|
+
const report = new Report();
|
|
1010
|
+
const plan = new DashboardConverter(substituteConstants(dashboard, report), report).convert();
|
|
1011
|
+
return { plan, edits: report.edits, warnings: report.warnings() };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// ── provisioning files ─────────────────────────────────────────────
|
|
1015
|
+
|
|
1016
|
+
const DATASOURCE_FIELDS = [
|
|
1017
|
+
"name",
|
|
1018
|
+
"type",
|
|
1019
|
+
"uid",
|
|
1020
|
+
"url",
|
|
1021
|
+
"access",
|
|
1022
|
+
"isDefault",
|
|
1023
|
+
"basicAuth",
|
|
1024
|
+
"basicAuthUser",
|
|
1025
|
+
"user",
|
|
1026
|
+
"database",
|
|
1027
|
+
"withCredentials",
|
|
1028
|
+
"jsonData",
|
|
1029
|
+
"secureJsonData",
|
|
1030
|
+
"editable",
|
|
1031
|
+
"orgId",
|
|
1032
|
+
"version",
|
|
1033
|
+
];
|
|
1034
|
+
|
|
1035
|
+
/** Plan a datasource provisioning file: one `Datasource` per entry. */
|
|
1036
|
+
export function planDatasourceProvisioning(doc: Json): { plan: Plan; warnings: string[] } {
|
|
1037
|
+
const report = new Report();
|
|
1038
|
+
const declarations: Declaration[] = [];
|
|
1039
|
+
const list = Array.isArray(doc.datasources) ? doc.datasources : [];
|
|
1040
|
+
list.forEach((ds, i) => {
|
|
1041
|
+
if (!isObject(ds) || typeof ds.name !== "string" || typeof ds.type !== "string") {
|
|
1042
|
+
report.drop(pointer("datasources", i), "datasources", `entry ${i}`, "(it has no name or type)");
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
const props: Json = {};
|
|
1046
|
+
for (const key of DATASOURCE_FIELDS) if (ds[key] !== undefined && ds[key] !== null) props[key] = ds[key];
|
|
1047
|
+
for (const key of Object.keys(ds)) {
|
|
1048
|
+
if (!DATASOURCE_FIELDS.includes(key)) report.drop(pointer("datasources", i, key), `datasource "${ds.name}"`, key, NO_PROP);
|
|
1049
|
+
}
|
|
1050
|
+
declarations.push({ id: `datasource:${i}`, kind: "new", className: "Datasource", props, name: ds.name, module: "datasources" });
|
|
1051
|
+
});
|
|
1052
|
+
for (const key of Object.keys(doc)) {
|
|
1053
|
+
if (key === "apiVersion" || key === "datasources") continue;
|
|
1054
|
+
report.drop(pointer(key), "the provisioning file", key, "(chant writes datasources only; prune and deleteDatasources are #2953)");
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
plan: {
|
|
1058
|
+
directory: "",
|
|
1059
|
+
modules: [{ key: "datasources", file: "datasources", summary: "Datasources, from a Grafana datasource provisioning file" }],
|
|
1060
|
+
declarations,
|
|
1061
|
+
customClasses: [],
|
|
1062
|
+
exports: declarations.map((d) => d.id),
|
|
1063
|
+
},
|
|
1064
|
+
warnings: report.warnings(),
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const PROVIDER_FIELDS = ["name", "orgId", "folder", "folderUid", "disableDeletion", "allowUiUpdates", "updateIntervalSeconds"];
|
|
1069
|
+
|
|
1070
|
+
/** Plan a dashboard provisioning file: one `DashboardProvider` per file provider. */
|
|
1071
|
+
export function planDashboardProvisioning(doc: Json): { plan: Plan; warnings: string[] } {
|
|
1072
|
+
const report = new Report();
|
|
1073
|
+
const declarations: Declaration[] = [];
|
|
1074
|
+
const list = Array.isArray(doc.providers) ? doc.providers : [];
|
|
1075
|
+
list.forEach((p, i) => {
|
|
1076
|
+
if (!isObject(p) || typeof p.name !== "string") {
|
|
1077
|
+
report.drop(pointer("providers", i), "providers", `entry ${i}`, "(it has no name)");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const subject = `provider "${p.name}"`;
|
|
1081
|
+
if (p.type !== undefined && p.type !== "file") {
|
|
1082
|
+
report.drop(pointer("providers", i), subject, "", `is of type ${String(p.type)}; chant writes file providers only, so it is left out`);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const props: Json = {};
|
|
1086
|
+
for (const key of PROVIDER_FIELDS) if (p[key] !== undefined && p[key] !== null) props[key] = p[key];
|
|
1087
|
+
const options = isObject(p.options) ? p.options : {};
|
|
1088
|
+
if (options.path !== undefined) props.path = options.path;
|
|
1089
|
+
if (options.foldersFromFilesStructure !== undefined) props.foldersFromFilesStructure = options.foldersFromFilesStructure;
|
|
1090
|
+
for (const key of Object.keys(options)) {
|
|
1091
|
+
if (key !== "path" && key !== "foldersFromFilesStructure") report.drop(pointer("providers", i, "options", key), subject, `options.${key}`, NO_PROP);
|
|
1092
|
+
}
|
|
1093
|
+
for (const key of Object.keys(p)) {
|
|
1094
|
+
if (!PROVIDER_FIELDS.includes(key) && key !== "type" && key !== "options") report.drop(pointer("providers", i, key), subject, key, NO_PROP);
|
|
1095
|
+
}
|
|
1096
|
+
declarations.push({ id: `provider:${i}`, kind: "new", className: "DashboardProvider", props, name: `${p.name} provider`, module: "providers" });
|
|
1097
|
+
});
|
|
1098
|
+
return {
|
|
1099
|
+
plan: {
|
|
1100
|
+
directory: "",
|
|
1101
|
+
modules: [{ key: "providers", file: "dashboard-providers", summary: "Dashboard providers, from a Grafana dashboard provisioning file" }],
|
|
1102
|
+
declarations,
|
|
1103
|
+
customClasses: [],
|
|
1104
|
+
exports: declarations.map((d) => d.id),
|
|
1105
|
+
},
|
|
1106
|
+
warnings: report.warnings(),
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// ── the parser ─────────────────────────────────────────────────────
|
|
1111
|
+
|
|
1112
|
+
/** The dashboard inside a resource or API response, and where it came from. */
|
|
1113
|
+
export function unwrapDashboard(data: Json): Json {
|
|
1114
|
+
if (looksLikeDashboardApiResponse(data)) return data.dashboard;
|
|
1115
|
+
if (looksLikeDashboardResource(data)) {
|
|
1116
|
+
const spec = { ...data.spec };
|
|
1117
|
+
const meta = isObject((data as Json).metadata) ? ((data as Json).metadata as Json) : {};
|
|
1118
|
+
if ((typeof spec.uid !== "string" || spec.uid === "") && typeof meta.name === "string") spec.uid = meta.name;
|
|
1119
|
+
return spec;
|
|
1120
|
+
}
|
|
1121
|
+
return data;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function parseDocument(content: string): unknown {
|
|
1125
|
+
try {
|
|
1126
|
+
return JSON.parse(content);
|
|
1127
|
+
} catch {
|
|
1128
|
+
// Provisioning files are YAML.
|
|
1129
|
+
}
|
|
1130
|
+
return jsYaml.load(content, { schema: jsYaml.CORE_SCHEMA });
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function resource(logicalId: string, type: string, plan: Plan, metadata?: Json): ResourceIR {
|
|
1134
|
+
const properties: PlanResourceProperties = { plan };
|
|
1135
|
+
return { logicalId, type, properties: properties as unknown as Json, ...(metadata ? { metadata } : {}) };
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** One dashboard's IR: its plan, and the source and edits the round trip checks it against. */
|
|
1139
|
+
function dashboardIR(dashboard: Json, extra: { warnings?: string[]; v2?: Json } = {}): TemplateIR {
|
|
1140
|
+
const { plan, edits, warnings } = planDashboard(dashboard);
|
|
1141
|
+
const metadata: DashboardResourceMetadata = { source: dashboard, edits, ...(extra.v2 ? { v2: extra.v2 } : {}) };
|
|
1142
|
+
const title = typeof dashboard.title === "string" ? dashboard.title : "dashboard";
|
|
1143
|
+
return {
|
|
1144
|
+
resources: [resource(logicalIdFor(title), DASHBOARD_RESOURCE_TYPE, plan, metadata as unknown as Json)],
|
|
1145
|
+
parameters: [],
|
|
1146
|
+
warnings: [...(extra.warnings ?? []), ...warnings],
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/** Parse dashboard JSON or a provisioning file into IR. */
|
|
1151
|
+
export function parseGrafana(content: string, options: GrafanaImportOptions = {}): TemplateIR {
|
|
1152
|
+
const data = content.trim() === "" ? undefined : parseDocument(content);
|
|
1153
|
+
if (!isObject(data)) {
|
|
1154
|
+
throw new Error("expected Grafana dashboard JSON or a provisioning file, as a JSON or YAML object");
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
if (looksLikeV2Dashboard(data)) {
|
|
1158
|
+
const { dashboard, warnings } = readV2Dashboard(data);
|
|
1159
|
+
const version = looksLikeDashboardResource(data) ? ` (${data.apiVersion})` : "";
|
|
1160
|
+
const note =
|
|
1161
|
+
`This is a v2 dashboard${version}. chant builds classic (v1) dashboard JSON, so it is read into the classic model the way Grafana reads it at v1` +
|
|
1162
|
+
(warnings.length > 0 ? "; what v2 holds that the classic model cannot is listed below." : ", and nothing v2-only is lost.");
|
|
1163
|
+
return dashboardIR(dashboard, { warnings: [note, ...warnings], v2: data });
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const lossy = lossyV1Read(data);
|
|
1167
|
+
if (lossy && !options.acceptLossyV1) {
|
|
1168
|
+
const why = lossy.failed
|
|
1169
|
+
? `Grafana could not convert it from ${lossy.storedVersion || "the version it is stored at"} (status.conversion.failed)`
|
|
1170
|
+
: `Grafana stores this dashboard as ${lossy.storedVersion} and converted it down to serve it at ${lossy.apiVersion}, dropping whatever v2 has and v1 does not (tabs, auto grids, conditional rendering), with nothing in the spec to show it`;
|
|
1171
|
+
return {
|
|
1172
|
+
resources: [],
|
|
1173
|
+
parameters: [],
|
|
1174
|
+
warnings: [
|
|
1175
|
+
`Not imported: ${why}. Read the dashboard at dashboard.grafana.app/v2 (or export it from the UI with the V2 Resource model) and import that; ` +
|
|
1176
|
+
"chant reads v2 and names everything it cannot carry. To import this down-converted copy anyway, parse it with acceptLossyV1.",
|
|
1177
|
+
],
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const dashboard: unknown = unwrapDashboard(data);
|
|
1182
|
+
if (looksLikeLegacyRowsDashboard(dashboard)) {
|
|
1183
|
+
return {
|
|
1184
|
+
resources: [],
|
|
1185
|
+
parameters: [],
|
|
1186
|
+
warnings: [
|
|
1187
|
+
`This dashboard was saved before Grafana 5.0 (schemaVersion ${String(dashboard.schemaVersion)}): its panels are inside a top-level "rows" list, ` +
|
|
1188
|
+
"which Grafana converts to a grid when it loads it and chant does not read. Import it into Grafana 11 or later, export it again, " +
|
|
1189
|
+
"and import that export. Nothing was imported.",
|
|
1190
|
+
],
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
if (looksLikeDashboard(dashboard)) {
|
|
1195
|
+
if (lossy) {
|
|
1196
|
+
return dashboardIR(dashboard, {
|
|
1197
|
+
warnings: [
|
|
1198
|
+
lossy.failed
|
|
1199
|
+
? `Grafana could not convert this dashboard from ${lossy.storedVersion || "the version it is stored at"} to ${lossy.apiVersion} (status.conversion.failed), so this copy may be incomplete. Imported anyway (acceptLossyV1).`
|
|
1200
|
+
: `Grafana stores this dashboard as ${lossy.storedVersion}, and this ${lossy.apiVersion} copy is a lossy down-conversion of it: ` +
|
|
1201
|
+
"whatever v2 has and v1 does not (tabs, auto grids, conditional rendering) is already gone. Imported anyway (acceptLossyV1).",
|
|
1202
|
+
],
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
return dashboardIR(dashboard);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (looksLikeDatasourceProvisioning(data)) {
|
|
1209
|
+
const { plan, warnings } = planDatasourceProvisioning(data as unknown as Json);
|
|
1210
|
+
return { resources: [resource("datasources", PROVISIONING_RESOURCE_TYPE, plan)], parameters: [], warnings };
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (looksLikeDashboardProvisioning(data)) {
|
|
1214
|
+
const { plan, warnings } = planDashboardProvisioning(data as unknown as Json);
|
|
1215
|
+
return { resources: [resource("dashboardProviders", PROVISIONING_RESOURCE_TYPE, plan)], parameters: [], warnings };
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (looksLikeAlertingProvisioning(data)) {
|
|
1219
|
+
const { plan, edits, warnings } = planAlertingProvisioning(data);
|
|
1220
|
+
const metadata: AlertingResourceMetadata = { source: data, edits };
|
|
1221
|
+
return { resources: [resource("alerting", PROVISIONING_RESOURCE_TYPE, plan, metadata as unknown as Json)], parameters: [], warnings };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
"this is not Grafana dashboard JSON (no panels list) or a provisioning file (no datasources, providers, or alerting groups, contactPoints, policies, muteTimes or templates list)",
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
/** The Grafana dashboard parser `chant import` runs. */
|
|
1230
|
+
export class GrafanaParser implements TemplateParser {
|
|
1231
|
+
constructor(private readonly options: GrafanaImportOptions = {}) {}
|
|
1232
|
+
|
|
1233
|
+
parse(content: string): TemplateIR {
|
|
1234
|
+
return parseGrafana(content, this.options);
|
|
1235
|
+
}
|
|
1236
|
+
}
|