@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,62 @@
|
|
|
1
|
+
/** How the importers report what they could not carry: warnings, and the edits that make the source equal the rebuilt output. */
|
|
2
|
+
|
|
3
|
+
import type { ImportEdit } from "./edits";
|
|
4
|
+
|
|
5
|
+
/** `a`, `a and b`, `a, b and c`. */
|
|
6
|
+
export function list(keys: string[]): string {
|
|
7
|
+
return keys.length === 1 ? keys[0] : `${keys.slice(0, -1).join(", ")} and ${keys[keys.length - 1]}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Collects edits, and warnings grouped by what they are about. */
|
|
11
|
+
export class Report {
|
|
12
|
+
readonly edits: ImportEdit[] = [];
|
|
13
|
+
private readonly headlines: string[] = [];
|
|
14
|
+
private readonly grouped = new Map<string, { subject: string; keys: string[]; why: string; kind: "drop" | "replace" }>();
|
|
15
|
+
|
|
16
|
+
/** A warning of its own. */
|
|
17
|
+
warn(message: string): void {
|
|
18
|
+
this.headlines.push(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `key` of `subject`, at `path`, is not carried. No warning when `why` is undefined (it is at Grafana's default). */
|
|
22
|
+
drop(path: string, subject: string, key: string, why?: string): void {
|
|
23
|
+
this.edits.push({ op: "remove", path });
|
|
24
|
+
if (why !== undefined) this.note("drop", subject, key, why);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The value at `path` is written as `value`. No warning when `why` is
|
|
29
|
+
* undefined (Grafana reads both the same); otherwise the warning reads
|
|
30
|
+
* `<subject>: <key> <why>`.
|
|
31
|
+
*/
|
|
32
|
+
replace(path: string, value: unknown, subject: string, key: string, why?: string): void {
|
|
33
|
+
this.edits.push({ op: "replace", path, value });
|
|
34
|
+
if (why !== undefined) this.note("replace", subject, key, why);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
edit(edit: ImportEdit): void {
|
|
38
|
+
this.edits.push(edit);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private note(kind: "drop" | "replace", subject: string, key: string, why: string): void {
|
|
42
|
+
const k = `${kind}\u0000${subject}\u0000${why}`;
|
|
43
|
+
const g = this.grouped.get(k) ?? { subject, keys: [], why, kind };
|
|
44
|
+
g.keys.push(key);
|
|
45
|
+
this.grouped.set(k, g);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
warnings(): string[] {
|
|
49
|
+
const out = [...this.headlines];
|
|
50
|
+
for (const g of this.grouped.values()) {
|
|
51
|
+
const n = g.keys.length;
|
|
52
|
+
const why = g.why === NO_PROP ? `(no prop takes ${n === 1 ? "it" : "them"})` : g.why;
|
|
53
|
+
if (n === 0 || g.keys[0] === "") out.push(`${g.subject} ${why}`);
|
|
54
|
+
else if (g.kind === "replace") out.push(`${g.subject}: ${list(g.keys)} ${why}`);
|
|
55
|
+
else out.push(`${g.subject}: ${list(g.keys)} ${n === 1 ? "is" : "are"} not carried ${why}`);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The `why` of a key no prop takes. */
|
|
62
|
+
export const NO_PROP = "(no prop takes it)";
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round trips through `chant import`.
|
|
3
|
+
*
|
|
4
|
+
* Dashboard JSON -> TypeScript -> `chant build` -> dashboard JSON must give
|
|
5
|
+
* back the same dashboard for every fixture: Grafana 12.4.11 and 13.2.2 UI
|
|
6
|
+
* exports (plain and "for sharing externally"), community dashboards from
|
|
7
|
+
* grafana.com, kube-prometheus's 33 dashboards, and what this lexicon's
|
|
8
|
+
* examples build. A v2 dashboard
|
|
9
|
+
* (#2947) round-trips through its classic form (./v2.ts). "The same" means
|
|
10
|
+
* equal after `normalizeDashboard` (Grafana's defaults and derived keys)
|
|
11
|
+
* once the importer's edits are applied to the source: every key it could
|
|
12
|
+
* not carry, and every value it wrote in another form. An edit that changes
|
|
13
|
+
* what Grafana does comes with an import warning, so the comparison is also
|
|
14
|
+
* the check that nothing was dropped silently.
|
|
15
|
+
*
|
|
16
|
+
* The generated source must lint clean (COR001, COR004 and COR009 among
|
|
17
|
+
* the rules), and the rebuilt dashboards must pass GRAF101-GRAF107 without
|
|
18
|
+
* errors. generated-types.e2e.test.ts type-checks the generated source.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, expect, test } from "vitest";
|
|
22
|
+
import { existsSync, readFileSync } from "fs";
|
|
23
|
+
import { join } from "path";
|
|
24
|
+
import { load } from "js-yaml";
|
|
25
|
+
import { build } from "@intentius/chant/build";
|
|
26
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
27
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
28
|
+
import { lintCommand } from "@intentius/chant/cli/commands/lint";
|
|
29
|
+
import { importCommand, importFromContent } from "@intentius/chant/cli/commands/import";
|
|
30
|
+
import { grafanaSerializer } from "../serializer";
|
|
31
|
+
import { validateGrafanaOutput, type GrafanaIssue } from "../validate-output";
|
|
32
|
+
import { buildGrafana, DATASOURCES_FILE, DASHBOARD_PROVIDERS_FILE } from "../build";
|
|
33
|
+
import { GrafanaParser, type DashboardResourceMetadata } from "./parser";
|
|
34
|
+
import { GrafanaGenerator } from "./generator";
|
|
35
|
+
import { applyEdits } from "./edits";
|
|
36
|
+
import { normalizeDashboard } from "./normalize";
|
|
37
|
+
import { COMMUNITY, KUBE_PROMETHEUS, LOSSY_V1_EXPORT, UI_EXPORTS, V2_EXPORTS, exampleOutputs, projectDir, read, removeDir, writeFiles } from "./testdata/fixtures";
|
|
38
|
+
|
|
39
|
+
type Json = Record<string, unknown>;
|
|
40
|
+
|
|
41
|
+
interface Imported {
|
|
42
|
+
/** Every generated file, concatenated with its path, for `toContain`. */
|
|
43
|
+
source: string;
|
|
44
|
+
paths: string[];
|
|
45
|
+
warnings: string[];
|
|
46
|
+
/** The source dashboard with the importer's edits applied. */
|
|
47
|
+
expected?: Json;
|
|
48
|
+
/** The rebuilt dashboard JSON text. */
|
|
49
|
+
text?: string;
|
|
50
|
+
rebuilt?: Json;
|
|
51
|
+
files: Record<string, string>;
|
|
52
|
+
buildErrors: unknown[];
|
|
53
|
+
issues: GrafanaIssue[];
|
|
54
|
+
lint: { errorCount: number; warningCount: number; output: string };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** GRAF101-GRAF107 over a build's grafana entities, as the post-synth checks run them: declared and external datasources included. */
|
|
58
|
+
function checks(entities: Map<string, Declarable>): GrafanaIssue[] {
|
|
59
|
+
const built = buildGrafana(new Map([...entities].filter(([, e]) => e.lexicon === "grafana")));
|
|
60
|
+
return validateGrafanaOutput({
|
|
61
|
+
dashboards: built.dashboards.map((d) => ({ source: d.file, json: d.json as unknown as Json })),
|
|
62
|
+
datasources: built.datasources,
|
|
63
|
+
externalDatasources: built.externalDatasources,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Dashboard JSON (or a provisioning file) -> IR -> TypeScript -> `chant build`, and `chant lint` over the source. */
|
|
68
|
+
async function importAndBuild(content: string): Promise<Imported> {
|
|
69
|
+
const ir = new GrafanaParser().parse(content);
|
|
70
|
+
const generated = new GrafanaGenerator().generate(ir);
|
|
71
|
+
const dir = projectDir();
|
|
72
|
+
try {
|
|
73
|
+
const srcDir = join(dir, "src");
|
|
74
|
+
writeFiles(srcDir, generated);
|
|
75
|
+
const result = await build(srcDir, [grafanaSerializer]);
|
|
76
|
+
const lint = await lintCommand({ path: srcDir, format: "stylish" });
|
|
77
|
+
const files = (result.outputs.get("grafana") as SerializerResult | undefined)?.files ?? {};
|
|
78
|
+
const dashboards = Object.entries(files).filter(([f]) => f.startsWith("dashboards/"));
|
|
79
|
+
const text = dashboards[0]?.[1];
|
|
80
|
+
const meta = ir.resources[0]?.metadata as unknown as DashboardResourceMetadata | undefined;
|
|
81
|
+
return {
|
|
82
|
+
source: generated.map((f) => `// ${f.path}\n${f.content}`).join("\n"),
|
|
83
|
+
paths: generated.map((f) => f.path),
|
|
84
|
+
warnings: ir.warnings ?? [],
|
|
85
|
+
expected: meta ? applyEdits(meta.source, meta.edits) : undefined,
|
|
86
|
+
text,
|
|
87
|
+
rebuilt: text === undefined ? undefined : (JSON.parse(text) as Json),
|
|
88
|
+
files,
|
|
89
|
+
buildErrors: result.errors,
|
|
90
|
+
issues: checks(result.entities),
|
|
91
|
+
lint: { errorCount: lint.errorCount, warningCount: lint.warningCount, output: lint.output },
|
|
92
|
+
};
|
|
93
|
+
} finally {
|
|
94
|
+
removeDir(dir);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A check error as `<code> <message>`, with the dashboard's name left out. */
|
|
99
|
+
const errorText = (i: GrafanaIssue): string => `${i.code} ${i.message.replace(/^Dashboard "[^"]*" /, "")}`;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Import, build, and expect the same dashboard back, lint-clean source and
|
|
103
|
+
* no check errors but `errors`: what a source dashboard has that the
|
|
104
|
+
* checks reject, carried as it is.
|
|
105
|
+
*/
|
|
106
|
+
async function expectRoundTrip(content: string, errors: readonly string[] = []): Promise<Imported> {
|
|
107
|
+
const out = await importAndBuild(content);
|
|
108
|
+
expect(out.buildErrors).toEqual([]);
|
|
109
|
+
expect(out.rebuilt).toBeDefined();
|
|
110
|
+
expect(normalizeDashboard(out.rebuilt!)).toEqual(normalizeDashboard(out.expected!));
|
|
111
|
+
expect(out.lint.errorCount + out.lint.warningCount, out.lint.output).toBe(0);
|
|
112
|
+
expect(out.issues.filter((i) => i.severity === "error").map(errorText)).toEqual(errors);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const NO_THRESHOLDS_MODE = (panel: number) => `GRAF107 /panels/${panel}/fieldConfig/defaults/thresholds: must have required property 'mode' (Grafana schema).`;
|
|
117
|
+
const NODES_ERRORS = [NO_THRESHOLDS_MODE(5), "GRAF107 /panels/8/transformations/1: must have required property 'options' (Grafana schema).", NO_THRESHOLDS_MODE(8)];
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What kube-prometheus's dashboards have that Grafana's CUE schema rejects
|
|
121
|
+
* (GRAF107), by fixture. The round trip carries each value as it is, so the
|
|
122
|
+
* rebuilt dashboard has the same errors as the source.
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* The import warnings for kube-prometheus's dashboards, by fixture; the rest
|
|
126
|
+
* import with none. The node dashboards' sortBy transformation has a
|
|
127
|
+
* `fields: {}` Grafana's sortBy transformer does not take (it reads only
|
|
128
|
+
* `sort`), so it is carried as it is through customTransformation().
|
|
129
|
+
*/
|
|
130
|
+
const SORT_BY_FIELDS = 'panel "Disk Space Usage" (id 9): transformation 6 is written with customTransformation(), untyped: the sortBy transformer takes no option "fields".';
|
|
131
|
+
const KUBE_PROMETHEUS_WARNINGS: Readonly<Record<string, readonly string[]>> = {
|
|
132
|
+
"kube-prometheus/nodes.json": [SORT_BY_FIELDS],
|
|
133
|
+
"kube-prometheus/nodes-aix.json": [SORT_BY_FIELDS],
|
|
134
|
+
"kube-prometheus/nodes-darwin.json": [SORT_BY_FIELDS],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const KUBE_PROMETHEUS_ERRORS: Readonly<Record<string, readonly string[]>> = {
|
|
138
|
+
"kube-prometheus/grafana-overview.json": ["GRAF107 /panels/2/options/footer/fields: must be array (Grafana schema)."],
|
|
139
|
+
"kube-prometheus/k8s-resources-pod.json": [
|
|
140
|
+
'GRAF107 /panels/1/fieldConfig/defaults/custom/axisColorMode: must be equal to one of the allowed values: "text", "series" (Grafana schema).',
|
|
141
|
+
],
|
|
142
|
+
"kube-prometheus/namespace-by-pod.json": [NO_THRESHOLDS_MODE(0), NO_THRESHOLDS_MODE(1)],
|
|
143
|
+
"kube-prometheus/pod-total.json": [NO_THRESHOLDS_MODE(0), NO_THRESHOLDS_MODE(1)],
|
|
144
|
+
"kube-prometheus/nodes.json": NODES_ERRORS,
|
|
145
|
+
"kube-prometheus/nodes-aix.json": NODES_ERRORS,
|
|
146
|
+
"kube-prometheus/nodes-darwin.json": NODES_ERRORS,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
describe("dashboard JSON -> TypeScript -> dashboard JSON", () => {
|
|
150
|
+
test("a custom variable in Grafana's `text : value` syntax, with an escaped comma and no current (#2944)", async () => {
|
|
151
|
+
const dashboard = {
|
|
152
|
+
uid: "kv",
|
|
153
|
+
title: "KV",
|
|
154
|
+
schemaVersion: 42,
|
|
155
|
+
panels: [],
|
|
156
|
+
templating: { list: [{ type: "custom", name: "env", query: "Production : prod,Staging : stg,a\\,b" }] },
|
|
157
|
+
};
|
|
158
|
+
const out = await expectRoundTrip(JSON.stringify(dashboard));
|
|
159
|
+
const env = (out.rebuilt!.templating as { list: Json[] }).list[0];
|
|
160
|
+
expect(env.query).toBe("Production : prod,Staging : stg,a\\,b");
|
|
161
|
+
expect(env.current).toEqual({ text: "Production", value: "prod" });
|
|
162
|
+
expect(env.options).toEqual([
|
|
163
|
+
{ selected: true, text: "Production", value: "prod" },
|
|
164
|
+
{ selected: false, text: "Staging", value: "stg" },
|
|
165
|
+
{ selected: false, text: "a,b", value: "a,b" },
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
for (const file of UI_EXPORTS) {
|
|
170
|
+
test(`Grafana UI export ${file}`, async () => {
|
|
171
|
+
const out = await expectRoundTrip(read(file));
|
|
172
|
+
// What Grafana exported passes every check once rebuilt, schema included. The one finding left is
|
|
173
|
+
// GRAF101 saying it cannot check a dashboard that names its datasources only through variables.
|
|
174
|
+
expect(out.issues.filter((i) => !(i.code === "GRAF101" && i.severity === "warning" && i.message.includes("cannot check")))).toEqual([]);
|
|
175
|
+
expect(out.paths.every((p) => p.startsWith("chant-fx-"))).toBe(true);
|
|
176
|
+
// The ad hoc filter and the annotation are named, not dropped silently.
|
|
177
|
+
if (file.includes("checkout")) {
|
|
178
|
+
expect(out.warnings).toContainEqual(expect.stringContaining('variable "Filters" (adhoc) is not carried'));
|
|
179
|
+
expect(out.warnings).toContainEqual(expect.stringContaining('the annotation "Deploys"'));
|
|
180
|
+
expect(out.source).toContain("repeat: env,");
|
|
181
|
+
expect(out.source).toContain("collapsed: true,");
|
|
182
|
+
} else if (file.includes("slo")) {
|
|
183
|
+
expect(out.warnings).toContainEqual(expect.stringMatching(/is a library panel \("Burn rate", uid chant-fx-burn\)/));
|
|
184
|
+
}
|
|
185
|
+
// A library panel, and the models an external export carries in __elements, are named.
|
|
186
|
+
const source = JSON.parse(read(file)) as Json;
|
|
187
|
+
const libraryPanels = (source.panels as Json[]).filter((p) => p.libraryPanel !== undefined).length;
|
|
188
|
+
expect(out.warnings.filter((w) => w.includes("is a library panel")).length).toBe(libraryPanels);
|
|
189
|
+
if (source.__elements && Object.keys(source.__elements as Json).length > 0) {
|
|
190
|
+
expect(out.warnings).toContain("dashboard: __elements is not carried (the library panels exported with it; library panels are not carried yet)");
|
|
191
|
+
}
|
|
192
|
+
if (file.includes(".external.")) {
|
|
193
|
+
expect(out.warnings).toContainEqual(expect.stringMatching(/^__inputs: DS_PROMETHEUS \(prometheus\)/));
|
|
194
|
+
expect(out.source).toContain("const dsPrometheus = new DatasourceVariable({");
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const file of V2_EXPORTS) {
|
|
200
|
+
test(`v2 dashboard ${file}, through its classic form`, async () => {
|
|
201
|
+
const out = await expectRoundTrip(read(file));
|
|
202
|
+
expect(out.paths.every((p) => /^chant-fx-(checkout|tabs)\//.test(p))).toBe(true);
|
|
203
|
+
expect(out.warnings[0]).toMatch(/^This is a v2 dashboard \(dashboard\.grafana\.app\/v2\)\./);
|
|
204
|
+
if (file.includes("tabs")) {
|
|
205
|
+
// Tabs become rows, and the auto grid's panels keep the positions Grafana gives them.
|
|
206
|
+
expect(out.warnings).toContainEqual(expect.stringMatching(/tabs "Overview" and "Details" become expanded rows/));
|
|
207
|
+
expect((out.rebuilt!.panels as Json[]).filter((p) => p.type === "row").map((p) => p.title)).toEqual(["Overview", "Details", "Latency ($env)", "Logs"]);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
test("datasources the dashboard names by uid become ExternalDatasources, and every check passes", async () => {
|
|
213
|
+
for (const file of ["exports/grafana-12.4.11/checkout.json", "exports/grafana-12.4.11/slo.json", "exports/grafana-13.2.2/slo.json"]) {
|
|
214
|
+
const out = await expectRoundTrip(read(file));
|
|
215
|
+
expect(out.issues, file).toEqual([]);
|
|
216
|
+
expect(out.source).toContain('const prom = new ExternalDatasource({ type: "prometheus", uid: "prom" });');
|
|
217
|
+
}
|
|
218
|
+
// The 12.4.11 export's datasource variable has prom selected: that is where the Prometheus uid comes from.
|
|
219
|
+
expect((await importAndBuild(read("exports/grafana-12.4.11/checkout.json"))).source).toContain('const loki = new ExternalDatasource({ type: "loki", uid: "loki" });');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("when a datasource variable's type is named by no uid, the uids stay plain refs and the warning says why", async () => {
|
|
223
|
+
const out = await expectRoundTrip(read("exports/grafana-13.2.2/checkout.json"));
|
|
224
|
+
expect(out.source).toContain('const loki: DatasourceRef<"loki"> = { type: "loki", uid: "loki" };');
|
|
225
|
+
expect(out.warnings).toContainEqual(expect.stringMatching(/^datasources: the dashboard names "loki" \(loki\) by uid, but no prometheus datasource, which \$datasource \(prometheus\) chooses among/));
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("an __inputs constant is filled in, and __inputs datasources become variables", async () => {
|
|
229
|
+
const out = await expectRoundTrip(read("exports/grafana-12.4.11/checkout.external.json"));
|
|
230
|
+
expect(out.warnings).toContain(
|
|
231
|
+
'__inputs: the constant VAR_SERVICE is written as its value "checkout", the value Grafana\'s import dialog fills in',
|
|
232
|
+
);
|
|
233
|
+
expect(out.source).toContain('const service = new ConstantVariable({ name: "service", skipUrlSync: true, value: "checkout" });');
|
|
234
|
+
// Every ${DS_LOKI} still resolves: the rebuilt dashboard declares it.
|
|
235
|
+
const list = (out.rebuilt!.templating as { list: Json[] }).list;
|
|
236
|
+
expect(list.slice(0, 2).map((v) => [v.name, v.type, v.query])).toEqual([
|
|
237
|
+
["DS_PROMETHEUS", "datasource", "prometheus"],
|
|
238
|
+
["DS_LOKI", "datasource", "loki"],
|
|
239
|
+
]);
|
|
240
|
+
expect(out.text).toContain('"uid": "${DS_LOKI}"');
|
|
241
|
+
expect(out.text).not.toContain("__inputs");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
for (const file of COMMUNITY) {
|
|
245
|
+
test(`grafana.com dashboard ${file}`, async () => {
|
|
246
|
+
const out = await expectRoundTrip(read(file));
|
|
247
|
+
// Every datasource reference resolved to a variable or a ref.
|
|
248
|
+
expect(out.warnings.join("\n")).not.toMatch(/: datasource is not carried/);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const file of KUBE_PROMETHEUS) {
|
|
253
|
+
test(`kube-prometheus dashboard ${file}`, async () => {
|
|
254
|
+
const out = await expectRoundTrip(read(file), KUBE_PROMETHEUS_ERRORS[file]);
|
|
255
|
+
expect(out.warnings).toEqual(KUBE_PROMETHEUS_WARNINGS[file] ?? []);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
test("kube-prometheus: a row keeps its line over an empty band, and over panels it overlaps (#2992)", async () => {
|
|
260
|
+
// Node Exporter / Nodes leaves lines 16 and 17 empty above its Disk row; Grafana closes the band when it draws the dashboard.
|
|
261
|
+
const nodes = await expectRoundTrip(read("kube-prometheus/nodes.json"), NODES_ERRORS);
|
|
262
|
+
expect(nodes.source).toMatch(/const diskRowGridPos: PropsOf<typeof Row>\["gridPos"\] = \{ y: 18 \};/);
|
|
263
|
+
// Prometheus / Remote-Write puts its Shards row on the line of the panel after it: the build keeps the overlap, and Grafana resolves it.
|
|
264
|
+
const remoteWrite = await expectRoundTrip(read("kube-prometheus/prometheus-remote-write.json"));
|
|
265
|
+
const shards = (remoteWrite.rebuilt!.panels as Json[]).filter((p) => p.title === "Shards" || p.title === "Current Shards");
|
|
266
|
+
expect(shards.map((p) => (p.gridPos as Json).y)).toEqual([16, 16]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("kube-prometheus: a gridPos without x is written with x 0, and a text panel keeps its Mixed datasource (#2992)", async () => {
|
|
270
|
+
const pv = await expectRoundTrip(read("kube-prometheus/persistentvolumesusage.json"));
|
|
271
|
+
expect((pv.rebuilt!.panels as Json[]).map((p) => (p.gridPos as Json).x)).toEqual([0, 18, 0, 18]);
|
|
272
|
+
const apiserver = await expectRoundTrip(read("kube-prometheus/apiserver.json"));
|
|
273
|
+
expect((apiserver.rebuilt!.panels as Json[])[0].datasource).toEqual({ type: "datasource", uid: "-- Mixed --" });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("Node Exporter Full keeps its 30-odd rows and every panel id", async () => {
|
|
277
|
+
const out = await expectRoundTrip(read("community/node-exporter-full.json"));
|
|
278
|
+
const source = JSON.parse(read("community/node-exporter-full.json")) as { panels: Json[] };
|
|
279
|
+
const ids = (panels: Json[]): unknown[] => panels.flatMap((p) => [p.id, ...ids((p.panels as Json[] | undefined) ?? [])]);
|
|
280
|
+
expect(ids(out.rebuilt!.panels as Json[])).toEqual(ids(source.panels));
|
|
281
|
+
// Split at eight declarables per module, a panel kept with its queries.
|
|
282
|
+
expect(out.paths.filter((p) => p.includes("/row-")).length).toBeGreaterThan(30);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("a panel type chant ships a class for is declared with it", async () => {
|
|
286
|
+
const out = await expectRoundTrip(read("community/traefik.json"));
|
|
287
|
+
expect(out.source).toMatch(/new PieChartPanel\(\{/);
|
|
288
|
+
expect(out.source).not.toContain("definePanel");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("every built-in panel type in a UI export is declared with its class", async () => {
|
|
292
|
+
const out = await expectRoundTrip(read("exports/grafana-12.4.11/panels.json"));
|
|
293
|
+
for (const cls of ["BarChartPanel", "BarGaugePanel", "PieChartPanel", "StateTimelinePanel", "StatusHistoryPanel", "HistogramPanel", "NodeGraphPanel", "XYChartPanel", "TrendPanel", "CanvasPanel", "GeomapPanel", "FlameGraphPanel", "AlertListPanel", "TracesPanel"]) {
|
|
294
|
+
expect(out.source).toMatch(new RegExp(`new ${cls}\\(\\{`));
|
|
295
|
+
}
|
|
296
|
+
expect(out.source).not.toContain("definePanel");
|
|
297
|
+
expect(out.warnings).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("a panel type chant has no class for goes through definePanel", async () => {
|
|
301
|
+
// The Traefik dashboard with its pie chart swapped for a community plugin chant does not ship.
|
|
302
|
+
const out = await expectRoundTrip(read("community/traefik.json").split('"type": "piechart"').join('"type": "grafana-polystat-panel"'));
|
|
303
|
+
expect(out.source).toContain("const GrafanaPolystatPanelPanel = definePanel()({");
|
|
304
|
+
expect(out.source).toContain('import { GrafanaPolystatPanelPanel } from "./plugins";');
|
|
305
|
+
expect(out.source).toMatch(/new GrafanaPolystatPanelPanel\(\{/);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("an AngularJS-era dashboard: schemaVersion carried, top-level panel settings named", async () => {
|
|
309
|
+
const out = await expectRoundTrip(read("community/prometheus-2-stats.json"));
|
|
310
|
+
expect(out.rebuilt!.schemaVersion).toBe(18);
|
|
311
|
+
expect(out.source).toContain("schemaVersion: 18,");
|
|
312
|
+
expect(out.warnings[1]).toMatch(/^13 panels are AngularJS panels \(graph, singlestat\) that keep their settings as top-level keys/);
|
|
313
|
+
expect(out.warnings).toContainEqual(expect.stringMatching(/^panel "WAL Corruptions" \(id 37\): colorBackground, .* are not carried \(no prop takes them\)$/));
|
|
314
|
+
// Its datasources are names ("${DS_PROMETHEUS}"), written as refs to the __inputs variable.
|
|
315
|
+
expect(out.text).toContain('"uid": "${DS_PROMETHEUS}"');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("the same dashboard after Grafana 12.4.11 migrated it imports with no warnings", async () => {
|
|
319
|
+
const out = await expectRoundTrip(read("community/prometheus-2-stats.grafana-12.4.11.json"));
|
|
320
|
+
expect(out.warnings).toEqual([]);
|
|
321
|
+
expect(out.source).toContain('const prom = new ExternalDatasource({ type: "prometheus", uid: "prom" });');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("what the examples build comes back as the same text", async () => {
|
|
325
|
+
const outputs = (await exampleOutputs()).filter(([f]) => /^[^/]+\/dashboards\/.*\.json$/.test(f));
|
|
326
|
+
expect(outputs.length).toBeGreaterThan(3);
|
|
327
|
+
for (const [name, text] of outputs) {
|
|
328
|
+
const out = await expectRoundTrip(text);
|
|
329
|
+
expect(out.warnings, name).toEqual([]);
|
|
330
|
+
expect(out.text, name).toBe(text);
|
|
331
|
+
}
|
|
332
|
+
}, 60_000);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("provisioning files", () => {
|
|
336
|
+
test("a datasource provisioning file becomes Datasources and builds back to the same file", async () => {
|
|
337
|
+
const [, yaml] = (await exampleOutputs()).find(([f]) => f === `getting-started/${DATASOURCES_FILE}`)!;
|
|
338
|
+
const out = await importAndBuild(yaml);
|
|
339
|
+
expect(out.warnings).toEqual([]);
|
|
340
|
+
expect(out.buildErrors).toEqual([]);
|
|
341
|
+
expect(out.source).toContain("const tempo = new Datasource({");
|
|
342
|
+
expect(load(out.files[DATASOURCES_FILE])).toEqual(load(yaml));
|
|
343
|
+
expect(out.lint.errorCount + out.lint.warningCount, out.lint.output).toBe(0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("a dashboard provisioning file becomes DashboardProviders", async () => {
|
|
347
|
+
const yaml = [
|
|
348
|
+
"apiVersion: 1",
|
|
349
|
+
"providers:",
|
|
350
|
+
" - name: platform",
|
|
351
|
+
" orgId: 1",
|
|
352
|
+
" folder: Platform",
|
|
353
|
+
" type: file",
|
|
354
|
+
" disableDeletion: true",
|
|
355
|
+
" allowUiUpdates: false",
|
|
356
|
+
" updateIntervalSeconds: 60",
|
|
357
|
+
" options:",
|
|
358
|
+
" path: /var/lib/grafana/dashboards/platform",
|
|
359
|
+
" foldersFromFilesStructure: false",
|
|
360
|
+
"",
|
|
361
|
+
].join("\n");
|
|
362
|
+
const out = await importAndBuild(yaml);
|
|
363
|
+
expect(out.warnings).toEqual([]);
|
|
364
|
+
expect(out.buildErrors).toEqual([]);
|
|
365
|
+
expect(out.source).toContain("new DashboardProvider({");
|
|
366
|
+
expect(load(out.files[DASHBOARD_PROVIDERS_FILE] ?? "")).toEqual(load(yaml));
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ── through core's import command ───────────────────────────────────
|
|
371
|
+
|
|
372
|
+
describe("chant import dashboard.json", () => {
|
|
373
|
+
for (const lexicon of [undefined, "grafana"]) {
|
|
374
|
+
test(lexicon ? "with --lexicon grafana" : "detected as a Grafana dashboard in a project that lists grafana", async () => {
|
|
375
|
+
const dir = projectDir();
|
|
376
|
+
try {
|
|
377
|
+
const templatePath = join(dir, "checkout.json");
|
|
378
|
+
const content = read("exports/grafana-13.2.2/checkout.external.json");
|
|
379
|
+
writeFiles(dir, [{ path: "checkout.json", content }]);
|
|
380
|
+
const output = join(dir, "src");
|
|
381
|
+
const result = await importCommand({ templatePath, output, force: true, lexicon });
|
|
382
|
+
expect(result.error).toBeUndefined();
|
|
383
|
+
expect(result.success).toBe(true);
|
|
384
|
+
expect(result.lexicon).toBe("grafana");
|
|
385
|
+
expect(result.generatedFiles).toEqual([
|
|
386
|
+
"chant-fx-checkout/variables.ts",
|
|
387
|
+
"chant-fx-checkout/panels.ts",
|
|
388
|
+
"chant-fx-checkout/row-details-for-job.ts",
|
|
389
|
+
"chant-fx-checkout/dashboard.ts",
|
|
390
|
+
]);
|
|
391
|
+
expect(result.warnings).toContainEqual(expect.stringContaining('variable "Filters" (adhoc) is not carried'));
|
|
392
|
+
const built = await build(output, [grafanaSerializer]);
|
|
393
|
+
expect(built.errors).toEqual([]);
|
|
394
|
+
const text = (built.outputs.get("grafana") as SerializerResult).files!["dashboards/chant-fx-checkout.json"];
|
|
395
|
+
const ir = new GrafanaParser().parse(content);
|
|
396
|
+
const meta = ir.resources[0].metadata as unknown as DashboardResourceMetadata;
|
|
397
|
+
expect(normalizeDashboard(JSON.parse(text))).toEqual(normalizeDashboard(applyEdits(meta.source, meta.edits)));
|
|
398
|
+
} finally {
|
|
399
|
+
removeDir(dir);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
test("a v2 dashboard is imported, and builds back to its classic form", async () => {
|
|
405
|
+
const dir = projectDir();
|
|
406
|
+
try {
|
|
407
|
+
const output = join(dir, "src");
|
|
408
|
+
const content = read(V2_EXPORTS[1]);
|
|
409
|
+
const result = await importFromContent({ content, lexicon: "grafana", output });
|
|
410
|
+
expect(result.error).toBeUndefined();
|
|
411
|
+
expect(result.generatedFiles).toContain("chant-fx-tabs/dashboard.ts");
|
|
412
|
+
expect(result.warnings[0]).toMatch(/^This is a v2 dashboard \(dashboard\.grafana\.app\/v2\)\./);
|
|
413
|
+
const built = await build(output, [grafanaSerializer]);
|
|
414
|
+
expect(built.errors).toEqual([]);
|
|
415
|
+
const text = (built.outputs.get("grafana") as SerializerResult).files!["dashboards/chant-fx-tabs.json"];
|
|
416
|
+
const meta = new GrafanaParser().parse(content).resources[0].metadata as unknown as DashboardResourceMetadata;
|
|
417
|
+
expect(normalizeDashboard(JSON.parse(text))).toEqual(normalizeDashboard(applyEdits(meta.source, meta.edits)));
|
|
418
|
+
} finally {
|
|
419
|
+
removeDir(dir);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("a v1 read of a dashboard Grafana stores as v2 is refused, and nothing is written", async () => {
|
|
424
|
+
const dir = projectDir();
|
|
425
|
+
try {
|
|
426
|
+
const output = join(dir, "src");
|
|
427
|
+
const result = await importFromContent({ content: read(LOSSY_V1_EXPORT), lexicon: "grafana", output });
|
|
428
|
+
expect(result.success).toBe(true);
|
|
429
|
+
expect(result.generatedFiles).toEqual([]);
|
|
430
|
+
expect(result.warnings).toEqual([expect.stringMatching(/^Not imported: Grafana stores this dashboard as v2 /)]);
|
|
431
|
+
expect(existsSync(join(output, "chant-fx-tabs"))).toBe(false);
|
|
432
|
+
} finally {
|
|
433
|
+
removeDir(dir);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("detection picks the v2 dashboard up too", async () => {
|
|
438
|
+
const dir = projectDir();
|
|
439
|
+
try {
|
|
440
|
+
const templatePath = join(dir, "checkout.v2.json");
|
|
441
|
+
writeFiles(dir, [{ path: "checkout.v2.json", content: read(V2_EXPORTS[0]) }]);
|
|
442
|
+
const result = await importCommand({ templatePath, output: join(dir, "src"), force: true });
|
|
443
|
+
expect(result.lexicon).toBe("grafana");
|
|
444
|
+
expect(result.warnings.join("\n")).toContain("v2 dashboard");
|
|
445
|
+
expect(result.generatedFiles).toContain("chant-fx-checkout/dashboard.ts");
|
|
446
|
+
} finally {
|
|
447
|
+
removeDir(dir);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("two dashboards imported into one project build together", async () => {
|
|
452
|
+
const dir = projectDir();
|
|
453
|
+
try {
|
|
454
|
+
const output = join(dir, "src");
|
|
455
|
+
for (const file of ["community/redis.json", "exports/grafana-13.2.2/slo.json"]) {
|
|
456
|
+
const result = await importFromContent({ content: read(file), lexicon: "grafana", output, force: true });
|
|
457
|
+
expect(result.success).toBe(true);
|
|
458
|
+
}
|
|
459
|
+
const built = await build(output, [grafanaSerializer]);
|
|
460
|
+
expect(built.errors).toEqual([]);
|
|
461
|
+
const files = Object.keys((built.outputs.get("grafana") as SerializerResult).files ?? {});
|
|
462
|
+
expect(files.filter((f) => f.startsWith("dashboards/")).sort()).toEqual(["dashboards/chant-fx-slo.json", "dashboards/e008bc3f-81a2-40f9-baf2-a33fd8dec7ec.json"]);
|
|
463
|
+
expect(readFileSync(join(output, "chant-fx-slo", "dashboard.ts"), "utf-8")).toContain('uid: "chant-fx-slo"');
|
|
464
|
+
} finally {
|
|
465
|
+
removeDir(dir);
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The import corpus, shared by the round-trip tests (roundtrip.test.ts) and
|
|
3
|
+
* the type-check of the generated source (generated-types.e2e.test.ts).
|
|
4
|
+
*
|
|
5
|
+
* - Grafana's own UI exports of two seed dashboards, from 12.4.11 and
|
|
6
|
+
* 13.2.2, plain and "for sharing externally" (test/fixtures/exports/,
|
|
7
|
+
* provenance in its README).
|
|
8
|
+
* - Community dashboards from grafana.com (test/fixtures/community/,
|
|
9
|
+
* provenance and licenses in its README).
|
|
10
|
+
* - kube-prometheus's 33 dashboards (test/fixtures/kube-prometheus/,
|
|
11
|
+
* provenance and license in its README).
|
|
12
|
+
* - Alerting provisioning files (test/fixtures/alerting/).
|
|
13
|
+
* - What this lexicon's examples build.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
16
|
+
import { dirname, join, resolve } from "path";
|
|
17
|
+
import { build } from "@intentius/chant/build";
|
|
18
|
+
import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
19
|
+
import { otelSerializer } from "@intentius/chant-lexicon-otel/serializer";
|
|
20
|
+
import { prometheusSerializer } from "@intentius/chant-lexicon-prometheus/serializer";
|
|
21
|
+
import { grafanaSerializer } from "../../serializer";
|
|
22
|
+
|
|
23
|
+
export const pkgDir = resolve(import.meta.dirname, "../../..");
|
|
24
|
+
export const repoRoot = resolve(pkgDir, "../..");
|
|
25
|
+
export const fixturesDir = join(pkgDir, "test", "fixtures");
|
|
26
|
+
|
|
27
|
+
/** A fixture file's text, by path under test/fixtures. */
|
|
28
|
+
export const read = (...p: string[]): string => readFileSync(join(fixturesDir, ...p), "utf-8");
|
|
29
|
+
|
|
30
|
+
/** The classic UI exports (every export but the v1 and v2 resources), as paths under test/fixtures. */
|
|
31
|
+
export const UI_EXPORTS: readonly string[] = ["grafana-12.4.11", "grafana-13.2.2"].flatMap((v) =>
|
|
32
|
+
readdirSync(join(fixturesDir, "exports", v))
|
|
33
|
+
.filter((f) => f.endsWith(".json") && !f.includes("-resource"))
|
|
34
|
+
.sort()
|
|
35
|
+
.map((f) => `exports/${v}/${f}`),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
/** v2 dashboards (#2947): the checkout V2 Resource UI export, and the tabs dashboard as the v2 API reads it. */
|
|
39
|
+
export const V2_EXPORTS: readonly string[] = ["exports/grafana-13.2.2/checkout.v2-resource.json", "exports/grafana-13.2.2/tabs.v2-resource.json"];
|
|
40
|
+
|
|
41
|
+
/** The tabs dashboard read at dashboard.grafana.app/v1: Grafana's lossy down-conversion of a dashboard it stores as v2. */
|
|
42
|
+
export const LOSSY_V1_EXPORT = "exports/grafana-13.2.2/tabs.v1-resource.json";
|
|
43
|
+
|
|
44
|
+
/** The grafana.com dashboards, as paths under test/fixtures. */
|
|
45
|
+
export const COMMUNITY: readonly string[] = [
|
|
46
|
+
"community/node-exporter-full.json",
|
|
47
|
+
"community/k8s-views-global.json",
|
|
48
|
+
"community/k8s-views-pods.json",
|
|
49
|
+
"community/traefik.json",
|
|
50
|
+
"community/redis.json",
|
|
51
|
+
"community/prometheus-2-stats.json",
|
|
52
|
+
"community/prometheus-2-stats.grafana-12.4.11.json",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** kube-prometheus v0.19.0's dashboards, one file per dashboard, as paths under test/fixtures. */
|
|
56
|
+
export const KUBE_PROMETHEUS: readonly string[] = readdirSync(join(fixturesDir, "kube-prometheus"))
|
|
57
|
+
.filter((f) => f.endsWith(".json"))
|
|
58
|
+
.sort()
|
|
59
|
+
.map((f) => `kube-prometheus/${f}`);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The alerting provisioning corpus (test/fixtures/alerting/, provenance and
|
|
63
|
+
* licenses in its README): Grafana 12.4.11 and 13.2.2 exports, and files
|
|
64
|
+
* from projects that provision Grafana alerting from files.
|
|
65
|
+
*/
|
|
66
|
+
export const ALERTING: readonly string[] = ["grafana-12.4.11", "grafana-13.2.2", "community"].flatMap((d) =>
|
|
67
|
+
readdirSync(join(fixturesDir, "alerting", d))
|
|
68
|
+
.filter((f) => /\.ya?ml$/.test(f))
|
|
69
|
+
.sort()
|
|
70
|
+
.map((f) => `alerting/${d}/${f}`),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
/** The example build roots and the serializers each needs. */
|
|
74
|
+
const EXAMPLES: ReadonlyArray<{ name: string; serializers: Serializer[] }> = [
|
|
75
|
+
{ name: "getting-started", serializers: [grafanaSerializer] },
|
|
76
|
+
{ name: "dashboards-from-declarations", serializers: [otelSerializer, prometheusSerializer, grafanaSerializer] },
|
|
77
|
+
{ name: "alerting", serializers: [prometheusSerializer, grafanaSerializer] },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/** Every file each example builds, keyed `<example>/<path>`. */
|
|
81
|
+
export async function exampleOutputs(): Promise<Array<[string, string]>> {
|
|
82
|
+
const out: Array<[string, string]> = [];
|
|
83
|
+
for (const { name, serializers } of EXAMPLES) {
|
|
84
|
+
const srcDir = join(pkgDir, "examples", name, "src");
|
|
85
|
+
if (!statSync(srcDir).isDirectory()) continue;
|
|
86
|
+
const result = await build(srcDir, serializers);
|
|
87
|
+
if (result.errors.length > 0) throw new Error(`${name}: ${result.errors.map(String).join("; ")}`);
|
|
88
|
+
const files = (result.outputs.get("grafana") as SerializerResult).files ?? {};
|
|
89
|
+
for (const [path, text] of Object.entries(files)) out.push([`${name}/${path}`, text]);
|
|
90
|
+
}
|
|
91
|
+
return out.sort(([a], [b]) => a.localeCompare(b));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A chant project directory inside the package, so the lexicon resolves as it does for a user. */
|
|
95
|
+
export function projectDir(): string {
|
|
96
|
+
const dir = mkdtempSync(join(pkgDir, ".roundtrip-tmp-"));
|
|
97
|
+
mkdirSync(join(dir, "src"));
|
|
98
|
+
writeFileSync(join(dir, "chant.config.ts"), 'export default { lexicons: ["grafana"] };\n');
|
|
99
|
+
writeFileSync(join(dir, "package.json"), '{ "name": "grafana-import-roundtrip", "private": true, "type": "module" }\n');
|
|
100
|
+
return dir;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Write generated files under `dir`, creating their directories. */
|
|
104
|
+
export function writeFiles(dir: string, files: ReadonlyArray<{ path: string; content: string }>): string[] {
|
|
105
|
+
const written: string[] = [];
|
|
106
|
+
for (const f of files) {
|
|
107
|
+
const path = join(dir, f.path);
|
|
108
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
109
|
+
writeFileSync(path, f.content);
|
|
110
|
+
written.push(path);
|
|
111
|
+
}
|
|
112
|
+
return written;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Remove a directory made by `projectDir`. */
|
|
116
|
+
export function removeDir(dir: string): void {
|
|
117
|
+
rmSync(dir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Every `.ts` file under a directory, recursively. */
|
|
121
|
+
export function tsFiles(dir: string): string[] {
|
|
122
|
+
const out: string[] = [];
|
|
123
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
124
|
+
const p = join(dir, e.name);
|
|
125
|
+
if (e.isDirectory()) out.push(...tsFiles(p));
|
|
126
|
+
else if (e.name.endsWith(".ts")) out.push(p);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|