diff --git a/.betterer.results b/.betterer.results index d7f730b52c0..fcb25c5f876 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3,14 +3,6 @@ // If this file contains merge conflicts, use `betterer merge` to automatically resolve them: // https://phenomnomnominal.github.io/betterer/docs/results-file/#merge // -exports[`no enzyme tests`] = { - value: `{ - "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:2976628669": [ - [0, 26, 13, "RegExp match", "2409514259"] - ] - }` -}; - exports[`better eslint`] = { value: `{ "e2e/benchmarks/live/4-20hz-panels.spec.ts:5381": [ @@ -277,8 +269,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "packages/grafana-data/src/geo/layer.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], "packages/grafana-data/src/panel/PanelPlugin.test.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -584,8 +575,7 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"], - [0, 0, 0, "Unexpected any. Specify a different type.", "6"] + [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], "packages/grafana-data/src/types/variables.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] @@ -993,10 +983,14 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "3"], [0, 0, 0, "Do not use any type assertions.", "4"] ], + "packages/grafana-schema/src/veneer/common.types.ts:5381": [ + [0, 0, 0, "Unexpected any. Specify a different type.", "0"] + ], "packages/grafana-schema/src/veneer/dashboard.types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"] + [0, 0, 0, "Unexpected any. Specify a different type.", "1"], + [0, 0, 0, "Do not use any type assertions.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"] ], "packages/grafana-toolkit/src/cli/tasks/component.create.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], @@ -1313,12 +1307,6 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], - "packages/grafana-ui/src/components/QueryField/QueryField.test.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"] - ], "packages/grafana-ui/src/components/QueryField/QueryField.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"] diff --git a/.betterer.ts b/.betterer.ts index af60ac38937..cf7e809107f 100644 --- a/.betterer.ts +++ b/.betterer.ts @@ -1,13 +1,10 @@ -import { regexp } from '@betterer/regexp'; import { BettererFileTest } from '@betterer/betterer'; import { ESLint, Linter } from 'eslint'; import { existsSync } from 'fs'; -import { exec } from 'child_process'; import path from 'path'; import glob from 'glob'; export default { - 'no enzyme tests': () => regexp(/from 'enzyme'/g).include('**/*.test.*'), 'better eslint': () => countEslintErrors().include('**/*.{ts,tsx}'), 'no undocumented stories': () => countUndocumentedStories().include('**/*.story.tsx'), }; diff --git a/.bingo/Variables.mk b/.bingo/Variables.mk index 1adc68d6ec1..257da6358d5 100644 --- a/.bingo/Variables.mk +++ b/.bingo/Variables.mk @@ -23,6 +23,12 @@ $(BRA): $(BINGO_DIR)/bra.mod @echo "(re)installing $(GOBIN)/bra-v0.0.0-20200517080246-1e3013ecaff8" @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=bra.mod -o=$(GOBIN)/bra-v0.0.0-20200517080246-1e3013ecaff8 "github.com/unknwon/bra" +CUE := $(GOBIN)/cue-v0.5.0-beta.2 +$(CUE): $(BINGO_DIR)/cue.mod + @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. + @echo "(re)installing $(GOBIN)/cue-v0.5.0-beta.2" + @cd $(BINGO_DIR) && GOWORK=off $(GO) build -mod=mod -modfile=cue.mod -o=$(GOBIN)/cue-v0.5.0-beta.2 "cuelang.org/go/cmd/cue" + DRONE := $(GOBIN)/drone-v1.5.0 $(DRONE): $(BINGO_DIR)/drone.mod @# Install binary/ries using Go 1.14+ build command. This is using bwplotka/bingo-controlled, separate go module with pinned dependencies. diff --git a/.bingo/cue.mod b/.bingo/cue.mod new file mode 100644 index 00000000000..79ccdfb391c --- /dev/null +++ b/.bingo/cue.mod @@ -0,0 +1,5 @@ +module _ // Auto generated by https://github.com/bwplotka/bingo. DO NOT EDIT + +go 1.19 + +require cuelang.org/go v0.5.0-beta.2 // cmd/cue diff --git a/.bingo/cue.sum b/.bingo/cue.sum new file mode 100644 index 00000000000..f59e3b21cb6 --- /dev/null +++ b/.bingo/cue.sum @@ -0,0 +1,30 @@ +cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= +cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI= +cuelang.org/go v0.5.0-beta.2 h1:am5M7jGvNTJ0rnjrFNyvE7fucL/wRqb0emK4XxdThQI= +cuelang.org/go v0.5.0-beta.2/go.mod h1:okjJBHFQFer+a41sAe2SaGm1glWS8oEb6CmJvn5Zdws= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd/v2 v2.0.1 h1:y1Rh3tEU89D+7Tgbw+lp52T6p/GJLpDmNvr10UWqLTE= +github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= +github.com/emicklei/proto v1.6.15 h1:XbpwxmuOPrdES97FrSfpyy67SSCV/wBIKXqgJzh6hNw= +github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de h1:D5x39vF5KCwKQaw+OC9ZPiLVHXz3UFw2+psEX+gYcto= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc h1:gSVONBi2HWMFXCa9jFdYvYk7IwW/mTLxWOF7rXS4LO0= +github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b h1:zd/2RNzIRkoGGMjE+YIsZ85CnDIz672JK2F3Zl4vux4= +github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= +golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/tools v0.0.0-20200612220849-54c614fe050c h1:g6oFfz6Cmw68izP3xsdud3Oxu145IPkeFzyRg58AKHM= +golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/.bingo/variables.env b/.bingo/variables.env index e58d95eca8b..7cb74529282 100644 --- a/.bingo/variables.env +++ b/.bingo/variables.env @@ -10,6 +10,8 @@ fi BRA="${GOBIN}/bra-v0.0.0-20200517080246-1e3013ecaff8" +CUE="${GOBIN}/cue-v0.5.0-beta.2" + DRONE="${GOBIN}/drone-v1.5.0" GOLANGCI_LINT="${GOBIN}/golangci-lint-v1.50.1" diff --git a/.vscode/launch.json b/.vscode/launch.json index a6b91b2f521..792addee950 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -27,6 +27,14 @@ "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "port": 9229 + }, + { + "name": "Debug Go test", + "type": "go", + "request": "launch", + "mode": "test", + "program": "${workspaceFolder}/${relativeFileDirname}", + "showLog": true } ] } diff --git a/Makefile b/Makefile index 07dba75646a..78cb6f15963 100644 --- a/Makefile +++ b/Makefile @@ -7,12 +7,13 @@ WIRE_TAGS = "oss" -include local/Makefile include .bingo/Variables.mk -.PHONY: all deps-go deps-js deps build-go build-backend build-server build-cli build-js build build-docker-full build-docker-full-ubuntu lint-go golangci-lint test-go test-js gen-ts test run run-frontend clean devenv devenv-down protobuf drone help gen-go gen-cue +.PHONY: all deps-go deps-js deps build-go build-backend build-server build-cli build-js build build-docker-full build-docker-full-ubuntu lint-go golangci-lint test-go test-js gen-ts test run run-frontend clean devenv devenv-down protobuf drone help gen-go gen-cue fix-cue GO = go GO_FILES ?= ./pkg/... SH_FILES ?= $(shell find ./scripts -name *.sh) GO_BUILD_FLAGS += $(if $(GO_BUILD_DEV),-dev) +GO_BUILD_FLAGS += $(if $(GO_BUILD_DEV),-dev) GO_BUILD_FLAGS += $(if $(GO_BUILD_TAGS),-build-tags=$(GO_BUILD_TAGS)) all: deps build @@ -75,6 +76,11 @@ gen-go: $(WIRE) gen-cue @echo "generate go files" $(WIRE) gen -tags $(WIRE_TAGS) ./pkg/server ./pkg/cmd/grafana-cli/runner +fix-cue: $(CUE) + @echo "formatting cue files" + $(CUE) fix kinds/**/*.cue + $(CUE) fix public/app/plugins/**/**/*.cue + gen-jsonnet: go generate ./devenv/jsonnet diff --git a/devenv/docker/blocks/tempo/docker-compose.yaml b/devenv/docker/blocks/tempo/docker-compose.yaml index 495a81e92aa..82915188e09 100644 --- a/devenv/docker/blocks/tempo/docker-compose.yaml +++ b/devenv/docker/blocks/tempo/docker-compose.yaml @@ -67,7 +67,7 @@ services: logging: *default-logging tempo: - image: grafana/tempo:latest + image: grafana/tempo:main-9a8474f command: - --config.file=/etc/tempo.yaml - --search.enabled=true diff --git a/devenv/docker/blocks/tempo/tempo.yaml b/devenv/docker/blocks/tempo/tempo.yaml index 5dfa486d345..168e6a3cc81 100644 --- a/devenv/docker/blocks/tempo/tempo.yaml +++ b/devenv/docker/blocks/tempo/tempo.yaml @@ -18,11 +18,6 @@ distributor: grpc: opencensus: -ingester: - trace_idle_period: 10s # the length of time after a trace has not received spans to consider it complete and flush it - max_block_bytes: 1_000_000 # cut the head block when it hits this size or ... - max_block_duration: 5m # this much time passes - compactor: compaction: compaction_window: 1h # blocks in this time window will be compacted together @@ -41,26 +36,19 @@ metrics_generator: - url: http://prometheus:9090/api/v1/write send_exemplars: true -query_frontend: - search: - max_duration: 0 # allow searches >1h - storage: trace: backend: local # backend configuration to use block: bloom_filter_false_positive: .05 # bloom filter false positive rate. lower values create larger filters but fewer false positives - index_downsample_bytes: 1000 # number of bytes per index record - encoding: zstd # block encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd, s2 + v2_index_downsample_bytes: 1000 # number of bytes per index record + v2_encoding: zstd # block encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd, s2 version: vParquet wal: path: /tmp/tempo/wal # where to store the the wal locally - encoding: snappy # wal encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd, s2 + v2_encoding: snappy # wal encoding/compression. options: none, gzip, lz4-64k, lz4-256k, lz4-1M, lz4, snappy, zstd, s2 local: path: /tmp/tempo/blocks - pool: - max_workers: 100 # worker pool determines the number of parallel requests to the object store backend - queue_depth: 10000 overrides: metrics_generator_processors: [service-graphs, span-metrics] diff --git a/docs/sources/datasources/graphite/template-variables/index.md b/docs/sources/datasources/graphite/template-variables/index.md index 36a8d51f66c..e62cb2d5ca3 100644 --- a/docs/sources/datasources/graphite/template-variables/index.md +++ b/docs/sources/datasources/graphite/template-variables/index.md @@ -21,6 +21,16 @@ Grafana refers to such variables as template variables. For an introduction to templating and template variables, refer to the [Templating]({{< relref "../../../dashboards/variables" >}}) and [Add and manage variables]({{< relref "../../../dashboards/variables/add-template-variables" >}}) documentation. +## Select a query type + +There are three query types for Graphite template variables + +| Query Type | Description | +| ----------------- | ------------------------------------------------------------------------------- | +| Default Query | Use functions such as `tags()`, `tag_values()`, `expand()` and metrics. | +| Value Query | Returns all the values for a query that includes a metric and function. | +| Metric Name Query | Returns all the names for a query that includes a metric and function. | + ## Use tag variables To create a variable using tag values, use the Grafana functions `tags` and `tag_values`. @@ -40,7 +50,7 @@ tag_values(server, server=~backend\*, app=~${apps:regex}) For details, refer to the [Graphite docs on the autocomplete API for tags](http://graphite.readthedocs.io/en/latest/tags.html#auto-complete-support). -### Use multi-valie variables in tag queries +### Use multi-value variables in tag queries Multi-value variables in tag queries use the advanced formatting syntax for variables introduced in Grafana v5.0: `{var:regex}`. Non-tag queries use the default glob formatting for multi-value variables. diff --git a/docs/sources/datasources/prometheus/template-variables/index.md b/docs/sources/datasources/prometheus/template-variables/index.md index 6e7fde54116..8b69205c235 100644 --- a/docs/sources/datasources/prometheus/template-variables/index.md +++ b/docs/sources/datasources/prometheus/template-variables/index.md @@ -99,7 +99,7 @@ For details, refer to the [Grafana blog](/blog/2020/09/28/new-in-grafana-7.2-__r The Prometheus data source supports two variable syntaxes for use in the **Query** field: -- `$`, for example `rate(http_requests_total{job=~"\$job"}[$_rate_interval])`, which is easier to read and write but does not allow you to use a variable in the middle of a word. +- `$`, for example `rate(http_requests_total{job=~"$job"}[$_rate_interval])`, which is easier to read and write but does not allow you to use a variable in the middle of a word. - `[[varname]]`, for example `rate(http_requests_total{job=~"[[job]]"}[$_rate_interval])` If you've enabled the _Multi-value_ or _Include all value_ options, Grafana converts the labels from plain text to a regex-compatible string, which requires you to use `=~` instead of `=`. diff --git a/docs/sources/developers/kinds/composable/alertgroupspanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/alertgroupspanelcfg/schema-reference.md new file mode 100644 index 00000000000..bc55a56dfd1 --- /dev/null +++ b/docs/sources/developers/kinds/composable/alertgroupspanelcfg/schema-reference.md @@ -0,0 +1,30 @@ +--- +keywords: + - grafana + - schema +title: AlertGroupsPanelCfg kind +--- +> Both documentation generation and kinds schemas are in active development and subject to change without prior notice. + +# AlertGroupsPanelCfg kind + +## Maturity: merged +## Version: 0.0 + +## Properties + +| Property | Type | Required | Description | +|----------------|-------------------------|----------|-------------| +| `PanelOptions` | [object](#paneloptions) | **Yes** | | + +## PanelOptions + +### Properties + +| Property | Type | Required | Description | +|----------------|---------|----------|-------------------------------------------------------------| +| `alertmanager` | string | **Yes** | Name of the alertmanager used as a source for alerts | +| `expandAll` | boolean | **Yes** | Expand all alert groups by default | +| `labels` | string | **Yes** | Comma-separated list of values used to filter alert results | + + diff --git a/docs/sources/developers/kinds/composable/annotationslistpanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/annotationslistpanelcfg/schema-reference.md index 6a45967ad19..3296b201a28 100644 --- a/docs/sources/developers/kinds/composable/annotationslistpanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/annotationslistpanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: AnnotationsListPanelCfg kind # AnnotationsListPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/barchartpanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/barchartpanelcfg/schema-reference.md index d16578d79f1..d31c28ea66f 100644 --- a/docs/sources/developers/kinds/composable/barchartpanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/barchartpanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: BarChartPanelCfg kind # BarChartPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md index b28705c548f..4abeb6078f1 100644 --- a/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/bargaugepanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: BarGaugePanelCfg kind # BarGaugePanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/dashboardlistpanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/dashboardlistpanelcfg/schema-reference.md index 865564346ce..1956a61ce2b 100644 --- a/docs/sources/developers/kinds/composable/dashboardlistpanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/dashboardlistpanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: DashboardListPanelCfg kind # DashboardListPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/gaugepanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/gaugepanelcfg/schema-reference.md index d2df553adfd..1f760818f46 100644 --- a/docs/sources/developers/kinds/composable/gaugepanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/gaugepanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: GaugePanelCfg kind # GaugePanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/histogrampanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/histogrampanelcfg/schema-reference.md index c4bf4075a5f..efb993406e1 100644 --- a/docs/sources/developers/kinds/composable/histogrampanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/histogrampanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: HistogramPanelCfg kind # HistogramPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/newspanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/newspanelcfg/schema-reference.md index 6a4832be146..b1b7c504862 100644 --- a/docs/sources/developers/kinds/composable/newspanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/newspanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: NewsPanelCfg kind # NewsPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/piechartpanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/piechartpanelcfg/schema-reference.md index 81eb2d26756..d051a2d3c72 100644 --- a/docs/sources/developers/kinds/composable/piechartpanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/piechartpanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: PieChartPanelCfg kind # PieChartPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/statpanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/statpanelcfg/schema-reference.md index 5ce03c0a60f..02fdd88a3ba 100644 --- a/docs/sources/developers/kinds/composable/statpanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/statpanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: StatPanelCfg kind # StatPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/composable/textpanelcfg/schema-reference.md b/docs/sources/developers/kinds/composable/textpanelcfg/schema-reference.md index d95a3ce689e..20b661a31d5 100644 --- a/docs/sources/developers/kinds/composable/textpanelcfg/schema-reference.md +++ b/docs/sources/developers/kinds/composable/textpanelcfg/schema-reference.md @@ -8,8 +8,8 @@ title: TextPanelCfg kind # TextPanelCfg kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/core/dashboard/schema-reference.md b/docs/sources/developers/kinds/core/dashboard/schema-reference.md index c4f68e8ca19..695d39fcc96 100644 --- a/docs/sources/developers/kinds/core/dashboard/schema-reference.md +++ b/docs/sources/developers/kinds/core/dashboard/schema-reference.md @@ -8,8 +8,8 @@ title: Dashboard kind # Dashboard kind -### Maturity: experimental -### Version: 0.0 +## Maturity: experimental +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/core/playlist/schema-reference.md b/docs/sources/developers/kinds/core/playlist/schema-reference.md index 165b8897acf..51698e99423 100644 --- a/docs/sources/developers/kinds/core/playlist/schema-reference.md +++ b/docs/sources/developers/kinds/core/playlist/schema-reference.md @@ -8,8 +8,8 @@ title: Playlist kind # Playlist kind -### Maturity: merged -### Version: 0.0 +## Maturity: merged +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/kinds/core/team/schema-reference.md b/docs/sources/developers/kinds/core/team/schema-reference.md index 9f1b8a2285f..e58402f70d4 100644 --- a/docs/sources/developers/kinds/core/team/schema-reference.md +++ b/docs/sources/developers/kinds/core/team/schema-reference.md @@ -8,8 +8,8 @@ title: Team kind # Team kind -### Maturity: merged -### Version: 0.0 +## Maturity: merged +## Version: 0.0 ## Properties diff --git a/docs/sources/developers/plugins/publish-a-plugin.md b/docs/sources/developers/plugins/publish-a-plugin.md index ca8092e53d1..3e3b51f3191 100644 --- a/docs/sources/developers/plugins/publish-a-plugin.md +++ b/docs/sources/developers/plugins/publish-a-plugin.md @@ -86,6 +86,7 @@ Before you submit your plugin, we ask that you read our guidelines and frequentl To speed up the time it takes to review your plugin: +- Get familiar with our plugin [publishing and signing criteria](publishing-and-signing-criteria.md) - Check that your plugin is ready for review using the [plugin validator](https://github.com/grafana/plugin-validator). - Read our [6 tips for improving your Grafana plugin before you publish](https://grafana.com/blog/2021/01/21/6-tips-for-improving-your-grafana-plugin-before-you-publish/). - Refer to [plugin-examples](https://github.com/grafana/grafana-plugin-examples) to review best practices for building your plugin. diff --git a/docs/sources/developers/plugins/publishing-and-signing-criteria.md b/docs/sources/developers/plugins/publishing-and-signing-criteria.md index 948ce45d10f..277ba504c2f 100644 --- a/docs/sources/developers/plugins/publishing-and-signing-criteria.md +++ b/docs/sources/developers/plugins/publishing-and-signing-criteria.md @@ -19,6 +19,9 @@ Grafana plugins must adhere to the following criteria when being reviewed for pu - Abuse: plugins should not perform actions beyond the scope of the intended use. - Do not include hidden files - Do not manipulate the underlying environment, privileges, or related processes +- Security: + - Should not access the filesystem + - Should not access environment variables ## Commercial diff --git a/docs/sources/explore/logs-integration.md b/docs/sources/explore/logs-integration.md index 323bbc312cc..c40fb5642a8 100644 --- a/docs/sources/explore/logs-integration.md +++ b/docs/sources/explore/logs-integration.md @@ -141,6 +141,10 @@ after switching to the Logs data source, the query changes to: This will return a chunk of logs in the selected time range that can be grepped/text searched. +### Logs sample + +If the selected data source implements logs sample, and supports both log and metric queries, then for metric queries you will be able to automatically see samples of log lines that contributed to visualized metrics. This feature is currently supported by Loki data sources. + #### Live tailing Use the Live tailing feature to see real-time logs on supported data sources. diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index c1fdca1d1c1..6dccb953693 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -46,6 +46,7 @@ Some stable features are enabled by default. You can disable a stable feature by | `datasourceLogger` | Logs all datasource requests | | `accessControlOnCall` | Access control primitives for OnCall | | `alertingNoNormalState` | Stop maintaining state of alerts that are not firing | +| `topNavCommandPalette` | Launch the Command Palette from the top navigation search box | ## Alpha feature toggles @@ -95,6 +96,7 @@ Alpha features might be changed or removed without prior notice. | `authnService` | Use new auth service to perform authentication | | `sessionRemoteCache` | Enable using remote cache for user sessions | | `alertingBacktesting` | Rule backtesting API for alerting | +| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files | | `azureMultipleResourcePicker` | Azure multiple resource picker | ## Development feature toggles diff --git a/docs/sources/setup-grafana/upgrade-grafana.md b/docs/sources/setup-grafana/upgrade-grafana.md index a379fd75a64..40297fc2d7e 100644 --- a/docs/sources/setup-grafana/upgrade-grafana.md +++ b/docs/sources/setup-grafana/upgrade-grafana.md @@ -13,33 +13,33 @@ weight: 500 # Upgrade Grafana -We recommend that you upgrade Grafana often to stay up to date with the latest fixes and enhancements. -In order to make this a reality, Grafana upgrades are backward compatible and the upgrade process is simple and quick. +We recommend that you upgrade Grafana often to stay current with the latest fixes and enhancements. +Because Grafana upgrades are backward compatible, the upgrade process is straightforward. Upgrading between many minor versions and one major version is generally safe, and dashboards and graphs will not change. -Upgrading between many minor versions and one major version is generally safe, and dashboards and graphs will look the same. -There might be minor breaking changes in some releases. -We outline these in the [What's New overviews]({{< relref "../whatsnew/" >}}) for each release. -For versions of Grafana prior to v9.2, we also published additional information in the [Release Notes]({{< relref "../release-notes/" >}}). -We also list all changes, with links to pull requests or issues when available, in the [Changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). +In addition to common tasks you should complete for all versions of Grafana, there might be additional upgrade tasks to complete for a version. -## Backup +> **Note:** There might be minor breaking changes in some releases. We outline these in the [What's New ]({{< relref "../whatsnew/" >}}) document for each release. -We recommend that you backup a few things in case you have to rollback the upgrade. +For versions of Grafana prior to v9.2, we published additional information in the [Release Notes]({{< relref "../release-notes/" >}}). -- Installed plugins - Back them up before you upgrade them in case you want to rollback the Grafana version and want to get the exact same versions you were running before the upgrade. -- Configuration files do not need to be backed up. However, you might want to in case you add new configuration options after upgrade and then rollback. +When available, we list all changes with links to pull requests or issues in the [Changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md). -### Database backup +> **Note:** When possible, we recommend that you test the Grafana upgrade process in a test or development environment. -Before upgrading it can be a good idea to backup your Grafana database. This will ensure that you can always rollback to your previous version. During startup, Grafana will automatically migrate the database schema (if there are changes or new tables). Sometimes this can cause issues if you later want to downgrade. +## Back up the Grafana database -#### sqlite +Although Grafana automatically upgrades the database on startup, we recommend that you back up your Grafana database so that you can roll back to a previous version, if required. -If you use sqlite you only need to make a backup of your `grafana.db` file. This is usually located at `/var/lib/grafana/grafana.db` on Unix systems. -If you are unsure what database you use and where it is stored check you grafana configuration file. If you -installed grafana to custom location using a binary tar/zip it is usually in `/data`. +### sqlite -#### mysql +If you use sqlite, you only need to back up the `grafana.db` file. On Unix systems, the database file is usually located in `/var/lib/grafana/`. + +If you are unsure which database you use and where it is stored, check the Grafana configuration file. If you +installed Grafana to a custom location using a binary tar/zip, the database is usually located in `/data`. + +### mysql + +To back up or restore a mysql Grafana database, run the following commands: ```bash backup: @@ -49,7 +49,9 @@ restore: > mysql -u root -p grafana < grafana_backup.sql ``` -#### postgres +### postgres + +To back up or restore a postgres Grafana database, run the following commands: ```bash backup: @@ -59,70 +61,117 @@ restore: > psql grafana < grafana_backup ``` -### Ubuntu or Debian +## Backup plugins -You can upgrade Grafana by following the same procedure as when you installed it. +We recommend that you back up installed plugins before you upgrade Grafana so that you can roll back to a previous version of Grafana, if necessary. -#### Upgrade Debian package +## Upgrade Grafana -If you installed Grafana by downloading a Debian package (`.deb`), then you can execute the same `dpkg -i` command but with the new package. It will upgrade your Grafana installation. +The following sections provide instructions for how to upgrade Grafana based on your installation method. -Go to the [download page](https://grafana.com/grafana/download?platform=linux) for the latest download -links. +### Debian -```bash -wget -sudo apt-get install -y adduser -sudo dpkg -i grafana__amd64.deb -``` +To upgrade Grafana installed from a Debian package (`.deb`), complete the following steps: -#### Upgrade from APT repository +1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. -If you installed Grafana from our APT repository, then Grafana will automatically update when you run apt-get upgrade to upgrade all system packages. + This enables you to upgrade Grafana without the risk of losing your configuration changes. -```bash -sudo apt-get update -sudo apt-get upgrade -``` +1. [Download](https://grafana.com/grafana/download?platform=linux) the latest version of Grafana. -#### Upgrade from binary .tar file +1. Execute the `dpkg -i` command. -If you downloaded the binary `.tar.gz` package, then you can just download and extract the new package and overwrite all your existing files. However, this might overwrite your config changes. + ```bash + wget + sudo apt-get install -y adduser + sudo dpkg -i grafana__amd64.deb + ``` -We recommend that you save your custom configuration changes in a file named `/conf/custom.ini`. -This allows you to upgrade Grafana without risking losing your configuration changes. +### APT repository -### Centos / RHEL +To upgrade Grafana installed from the Grafana Labs APT repository, complete the following steps: -If you installed Grafana by downloading an RPM package you can just follow the same installation guide and execute the same `yum install` or `rpm -i` command but with the new package. It will upgrade your Grafana installation. +1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. -If you used our YUM repository: + This enables you to upgrade Grafana without the risk of losing your configuration changes. -```bash -sudo yum update grafana -``` +1. Run the following command. + + ```bash + sudo apt-get update + sudo apt-get upgrade + ``` + +Grafana automatically updates when you run `apt-get upgrade`. + +### Binary .tar file + +To upgrade Grafana installed from the binary `.tar.gz` package, complete the following steps: + +1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. + + This enables you to upgrade Grafana without the risk of losing your configuration changes. + +1. [Download](https://grafana.com/grafana/download) the binary `.tar.gz` package. + +1. Extract the downloaded package and overwrite the existing files. + +### Centos or RHEL + +To upgrade Grafana running on Centos or RHEL, complete the following steps: + +1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. + + This enables you to upgrade Grafana without the risk of losing your configuration changes. + +1. Perform one of the following steps based on your installation. + + - If you [downloaded an RPM package](https://grafana.com/grafana/download) to install Grafana, then complete the steps documented in [Install on RPM-based Linux]({{< relref "./installation/rpm" >}}) to upgrade Grafana. + - If you used the Grafana YUM repository, execute the following command: + + ```bash + sudo yum update grafana + ``` ### Docker -This just an example, details depend on how you configured your grafana container. +To upgrade Grafana running in a Docker container, complete the following steps: -```bash -docker pull grafana/grafana -docker stop my-grafana-container -docker rm my-grafana-container -docker run -d --name=my-grafana-container --restart=always -v /var/lib/grafana:/var/lib/grafana grafana/grafana -``` +1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. + + This enables you to upgrade Grafana without the risk of losing your configuration changes. + +1. Run a command similar to the following command. + + > **Note:** This is an example. The parameters you enter depend on how you configured your Grafana container. + + ```bash + docker pull grafana/grafana + docker stop my-grafana-container + docker rm my-grafana-container + docker run -d --name=my-grafana-container --restart=always -v /var/lib/grafana:/var/lib/grafana grafana/grafana + ``` ### Windows -If you downloaded the Windows binary package you can just download a newer package and extract to the same location (and overwrite the existing files). This might overwrite your configuration changes. We recommend that you save your configuration changes in a file named `/conf/custom.ini` as this will make upgrades easier without risking losing your configuration changes. +To upgrade Grafana installed on Windows, complete the following steps: -## Update plugins +1. In your current installation of Grafana, save your custom configuration changes to a file named `/conf/custom.ini`. -After you have upgraded, we strongly recommend that you update all your plugins as a new version of Grafana + This enables you to upgrade Grafana without the risk of losing your configuration changes. + +1. [Download](https://grafana.com/grafana/download) the Windows binary package. + +1. Extract the contents of the package to the location in which you installed Grafana. + + You can overwrite existing files and folders, when prompted. + +## Update Grafana plugins + +After you upgrade Grafana, we recommend that you update all plugins because a new version of Grafana can make older plugins stop working properly. -You can update all plugins using +Run the following command to update plugins: ```bash grafana-cli plugins update-all diff --git a/go.mod b/go.mod index b5ae8b4808b..85d53c9f914 100644 --- a/go.mod +++ b/go.mod @@ -277,6 +277,7 @@ require ( github.com/Masterminds/semver/v3 v3.1.1 github.com/dave/dst v0.27.2 github.com/grafana/thema v0.0.0-20230122235053-b4b6714dd1c9 + github.com/hmarr/codeowners v1.1.1 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f k8s.io/apimachinery v0.25.3 ) diff --git a/go.sum b/go.sum index 27ca07ac835..4cd7978beac 100644 --- a/go.sum +++ b/go.sum @@ -1563,6 +1563,8 @@ github.com/hetznercloud/hcloud-go v1.26.2/go.mod h1:2C5uMtBiMoFr3m7lBFPf7wXTdh33 github.com/hetznercloud/hcloud-go v1.32.0/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME= github.com/hetznercloud/hcloud-go v1.33.2 h1:ptWKVYLW7YtjXzsqTFKFxwpVo3iM9UMkVPBYQE4teLU= github.com/hetznercloud/hcloud-go v1.33.2/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME= +github.com/hmarr/codeowners v1.1.1 h1:a1eD22I36JYH6O/zb+R+JkJgQ3CSlbu6H9EserOojSw= +github.com/hmarr/codeowners v1.1.1/go.mod h1:+ez+YARvfVhzL1MzY0f2+D/VusjODs4iRj3tO/IxBMw= github.com/hodgesds/perf-utils v0.0.8/go.mod h1:F6TfvsbtrF88i++hou29dTXlI2sfsJv+gRZDtmTJkAs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= diff --git a/jest.config.js b/jest.config.js index f71574e2db9..7db7afdf68c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -20,7 +20,6 @@ module.exports = { testTimeout: 30000, resolver: `/public/test/jest-resolver.js`, setupFilesAfterEnv: ['./public/test/setupTests.ts'], - snapshotSerializers: ['enzyme-to-json/serializer'], globals: { __webpack_public_path__: '', // empty string }, diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 62a905c3dea..74a478b786d 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -8,7 +8,8 @@ maturity: "experimental" lineage: seqs: [ { schemas: [ - {// 0.0 + // 0.0 + { @grafana(TSVeneer="type") // Unique numeric identifier for the dashboard. @@ -413,7 +414,7 @@ lineage: seqs: [ #MatcherConfig: { id: string | *"" @grafanamaturity(NeedsExpertReview) options?: _ @grafanamaturity(NeedsExpertReview) - } @cuetsy(kind="interface") + } @cuetsy(kind="interface") @grafana(TSVeneer="type") #DynamicConfigValue: { id: string | *"" @grafanamaturity(NeedsExpertReview) @@ -506,6 +507,7 @@ lineage: seqs: [ } ... } @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview) + #HeatmapPanel: { type: "heatmap" @grafanamaturity(NeedsExpertReview) ... diff --git a/kinds/playlist/playlist_kind.cue b/kinds/playlist/playlist_kind.cue index 6b3865b9d85..2dbb7ed2942 100644 --- a/kinds/playlist/playlist_kind.cue +++ b/kinds/playlist/playlist_kind.cue @@ -1,12 +1,13 @@ package kind -name: "Playlist" +name: "Playlist" maturity: "merged" lineage: seqs: [ { schemas: [ - {//0.0 + //0.0 + { // Unique playlist identifier. Generated on creation, either by the // creator of the playlist of by the application. uid: string @@ -39,9 +40,9 @@ lineage: seqs: [ value: string // Title is an unused property -- it will be removed in the future - title?: string + title?: string } @cuetsy(kind="interface") - } + }, ] - } + }, ] diff --git a/kinds/team/team_kind.cue b/kinds/team/team_kind.cue index a7214bd2082..73cbc9b761d 100644 --- a/kinds/team/team_kind.cue +++ b/kinds/team/team_kind.cue @@ -1,6 +1,6 @@ package kind -name: "Team" +name: "Team" maturity: "merged" lineage: seqs: [ @@ -21,7 +21,9 @@ lineage: seqs: [ // TODO - it seems it's a team_member.permission, unlikely it should belong to the team kind permission: #Permission @grafanamaturity(ToMetadata="kind", MaybeRemove) // AccessControl metadata associated with a given resource. - accessControl?: [string]: bool @grafanamaturity(ToMetadata="sys") + accessControl?: { + [string]: bool @grafanamaturity(ToMetadata="sys") + } // Created indicates when the team was created. created: int64 @grafanamaturity(ToMetadata="sys") // Updated indicates when the team was updated. diff --git a/package.json b/package.json index 9e52e930895..99c2f4ef579 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,12 @@ ], "*pkg/**/*.go": [ "gofmt -w -s" + ], + "*kinds/**/*.cue": [ + "make fix-cue" + ], + "*public/app/plugins/**/**/*.cue": [ + "make fix-cue" ] }, "devDependencies": { @@ -117,8 +123,6 @@ "@types/d3-force": "^2.1.0", "@types/d3-scale-chromatic": "1.3.1", "@types/debounce-promise": "3.1.5", - "@types/enzyme": "3.10.12", - "@types/enzyme-adapter-react-16": "1.0.6", "@types/eslint": "8.4.9", "@types/file-saver": "2.0.5", "@types/glob": "^8.0.0", @@ -161,7 +165,6 @@ "@types/uuid": "8.3.4", "@typescript-eslint/eslint-plugin": "5.42.0", "@typescript-eslint/parser": "5.42.0", - "@wojtekmaj/enzyme-adapter-react-17": "0.8.0", "autoprefixer": "10.4.13", "babel-jest": "29.3.1", "babel-loader": "9.1.0", @@ -173,8 +176,6 @@ "css-loader": "6.7.1", "css-minimizer-webpack-plugin": "4.2.2", "cypress": "9.5.1", - "enzyme": "3.11.0", - "enzyme-to-json": "3.6.2", "esbuild": "0.16.17", "esbuild-loader": "2.21.0", "esbuild-plugin-browserslist": "^0.6.0", @@ -403,7 +404,8 @@ "uuid": "9.0.0", "vendor": "link:./public/vendor", "visjs-network": "4.25.0", - "whatwg-fetch": "3.6.2" + "whatwg-fetch": "3.6.2", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.19.1/xlsx-0.19.1.tgz" }, "resolutions": { "underscore": "1.13.6", diff --git a/packages/grafana-data/src/geo/layer.ts b/packages/grafana-data/src/geo/layer.ts index 6859174ef7c..4ad67866fbb 100644 --- a/packages/grafana-data/src/geo/layer.ts +++ b/packages/grafana-data/src/geo/layer.ts @@ -2,74 +2,23 @@ import { Map as OpenLayersMap } from 'ol'; import BaseLayer from 'ol/layer/Base'; import { ReactNode } from 'react'; +import { MapLayerOptions, FrameGeometrySourceMode } from '@grafana/schema'; + import { EventBus } from '../events'; import { GrafanaTheme2 } from '../themes'; -import { MatcherConfig, PanelData } from '../types'; +import { PanelData } from '../types'; import { PanelOptionsEditorBuilder } from '../utils'; import { RegistryItemWithOptions } from '../utils/Registry'; /** - * @alpha + * @deprecated use the type from schema */ -export enum FrameGeometrySourceMode { - Auto = 'auto', // Will scan fields and find best match - Geohash = 'geohash', - Coords = 'coords', // lon field, lat field - Lookup = 'lookup', // keys > location - // H3 = 'h3', - // WKT = 'wkt, - // geojson? geometry text -} +export { FrameGeometrySourceMode }; /** - * @alpha + * @deprecated use the type from schema */ -export interface FrameGeometrySource { - mode: FrameGeometrySourceMode; - - // Field mappings - geohash?: string; - latitude?: string; - longitude?: string; - h3?: string; - wkt?: string; - lookup?: string; - - // Path to Gazetteer - gazetteer?: string; -} - -/** - * This gets saved in panel json - * - * depending on the type, it may have additional config - * - * This exists in `grafana/data` so the types are well known and extendable but the - * layout/frame is control by the map panel - * - * @alpha - */ -export interface MapLayerOptions { - type: string; - name: string; // configured unique display name - - // Custom options depending on the type - config?: TConfig; - - // Common method to define geometry fields - location?: FrameGeometrySource; - - // Defines which data query refId is associated with the layer - filterData?: MatcherConfig; - - // Common properties: - // https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html - // Layer opacity (0-1) - opacity?: number; - - // Check tooltip (defaults to true) - tooltip?: boolean; -} +export type { FrameGeometrySource, MapLayerOptions } from '@grafana/schema'; /** * @alpha @@ -81,7 +30,7 @@ export interface MapLayerHandler { */ update?: (data: PanelData) => void; - /** Optional callback to cleaup before getting removed */ + /** Optional callback for cleanup before getting removed */ dispose?: () => void; /** return react node for the legend */ diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index 3063eaa7224..00bf7ae113f 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -38,7 +38,7 @@ export type PreferredVisualisationType = typeof preferredVisualizationTypes[numb export interface QueryResultMeta { type?: DataFrameType; - /** DatasSource Specific Values */ + /** DataSource Specific Values */ custom?: Record; /** Stats */ diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index e19fb2fe59f..64b10278f96 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -87,6 +87,8 @@ export interface FeatureToggles { sessionRemoteCache?: boolean; disablePrometheusExemplarSampling?: boolean; alertingBacktesting?: boolean; + editPanelCSVDragAndDrop?: boolean; alertingNoNormalState?: boolean; azureMultipleResourcePicker?: boolean; + topNavCommandPalette?: boolean; } diff --git a/packages/grafana-data/src/types/transformations.ts b/packages/grafana-data/src/types/transformations.ts index 017a72bfc49..270ec1cbdda 100644 --- a/packages/grafana-data/src/types/transformations.ts +++ b/packages/grafana-data/src/types/transformations.ts @@ -1,3 +1,4 @@ +export type { MatcherConfig } from '@grafana/schema'; import { MonoTypeOperatorFunction } from 'rxjs'; import { RegistryItemWithOptions } from '../utils/Registry'; @@ -80,10 +81,6 @@ export interface ValueMatcherInfo extends RegistryItemWithOption isApplicable: (field: Field) => boolean; getDefaultOptions: (field: Field) => TOptions; } -export interface MatcherConfig { - id: string; - options?: TOptions; -} /** * @public diff --git a/packages/grafana-schema/src/common/common.gen.ts b/packages/grafana-schema/src/common/common.gen.ts index ab3f4bae139..9557cee3648 100644 --- a/packages/grafana-schema/src/common/common.gen.ts +++ b/packages/grafana-schema/src/common/common.gen.ts @@ -8,6 +8,17 @@ // Run 'make gen-cue' from repository root to regenerate. +/** + * TODO docs + */ +export interface DataSourceJsonData { + alertmanagerUid?: string; + authType?: string; + defaultRegion?: string; + manageAlerts?: boolean; + profile?: string; +} + /** * These are the common properties available to all queries in all datasources. * Specific implementations will *extend* this interface, adding the required @@ -40,6 +51,43 @@ export interface DataQuery { refId: string; } +export interface MapLayerOptions { + /** + * Custom options depending on the type + */ + config?: unknown; + /** + * Defines a frame MatcherConfig that may filter data for the given layer + */ + filterData?: unknown; + /** + * Common method to define geometry fields + */ + location?: FrameGeometrySource; + /** + * configured unique display name + */ + name: string; + /** + * Common properties: + * https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html + * Layer opacity (0-1) + */ + opacity?: number; + /** + * Check tooltip (defaults to true) + */ + tooltip?: boolean; + type: string; +} + +export enum FrameGeometrySourceMode { + Auto = 'auto', + Coords = 'coords', + Geohash = 'geohash', + Lookup = 'lookup', +} + /** * TODO docs */ @@ -487,6 +535,14 @@ export enum BarGaugeDisplayMode { Lcd = 'lcd', } +/** + * TODO docs + */ +export interface VizTooltipOptions { + mode: TooltipDisplayMode; + sort: SortOrder; +} + /** * Internally, this is the "type" of cell that's being displayed * in the table such as colored text, JSON, gauge, etc. @@ -595,14 +651,24 @@ export interface DataSourceRef { uid?: string; } -/** - * TODO docs - */ -export interface VizTooltipOptions { - mode: TooltipDisplayMode; - sort: SortOrder; +export interface FrameGeometrySource { + /** + * Path to Gazetteer + */ + gazetteer?: string; + /** + * Field mappings + */ + geohash?: string; + latitude?: string; + longitude?: string; + lookup?: string; + mode: FrameGeometrySourceMode; + wkt?: string; } +export interface Labels {} + /** * Field options for each field within a table (e.g 10, "The String", 64.20, etc.) * Generally defines alignment, filtering capabilties, display options, etc. diff --git a/packages/grafana-schema/src/common/data_source.cue b/packages/grafana-schema/src/common/data_source.cue new file mode 100644 index 00000000000..c74c9892fb6 --- /dev/null +++ b/packages/grafana-schema/src/common/data_source.cue @@ -0,0 +1,10 @@ +package common + +// TODO docs +DataSourceJsonData: { + authType?: string + defaultRegion?: string + profile?: string + manageAlerts?: bool + alertmanagerUid?: string +} @cuetsy(kind="interface") diff --git a/packages/grafana-schema/src/common/geo.cue b/packages/grafana-schema/src/common/geo.cue new file mode 100644 index 00000000000..cd17654f8f9 --- /dev/null +++ b/packages/grafana-schema/src/common/geo.cue @@ -0,0 +1,33 @@ +package common + +MapLayerOptions: { + type: string + // configured unique display name + name: string + // Custom options depending on the type + config?: _ + // Common method to define geometry fields + location?: FrameGeometrySource + // Defines a frame MatcherConfig that may filter data for the given layer + filterData?: _ + // Common properties: + // https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html + // Layer opacity (0-1) + opacity?: int64 + // Check tooltip (defaults to true) + tooltip?: bool +} @cuetsy(kind="interface") @grafana(TSVeneer="type") + +FrameGeometrySourceMode: "auto" | "geohash" |"coords" | "lookup" @cuetsy(kind="enum",memberNames="Auto|Geohash|Coords|Lookup") + +FrameGeometrySource: { + mode: FrameGeometrySourceMode + // Field mappings + geohash?: string + latitude?: string + longitude?: string + wkt?: string + lookup?: string + // Path to Gazetteer + gazetteer?: string +} @cuetsy(kind="interface") diff --git a/packages/grafana-schema/src/common/mudball.cue b/packages/grafana-schema/src/common/mudball.cue index ad648cccb68..537c6549570 100644 --- a/packages/grafana-schema/src/common/mudball.cue +++ b/packages/grafana-schema/src/common/mudball.cue @@ -1,7 +1,5 @@ package common -// TODO break this up into individual files. Current limitation on this is codegen logic, imports, dependencies - // TODO docs AxisPlacement: "auto" | "top" | "right" | "bottom" | "left" | "hidden" @cuetsy(kind="enum") @@ -247,4 +245,6 @@ VizTooltipOptions: { sort: SortOrder } @cuetsy(kind="interface") - +Labels: { + [string]: string +} @cuetsy(kind="interface") diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index 2fac4368b56..9fdf476a0e4 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -26,7 +26,6 @@ export type { SpecialValueMap, ValueMappingResult, Transformation, - MatcherConfig, RowPanel, GraphPanel, HeatmapPanel @@ -46,7 +45,6 @@ export { SpecialValueMatch, DashboardCursorSync, defaultDashboardCursorSync, - defaultMatcherConfig, defaultRowPanel } from './raw/dashboard/x/dashboard_types.gen'; @@ -65,6 +63,7 @@ export type { DataSourceRef, Panel, FieldConfigSource, + MatcherConfig, FieldConfig } from './veneer/dashboard.types'; @@ -83,6 +82,7 @@ export { VariableHide, defaultPanel, defaultFieldConfigSource, + defaultMatcherConfig, defaultFieldConfig } from './veneer/dashboard.types'; diff --git a/packages/grafana-schema/src/veneer/common.types.ts b/packages/grafana-schema/src/veneer/common.types.ts index cf65607d001..1e35686cbbe 100644 --- a/packages/grafana-schema/src/veneer/common.types.ts +++ b/packages/grafana-schema/src/veneer/common.types.ts @@ -1,5 +1,13 @@ import * as raw from '../common/common.gen'; +import { MatcherConfig } from './dashboard.types'; + +export interface MapLayerOptions extends raw.MapLayerOptions { + // Custom options depending on the type + config?: TConfig; + filterData?: MatcherConfig; +} + export interface DataQuery extends raw.DataQuery { // TODO remove explicit nulls datasource?: raw.DataSourceRef | null; diff --git a/packages/grafana-schema/src/veneer/dashboard.types.ts b/packages/grafana-schema/src/veneer/dashboard.types.ts index 53b1d22921c..bab4aa4450d 100644 --- a/packages/grafana-schema/src/veneer/dashboard.types.ts +++ b/packages/grafana-schema/src/veneer/dashboard.types.ts @@ -43,6 +43,10 @@ export interface FieldConfigSource> extends r defaults: FieldConfig; } +export interface MatcherConfig extends raw.MatcherConfig { + options?: TConfig; +} + export const defaultDashboard = raw.defaultDashboard as Dashboard; export const defaultVariableModel = { ...raw.defaultVariableModel, @@ -60,3 +64,4 @@ export const defaultVariableModel = { export const defaultPanel: Partial = raw.defaultPanel; export const defaultFieldConfig: Partial = raw.defaultFieldConfig; export const defaultFieldConfigSource: Partial = raw.defaultFieldConfigSource; +export const defaultMatcherConfig: Partial = raw.defaultMatcherConfig; diff --git a/packages/grafana-toolkit/README.md b/packages/grafana-toolkit/README.md index 8b853ac6b41..800492fa133 100644 --- a/packages/grafana-toolkit/README.md +++ b/packages/grafana-toolkit/README.md @@ -146,14 +146,7 @@ Yes! grafana-toolkit supports TypeScript by default. grafana-toolkit comes with Jest as a test runner. -Internally at Grafana we use Enzyme. If you are developing React plugin and you want to configure Enzyme as a testing utility, then you need to configure `enzyme-adapter-react`. To do so, create `/config/jest-setup.ts` file that will provide necessary setup. Copy the following code into that file to get Enzyme working with React: - -```ts -import { configure } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; - -configure({ adapter: new Adapter() }); -``` +Internally at Grafana we use React Testing Library. You can also set up Jest with shims of your needs by creating `jest-shim.ts` file in the same directory: `/config/jest-shim.ts` diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 20678316f77..dc219b691c4 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -141,8 +141,6 @@ "@testing-library/user-event": "14.4.3", "@types/common-tags": "^1.8.0", "@types/d3": "7.4.0", - "@types/enzyme": "3.10.12", - "@types/enzyme-adapter-react-16": "1.0.6", "@types/hoist-non-react-statics": "3.3.1", "@types/is-hotkey": "0.1.7", "@types/jest": "29.2.3", @@ -168,11 +166,9 @@ "@types/testing-library__jest-dom": "5.14.5", "@types/tinycolor2": "1.4.3", "@types/uuid": "8.3.4", - "@wojtekmaj/enzyme-adapter-react-17": "0.8.0", "common-tags": "1.8.2", "css-loader": "6.7.1", "csstype": "3.1.1", - "enzyme": "3.11.0", "esbuild": "0.16.17", "expose-loader": "4.0.0", "mock-raf": "1.0.1", diff --git a/packages/grafana-ui/src/components/Button/Button.tsx b/packages/grafana-ui/src/components/Button/Button.tsx index ae3a9032956..711b42dcd74 100644 --- a/packages/grafana-ui/src/components/Button/Button.tsx +++ b/packages/grafana-ui/src/components/Button/Button.tsx @@ -311,3 +311,18 @@ export const clearButtonStyles = (theme: GrafanaTheme2) => { padding: 0; `; }; + +export const clearLinkButtonStyles = (theme: GrafanaTheme2) => { + return css` + background: transparent; + border: none; + padding: 0; + font-family: inherit; + color: inherit; + height: 100%; + &:hover { + background: transparent; + color: inherit; + } + `; +}; diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.tsx index 8cfc3c5312d..92186235865 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.tsx @@ -30,19 +30,23 @@ export const Checkbox = React.forwardRef( return ( ); } @@ -55,6 +59,9 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => { return { wrapper: css` + display: flex; + gap: ${theme.spacing(labelPadding)}; + align-items: baseline; position: relative; vertical-align: middle; font-size: 0; @@ -138,18 +145,17 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => { css` position: relative; z-index: 2; - padding-left: ${theme.spacing(labelPadding)}; - white-space: nowrap; cursor: pointer; - position: relative; top: -3px; + max-width: fit-content; + line-height: ${theme.typography.bodySmall.lineHeight}; + margin-bottom: 0; ` ), description: cx( labelStyles.description, css` line-height: ${theme.typography.bodySmall.lineHeight}; - padding-left: ${theme.spacing(checkboxSize + labelPadding)}; margin-top: 0; /* The margin effectively comes from the top: -2px on the label above it */ ` ), diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx index b661ed4b312..7557fa5d7b3 100644 --- a/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx @@ -1,6 +1,5 @@ -import { mount, shallow } from 'enzyme'; +import { render } from '@testing-library/react'; import React from 'react'; -import { Editor } from 'slate-react'; import { createTheme } from '@grafana/data'; @@ -8,120 +7,102 @@ import { UnThemedQueryField } from './QueryField'; describe('', () => { it('should render with null initial value', () => { - const wrapper = shallow( - - ); - expect(wrapper.find('div').exists()).toBeTruthy(); + expect(() => + render( + + ) + ).not.toThrow(); }); it('should render with empty initial value', () => { - const wrapper = shallow( - - ); - expect(wrapper.find('div').exists()).toBeTruthy(); + expect(() => + render() + ).not.toThrow(); }); it('should render with initial value', () => { - const wrapper = shallow( - - ); - expect(wrapper.find('div').exists()).toBeTruthy(); + expect(() => + render( + + ) + ).not.toThrow(); }); - it('should execute query on blur', () => { - const onRun = jest.fn(); - const wrapper = mount( - - ); - const field = wrapper.instance() as UnThemedQueryField; - const ed = wrapper.find(Editor).instance() as Editor; - expect(onRun.mock.calls.length).toBe(0); - field.handleBlur(undefined, ed, () => {}); - expect(onRun.mock.calls.length).toBe(1); - }); - - it('should run onChange with clean text', () => { - const onChange = jest.fn(); - const wrapper = shallow( - - ); - const field = wrapper.instance() as UnThemedQueryField; - field.runOnChange(); - expect(onChange.mock.calls.length).toBe(1); - expect(onChange.mock.calls[0][0]).toBe('my clean query '); - }); - - it('should run custom on blur, but not necessarily execute query', () => { - const onBlur = jest.fn(); - const onRun = jest.fn(); - const wrapper = mount( - - ); - const field = wrapper.instance() as UnThemedQueryField; - const ed = wrapper.find(Editor).instance() as Editor; - expect(onBlur.mock.calls.length).toBe(0); - expect(onRun.mock.calls.length).toBe(0); - field.handleBlur(undefined, ed, () => {}); - expect(onBlur.mock.calls.length).toBe(1); - expect(onRun.mock.calls.length).toBe(0); - }); describe('syntaxLoaded', () => { it('should re-render the editor after syntax has fully loaded', () => { - const wrapper: any = shallow( - + const mockOnRichValueChange = jest.fn(); + const { rerender } = render( + ); - const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); - wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) }; - wrapper.setProps({ syntaxLoaded: true }); - expect(spyOnChange).toHaveBeenCalledWith('fooo', true); + rerender( + + ); + expect(mockOnRichValueChange).toHaveBeenCalled(); }); + it('should not re-render the editor if syntax is already loaded', () => { - const wrapper: any = shallow( - + const mockOnRichValueChange = jest.fn(); + const { rerender } = render( + ); - const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); - wrapper.setProps({ syntaxLoaded: true }); - wrapper.instance().editor = {}; - wrapper.setProps({ syntaxLoaded: true }); - expect(spyOnChange).not.toBeCalled(); - }); - it('should not re-render the editor if editor itself is not defined', () => { - const wrapper: any = shallow( - + rerender( + ); - const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); - wrapper.setProps({ syntaxLoaded: true }); - expect(wrapper.instance().editor).toBeFalsy(); - expect(spyOnChange).not.toBeCalled(); + expect(mockOnRichValueChange).not.toBeCalled(); }); + it('should not re-render the editor twice once syntax is fully loaded', () => { - const wrapper: any = shallow( - + const mockOnRichValueChange = jest.fn(); + const { rerender } = render( + ); - const spyOnChange = jest.spyOn(wrapper.instance(), 'onChange').mockImplementation(jest.fn()); - wrapper.instance().editor = { insertText: () => ({ deleteBackward: () => ({ value: 'fooo' }) }) }; - wrapper.setProps({ syntaxLoaded: true }); - wrapper.setProps({ syntaxLoaded: true }); - expect(spyOnChange).toBeCalledTimes(1); + rerender( + + ); + rerender( + + ); + expect(mockOnRichValueChange).toBeCalledTimes(1); }); }); }); diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 026cfb16e8a..7f134b3056a 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -5,7 +5,9 @@ import tinycolor from 'tinycolor2'; import { DisplayValue, formattedValueToString } from '@grafana/data'; import { TableCellBackgroundDisplayMode, TableCellOptions } from '@grafana/schema'; +import { useStyles2 } from '../../themes'; import { getCellLinks, getTextColorForAlphaBackground } from '../../utils'; +import { Button, clearLinkButtonStyles } from '../Button'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; @@ -31,6 +33,7 @@ export const DefaultCell: FC = (props) => { const cellOptions = getCellOptions(field); const cellStyle = getCellStyle(tableStyles, cellOptions, displayValue, inspectEnabled); const hasLinks = Boolean(getCellLinks(field, row)?.length); + const clearButtonStyle = useStyles2(clearLinkButtonStyles); return (
@@ -39,11 +42,16 @@ export const DefaultCell: FC = (props) => { {hasLinks && ( getCellLinks(field, row) || []}> {(api) => { - return ( -
- {value} -
- ); + const content =
{value}
; + if (api.openMenu) { + return ( + + ); + } else { + return content; + } }}
)} diff --git a/packages/grafana-ui/src/components/Table/FilterPopup.tsx b/packages/grafana-ui/src/components/Table/FilterPopup.tsx index 7261e66ee96..3498b0b3700 100644 --- a/packages/grafana-ui/src/components/Table/FilterPopup.tsx +++ b/packages/grafana-ui/src/components/Table/FilterPopup.tsx @@ -50,6 +50,8 @@ export const FilterPopup: FC = ({ column: { preFilteredRows, filterValue, return ( + {/* This is just blocking click events from bubbeling and should not have a keyboard interaction. */} + {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
diff --git a/packages/grafana-ui/src/components/Table/ImageCell.tsx b/packages/grafana-ui/src/components/Table/ImageCell.tsx index c1e8b2ee2f1..9dffa16b88e 100644 --- a/packages/grafana-ui/src/components/Table/ImageCell.tsx +++ b/packages/grafana-ui/src/components/Table/ImageCell.tsx @@ -1,7 +1,9 @@ import { cx } from '@emotion/css'; import React, { FC } from 'react'; +import { useStyles2 } from '../../themes'; import { getCellLinks } from '../../utils'; +import { Button, clearLinkButtonStyles } from '../Button'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { TableCellProps } from './types'; @@ -12,6 +14,7 @@ export const ImageCell: FC = (props) => { const displayValue = field.display!(cell.value); const hasLinks = Boolean(getCellLinks(field, row)?.length); + const clearButtonStyle = useStyles2(clearLinkButtonStyles); return (
@@ -19,11 +22,16 @@ export const ImageCell: FC = (props) => { {hasLinks && ( getCellLinks(field, row) || []}> {(api) => { - return ( -
- -
- ); + const img = ; + if (api.openMenu) { + return ( + + ); + } else { + return img; + } }}
)} diff --git a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx index 70c73fce64e..68483d62e19 100644 --- a/packages/grafana-ui/src/components/Table/JSONViewCell.tsx +++ b/packages/grafana-ui/src/components/Table/JSONViewCell.tsx @@ -2,7 +2,9 @@ import { css, cx } from '@emotion/css'; import { isString } from 'lodash'; import React from 'react'; +import { useStyles2 } from '../../themes'; import { getCellLinks } from '../../utils'; +import { Button, clearLinkButtonStyles } from '../Button'; import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu'; import { CellActions } from './CellActions'; @@ -28,6 +30,7 @@ export function JSONViewCell(props: TableCellProps): JSX.Element { } const hasLinks = Boolean(getCellLinks(field, row)?.length); + const clearButtonStyle = useStyles2(clearLinkButtonStyles); return (
@@ -36,11 +39,15 @@ export function JSONViewCell(props: TableCellProps): JSX.Element { {hasLinks && ( getCellLinks(field, row) || []}> {(api) => { - return ( -
- {displayValue} -
- ); + if (api.openMenu) { + return ( + + ); + } else { + return <>{displayValue}; + } }}
)} diff --git a/packages/jaeger-ui-components/package.json b/packages/jaeger-ui-components/package.json index cce91d58fb6..8984f23da5d 100644 --- a/packages/jaeger-ui-components/package.json +++ b/packages/jaeger-ui-components/package.json @@ -14,7 +14,6 @@ "@testing-library/react": "12.1.4", "@testing-library/user-event": "14.4.3", "@types/deep-freeze": "^0.1.1", - "@types/enzyme": "3.10.12", "@types/hoist-non-react-statics": "^3.3.1", "@types/jest": "29.2.3", "@types/lodash": "4.14.187", @@ -25,7 +24,6 @@ "@types/slate-react": "0.22.9", "@types/testing-library__jest-dom": "5.14.5", "@types/tinycolor2": "1.4.3", - "enzyme": "3.11.0", "sinon": "14.0.1", "typescript": "4.8.4" }, diff --git a/pkg/api/dtos/folder.go b/pkg/api/dtos/folder.go index 54b96b603f8..618a64a0d45 100644 --- a/pkg/api/dtos/folder.go +++ b/pkg/api/dtos/folder.go @@ -31,5 +31,5 @@ type FolderSearchHit struct { Uid string `json:"uid"` Title string `json:"title"` AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"` - ParentUID string `json:"parent_uid,omitempty"` + ParentUID string `json:"parentUid,omitempty"` } diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 1a5f35f11bd..bfc6cad7e90 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -39,7 +39,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { OrgID: c.OrgID, Limit: c.QueryInt64("limit"), Page: c.QueryInt64("page"), - UID: c.Query("parent_uid"), + UID: c.Query("parentUid"), SignedInUser: c.SignedInUser, }) } else { @@ -340,7 +340,7 @@ type GetFoldersParams struct { // The parent folder UID // in:query // required:false - ParentUID string `json:"parent_uid"` + ParentUID string `json:"parentUid"` } // swagger:parameters getFolderByUID diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 18e78a88d74..233dcde5d0c 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -13,24 +13,13 @@ import ( "strings" "sync" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/middleware/csrf" - "github.com/grafana/grafana/pkg/services/auth" - "github.com/grafana/grafana/pkg/services/authn" - "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/oauthtoken" - "github.com/grafana/grafana/pkg/services/querylibrary" - "github.com/grafana/grafana/pkg/services/searchV2" - "github.com/grafana/grafana/pkg/services/stats" - "github.com/grafana/grafana/pkg/services/store/entity/httpentitystore" - "github.com/grafana/grafana/pkg/services/store/k8saccess" - "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/grafana/grafana/pkg/api/avatar" "github.com/grafana/grafana/pkg/api/routing" httpstatic "github.com/grafana/grafana/pkg/api/static" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/kvstore" @@ -41,7 +30,7 @@ import ( loginpkg "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/middleware" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/middleware/csrf" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/plugincontext" "github.com/grafana/grafana/pkg/registry/corekind" @@ -49,6 +38,8 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/apikey" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/comments" "github.com/grafana/grafana/pkg/services/contexthandler" @@ -63,10 +54,12 @@ import ( "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/export" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/ldap" "github.com/grafana/grafana/pkg/services/libraryelements" "github.com/grafana/grafana/pkg/services/librarypanels" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/live/pushhttp" "github.com/grafana/grafana/pkg/services/login" @@ -74,6 +67,7 @@ import ( "github.com/grafana/grafana/pkg/services/navtree" "github.com/grafana/grafana/pkg/services/ngalert" "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/services/plugindashboards" @@ -83,9 +77,11 @@ import ( publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/queryhistory" + "github.com/grafana/grafana/pkg/services/querylibrary" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/secrets" secretsKV "github.com/grafana/grafana/pkg/services/secrets/kvstore" @@ -94,7 +90,10 @@ import ( "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/star" + "github.com/grafana/grafana/pkg/services/stats" "github.com/grafana/grafana/pkg/services/store" + "github.com/grafana/grafana/pkg/services/store/entity/httpentitystore" + "github.com/grafana/grafana/pkg/services/store/k8saccess" "github.com/grafana/grafana/pkg/services/tag" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/teamguardian" @@ -102,6 +101,7 @@ import ( "github.com/grafana/grafana/pkg/services/thumbs" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -130,10 +130,10 @@ type HTTPServer struct { RemoteCacheService *remotecache.RemoteCache ProvisioningService provisioning.ProvisioningService Login login.Service - License models.Licensing + License licensing.Licensing AccessControl accesscontrol.AccessControl DataProxy *datasourceproxy.DataSourceProxyService - PluginRequestValidator models.PluginRequestValidator + PluginRequestValidator validations.PluginRequestValidator pluginClient plugins.Client pluginStore plugins.Store pluginInstaller plugins.Installer @@ -222,9 +222,9 @@ type ServerOptions struct { } func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routing.RouteRegister, bus bus.Bus, - renderService rendering.Service, licensing models.Licensing, hooksService *hooks.HooksService, + renderService rendering.Service, licensing licensing.Licensing, hooksService *hooks.HooksService, cacheService *localcache.CacheService, sqlStore *sqlstore.SQLStore, alertEngine *alerting.AlertEngine, - pluginRequestValidator models.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver, + pluginRequestValidator validations.PluginRequestValidator, pluginStaticRouteResolver plugins.StaticRouteResolver, pluginDashboardService plugindashboards.Service, pluginStore plugins.Store, pluginClient plugins.Client, pluginErrorResolver plugins.ErrorResolver, pluginInstaller plugins.Installer, settingsProvider setting.Provider, dataSourceCache datasources.CacheService, userTokenService auth.UserTokenService, diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index e66574085a9..757c39fd999 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" @@ -202,7 +203,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon OrgID: c.OrgID, }) if err != nil { - if !errors.Is(err, models.ErrPluginSettingNotFound) { + if !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) { return response.Error(http.StatusInternalServerError, "Failed to get plugin settings", nil) } } else { @@ -227,7 +228,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon } func (hs *HTTPServer) UpdatePluginSetting(c *models.ReqContext) response.Response { - cmd := models.UpdatePluginSettingCmd{} + cmd := pluginsettings.UpdatePluginSettingCmd{} if err := web.Bind(c.Req, &cmd); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } diff --git a/pkg/cmd/grafana-cli/runner/wireexts_oss.go b/pkg/cmd/grafana-cli/runner/wireexts_oss.go index 930c1fcf31f..be33d32cd94 100644 --- a/pkg/cmd/grafana-cli/runner/wireexts_oss.go +++ b/pkg/cmd/grafana-cli/runner/wireexts_oss.go @@ -6,7 +6,6 @@ package runner import ( "github.com/google/wire" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/server/backgroundsvcs" "github.com/grafana/grafana/pkg/server/usagestatssvcs" @@ -41,7 +40,7 @@ var wireExtsSet = wire.NewSet( wireSet, migrations.ProvideOSSMigrations, licensing.ProvideService, - wire.Bind(new(models.Licensing), new(*licensing.OSSLicensingService)), + wire.Bind(new(licensing.Licensing), new(*licensing.OSSLicensingService)), wire.Bind(new(registry.DatabaseMigrator), new(*migrations.OSSMigrations)), setting.ProvideProvider, wire.Bind(new(setting.Provider), new(*setting.OSSImpl)), @@ -56,7 +55,7 @@ var wireExtsSet = wire.NewSet( thumbs.ProvideCrawlerAuthSetupService, wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)), validations.ProvideValidator, - wire.Bind(new(models.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)), + wire.Bind(new(validations.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)), provisioning.ProvideService, wire.Bind(new(provisioning.ProvisioningService), new(*provisioning.ProvisioningServiceImpl)), backgroundsvcs.ProvideBackgroundServiceRegistry, diff --git a/pkg/codegen/tmpl/docs.tmpl b/pkg/codegen/tmpl/docs.tmpl index 710d43aee2a..918b1dd9a87 100644 --- a/pkg/codegen/tmpl/docs.tmpl +++ b/pkg/codegen/tmpl/docs.tmpl @@ -8,7 +8,7 @@ title: {{ .KindName }} kind # {{ .KindName }} kind -### Maturity: {{ .KindMaturity }} -### Version: {{ .KindVersion }} +## Maturity: {{ .KindMaturity }} +## Version: {{ .KindVersion }} {{ .Markdown }} diff --git a/pkg/expr/commands_test.go b/pkg/expr/commands_test.go index aa741dada2e..9109e6fc6ec 100644 --- a/pkg/expr/commands_test.go +++ b/pkg/expr/commands_test.go @@ -169,7 +169,7 @@ func TestReduceExecute(t *testing.T) { func randomReduceFunc() string { res := mathexp.GetSupportedReduceFuncs() - return res[rand.Intn(len(res)-1)] + return res[rand.Intn(len(res))] } func TestResampleCommand_Execute(t *testing.T) { diff --git a/pkg/infra/httpclient/httpclientprovider/host_redirect_validation_middleware.go b/pkg/infra/httpclient/httpclientprovider/host_redirect_validation_middleware.go index 7bffa1126c6..c53ead6e7a5 100644 --- a/pkg/infra/httpclient/httpclientprovider/host_redirect_validation_middleware.go +++ b/pkg/infra/httpclient/httpclientprovider/host_redirect_validation_middleware.go @@ -4,14 +4,14 @@ import ( "errors" "net/http" - "github.com/grafana/grafana/pkg/models" - sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + + "github.com/grafana/grafana/pkg/services/validations" ) const HostRedirectValidationMiddlewareName = "host-redirect-validation" -func RedirectLimitMiddleware(reqValidator models.PluginRequestValidator) sdkhttpclient.Middleware { +func RedirectLimitMiddleware(reqValidator validations.PluginRequestValidator) sdkhttpclient.Middleware { return sdkhttpclient.NamedMiddlewareFunc(HostRedirectValidationMiddlewareName, func(opts sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { return sdkhttpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { res, err := next.RoundTrip(req) diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index da9eb67f966..c8aea67c8a3 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -6,19 +6,20 @@ import ( "time" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/mwitkow/go-conntrack" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics/metricutil" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" - "github.com/mwitkow/go-conntrack" ) var newProviderFunc = sdkhttpclient.NewProvider // New creates a new HTTP client provider with pre-configured middlewares. -func New(cfg *setting.Cfg, validator models.PluginRequestValidator, tracer tracing.Tracer) *sdkhttpclient.Provider { +func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer tracing.Tracer) *sdkhttpclient.Provider { logger := log.New("httpclient") userAgent := fmt.Sprintf("Grafana/%s", cfg.BuildVersion) diff --git a/pkg/kindsys/kindsysreport/attributes.go b/pkg/kindsys/kindsysreport/attributes.go new file mode 100644 index 00000000000..c51a4b3df74 --- /dev/null +++ b/pkg/kindsys/kindsysreport/attributes.go @@ -0,0 +1,74 @@ +package kindsysreport + +import ( + "cuelang.org/go/cue" +) + +type AttributeWalker struct { + seen map[cue.Value]bool + count map[string]int +} + +func (w *AttributeWalker) Count(sch cue.Value, attrs ...string) map[string]int { + w.seen = make(map[cue.Value]bool) + w.count = make(map[string]int) + + for _, attr := range attrs { + w.count[attr] = 0 + } + + w.walk(cue.MakePath(), sch) + return w.count +} + +func (w *AttributeWalker) walk(p cue.Path, v cue.Value) { + if w.seen[v] { + return + } + + w.seen[v] = true + + for attr := range w.count { + if found := v.Attribute(attr); found.Err() == nil { + w.count[attr]++ + } + } + + // nolint: exhaustive + switch v.Kind() { + case cue.StructKind: + // If current cue.Value is a reference to another + // definition, we don't want to traverse its fields + // individually, because we'll do so for the actual def. + if v != cue.Dereference(v) { + return + } + + iter, err := v.Fields(cue.All()) + if err != nil { + panic(err) + } + + for iter.Next() { + w.walk(appendPath(p, iter.Selector()), iter.Value()) + } + if lv := v.LookupPath(cue.MakePath(cue.AnyString)); lv.Exists() { + w.walk(appendPath(p, cue.AnyString), lv) + } + case cue.ListKind: + list, err := v.List() + if err != nil { + panic(err) + } + for i := 0; list.Next(); i++ { + w.walk(appendPath(p, cue.Index(i)), list.Value()) + } + if lv := v.LookupPath(cue.MakePath(cue.AnyIndex)); lv.Exists() { + w.walk(appendPath(p, cue.AnyString), lv) + } + } +} + +func appendPath(p cue.Path, sel cue.Selector) cue.Path { + return cue.MakePath(append(p.Selectors(), sel)...) +} diff --git a/pkg/kindsys/kindsysreport/codeowners.go b/pkg/kindsys/kindsysreport/codeowners.go new file mode 100644 index 00000000000..5c1a3edcc7a --- /dev/null +++ b/pkg/kindsys/kindsysreport/codeowners.go @@ -0,0 +1,61 @@ +package kindsysreport + +import ( + "os" + "path/filepath" + + "github.com/hmarr/codeowners" +) + +type CodeOwnersFinder struct { + ruleset codeowners.Ruleset +} + +func NewCodeOwnersFinder(groot string) (CodeOwnersFinder, error) { + //nolint:gosec + file, err := os.Open(filepath.Join(groot, ".github", "CODEOWNERS")) + if err != nil { + return CodeOwnersFinder{}, err + } + + ruleset, err := codeowners.ParseFile(file) + if err != nil { + return CodeOwnersFinder{}, err + } + + return CodeOwnersFinder{ + ruleset: ruleset, + }, nil +} + +func (f CodeOwnersFinder) FindFor(pp ...string) ([]string, error) { + if len(f.ruleset) == 0 { + return nil, nil + } + + // Set, to avoid duplicates + m := make(map[string]struct{}) + + for _, p := range pp { + r, err := f.ruleset.Match(p) + if err != nil { + return nil, err + } + + // No rule found for path p + if r == nil { + continue + } + + for _, o := range r.Owners { + m[o.Value] = struct{}{} + } + } + + result := make([]string, 0, len(m)) + for k := range m { + result = append(result, k) + } + + return result, nil +} diff --git a/pkg/kindsys/report.go b/pkg/kindsys/report.go index a554f685674..b853abbe97d 100644 --- a/pkg/kindsys/report.go +++ b/pkg/kindsys/report.go @@ -9,18 +9,41 @@ import ( "context" "encoding/json" "fmt" + "net/url" "os" + "path/filepath" + "reflect" "sort" "strings" + "cuelang.org/go/cue" + "github.com/grafana/codejen" "github.com/grafana/grafana/pkg/kindsys" + "github.com/grafana/grafana/pkg/kindsys/kindsysreport" "github.com/grafana/grafana/pkg/plugins/pfs/corelist" "github.com/grafana/grafana/pkg/plugins/plugindef" "github.com/grafana/grafana/pkg/registry/corekind" + "github.com/grafana/thema" ) -const reportFileName = "report.json" +const ( + // Program's output + reportFileName = "report.json" + + // External references + repoBaseURL = "https://github.com/grafana/grafana/tree/main" + docsBaseURL = "https://grafana.com/docs/grafana/next/developers/kinds" + + // Local references + coreTSPath = "packages/grafana-schema/src/raw/%s/%s/%s_types.gen.ts" + coreGoPath = "pkg/kinds/%s" + coreCUEPath = "kinds/%s/%s_kind.cue" + + composableTSPath = "public/app/plugins/%s/%s/%s.gen.ts" + composableGoPath = "pkg/tsdb/%s/kinds/%s/types_%s_gen.go" + composableCUEPath = "public/app/plugins/%s/%s/%s.cue" +) func main() { report := buildKindStateReport() @@ -54,17 +77,67 @@ var plannedCoreKinds = []string{ "QueryHistory", } -type KindStateReport struct { - Kinds map[string]kindsys.SomeKindProperties `json:"kinds"` - Dimensions map[string]Dimension `json:"dimensions"` +type KindLinks struct { + Schema string + Go string + Ts string + Docs string } -func (r *KindStateReport) add(k kindsys.SomeKindProperties, category string) { +type Kind struct { + kindsys.SomeKindProperties + Category string + Links KindLinks + GrafanaMaturityCount int + CodeOwners []string +} + +// MarshalJSON is overwritten to marshal +// kindsys.SomeKindProperties at root level. +func (k Kind) MarshalJSON() ([]byte, error) { + b, err := json.Marshal(k.SomeKindProperties) + if err != nil { + return nil, err + } + + var m map[string]interface{} + if err = json.Unmarshal(b, &m); err != nil { + return nil, err + } + + m["category"] = k.Category + m["grafanaMaturityCount"] = k.GrafanaMaturityCount + + if len(k.CodeOwners) == 0 { + m["codeowners"] = []string{} + } else { + m["codeowners"] = k.CodeOwners + } + + m["links"] = map[string]string{} + for _, ref := range []string{"Schema", "Go", "Ts", "Docs"} { + refVal := reflect.ValueOf(k.Links).FieldByName(ref).String() + if len(refVal) > 0 { + m["links"].(map[string]string)[toCamelCase(ref)] = refVal + } else { + m["links"].(map[string]string)[toCamelCase(ref)] = "n/a" + } + } + + return json.Marshal(m) +} + +type KindStateReport struct { + Kinds map[string]Kind `json:"kinds"` + Dimensions map[string]Dimension `json:"dimensions"` +} + +func (r *KindStateReport) add(k Kind) { kName := k.Common().MachineName r.Kinds[kName] = k r.Dimensions["maturity"][k.Common().Maturity.String()].add(kName) - r.Dimensions["category"][category].add(kName) + r.Dimensions["category"][k.Category].add(kName) } type Dimension map[string]*DimensionValue @@ -85,7 +158,7 @@ func (dv *DimensionValue) add(s string) { // the final report. func emptyKindStateReport() *KindStateReport { return &KindStateReport{ - Kinds: make(map[string]kindsys.SomeKindProperties), + Kinds: make(map[string]Kind), Dimensions: map[string]Dimension{ "maturity": { "planned": emptyDimensionValue("planned"), @@ -114,12 +187,23 @@ func buildKindStateReport() *KindStateReport { r := emptyKindStateReport() b := corekind.NewBase(nil) + groot := filepath.Join(elsedie(os.Getwd())("cannot get cwd"), "..", "..") + of := elsedie(kindsysreport.NewCodeOwnersFinder(groot))("cannot parse .github/codeowners") + seen := make(map[string]bool) for _, k := range b.All() { seen[k.Props().Common().Name] = true + lin := k.Lineage() switch k.Props().(type) { case kindsys.CoreProperties: - r.add(k.Props(), "core") + links := buildCoreLinks(lin, k.Decl().Properties) + r.add(Kind{ + SomeKindProperties: k.Props(), + Category: "core", + Links: links, + GrafanaMaturityCount: grafanaMaturityAttrCount(lin.Latest().Underlying()), + CodeOwners: findCodeOwners(of, links), + }) } } @@ -128,25 +212,35 @@ func buildKindStateReport() *KindStateReport { continue } - r.add(kindsys.CoreProperties{ - CommonProperties: kindsys.CommonProperties{ - Name: kn, - PluralName: kn + "s", - MachineName: machinize(kn), - PluralMachineName: machinize(kn) + "s", - Maturity: "planned", + r.add(Kind{ + SomeKindProperties: kindsys.CoreProperties{ + CommonProperties: kindsys.CommonProperties{ + Name: kn, + PluralName: kn + "s", + MachineName: machinize(kn), + PluralMachineName: machinize(kn) + "s", + Maturity: "planned", + }, }, - }, "core") + Category: "core", + }) } all := kindsys.SchemaInterfaces(nil) for _, pp := range corelist.New(nil) { for _, si := range all { if ck, has := pp.ComposableKinds[si.Name()]; has { - r.add(ck.Props(), "composable") + links := buildComposableLinks(pp.Properties, ck.Decl().Properties) + r.add(Kind{ + SomeKindProperties: ck.Props(), + Category: "composable", + Links: links, + GrafanaMaturityCount: grafanaMaturityAttrCount(ck.Lineage().Latest().Underlying()), + CodeOwners: findCodeOwners(of, links), + }) } else if may := si.Should(string(pp.Properties.Type)); may { n := plugindef.DerivePascalName(pp.Properties) + si.Name() - props := kindsys.ComposableProperties{ + ck := kindsys.ComposableProperties{ SchemaInterface: si.Name(), CommonProperties: kindsys.CommonProperties{ Name: n, @@ -157,7 +251,10 @@ func buildKindStateReport() *KindStateReport { Maturity: "planned", }, } - r.add(props, "composable") + r.add(Kind{ + SomeKindProperties: ck, + Category: "composable", + }) } } } @@ -171,6 +268,79 @@ func buildKindStateReport() *KindStateReport { return r } +func buildCoreLinks(lin thema.Lineage, cp kindsys.CoreProperties) KindLinks { + const category = "core" + vpath := fmt.Sprintf("v%v", lin.Latest().Version()[0]) + if cp.Maturity.Less(kindsys.MaturityStable) { + vpath = "x" + } + + return KindLinks{ + Schema: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(coreCUEPath, cp.MachineName, cp.MachineName)))("cannot build schema link"), + Go: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(coreGoPath, cp.MachineName)))("cannot build go link"), + Ts: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(coreTSPath, cp.MachineName, vpath, cp.MachineName)))("cannot build ts link"), + Docs: elsedie(url.JoinPath(docsBaseURL, category, cp.MachineName, "schema-reference"))("cannot build docs link"), + } +} + +// used to map names for those plugins that aren't following +// naming conventions, like 'annonlist' which comes from "Annotations list". +var irregularPluginNames = map[string]string{ + // Panel + "alertgroups": "alertGroups", + "annotationslist": "annolist", + "dashboardlist": "dashlist", + "nodegraph": "nodeGraph", + "statetimeline": "state-timeline", + "statushistory": "status-history", + "tableold": "table-old", + // Datasource + "googlecloudmonitoring": "cloud-monitoring", + "azuremonitor": "grafana-azure-monitor-datasource", + "microsoftsqlserver": "mssql", + "postgresql": "postgres", + "testdatadb": "testdata", +} + +func buildComposableLinks(pp plugindef.PluginDef, cp kindsys.ComposableProperties) KindLinks { + const category = "composable" + schemaInterface := strings.ToLower(cp.SchemaInterface) + + pName := strings.Replace(cp.MachineName, schemaInterface, "", 1) + if irr, ok := irregularPluginNames[pName]; ok { + pName = irr + } + + var goLink string + if pp.Backend != nil && *pp.Backend { + goLink = elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(composableGoPath, pName, schemaInterface, schemaInterface)))("cannot build go link") + } + + return KindLinks{ + Schema: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(composableCUEPath, string(pp.Type), pName, schemaInterface)))("cannot build schema link"), + Go: goLink, + Ts: elsedie(url.JoinPath(repoBaseURL, fmt.Sprintf(composableTSPath, string(pp.Type), pName, schemaInterface)))("cannot build ts link"), + Docs: elsedie(url.JoinPath(docsBaseURL, category, cp.MachineName, "schema-reference"))("cannot build docs link"), + } +} + +func grafanaMaturityAttrCount(sch cue.Value) int { + const attr = "grafanamaturity" + aw := new(kindsysreport.AttributeWalker) + return aw.Count(sch, attr)[attr] +} + +func findCodeOwners(of kindsysreport.CodeOwnersFinder, links KindLinks) []string { + owners := elsedie(of.FindFor([]string{ + toLocalPath(links.Schema), + toLocalPath(links.Go), + toLocalPath(links.Ts), + }...))("cannot find code owners") + + sort.Strings(owners) + return owners +} + func machinize(s string) string { return strings.Map(func(r rune) rune { switch { @@ -190,6 +360,14 @@ func machinize(s string) string { }, s) } +func toCamelCase(s string) string { + return strings.ToLower(string(s[0])) + s[1:] +} + +func toLocalPath(s string) string { + return strings.Replace(s, repoBaseURL+"/", "", 1) +} + type reportJenny struct{} func (reportJenny) JennyName() string { diff --git a/pkg/kindsys/report.json b/pkg/kindsys/report.json index ac2f97db3a5..67d72700ab8 100644 --- a/pkg/kindsys/report.json +++ b/pkg/kindsys/report.json @@ -1,954 +1,1657 @@ { "kinds": { "alertgroupspanelcfg": { - "name": "AlertGroupsPanelCfg", - "pluralName": "AlertGroupsPanelCfgs", - "machineName": "alertgroupspanelcfg", - "pluralMachineName": "alertgroupspanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [ + "grafana/alerting-squad-frontend" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/alertgroupspanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/alertGroups/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/alertGroups/panelcfg.gen.ts" + }, + "machineName": "alertgroupspanelcfg", + "maturity": "merged", + "name": "AlertGroupsPanelCfg", + "pluralMachineName": "alertgroupspanelcfgs", + "pluralName": "AlertGroupsPanelCfgs", "schemaInterface": "PanelCfg" }, "alertlistpanelcfg": { - "name": "AlertListPanelCfg", - "pluralName": "AlertListPanelCfgs", - "machineName": "alertlistpanelcfg", - "pluralMachineName": "alertlistpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "alertlistpanelcfg", + "maturity": "planned", + "name": "AlertListPanelCfg", + "pluralMachineName": "alertlistpanelcfgs", + "pluralName": "AlertListPanelCfgs", "schemaInterface": "PanelCfg" }, "alertmanagerdataquery": { - "name": "AlertmanagerDataQuery", - "pluralName": "AlertmanagerDataQuerys", - "machineName": "alertmanagerdataquery", - "pluralMachineName": "alertmanagerdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "alertmanagerdataquery", + "maturity": "planned", + "name": "AlertmanagerDataQuery", + "pluralMachineName": "alertmanagerdataquerys", + "pluralName": "AlertmanagerDataQuerys", "schemaInterface": "DataQuery" }, "alertmanagerdatasourcecfg": { - "name": "AlertmanagerDataSourceCfg", - "pluralName": "AlertmanagerDataSourceCfgs", - "machineName": "alertmanagerdatasourcecfg", - "pluralMachineName": "alertmanagerdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "alertmanagerdatasourcecfg", + "maturity": "planned", + "name": "AlertmanagerDataSourceCfg", + "pluralMachineName": "alertmanagerdatasourcecfgs", + "pluralName": "AlertmanagerDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "annotationslistpanelcfg": { - "name": "AnnotationsListPanelCfg", - "pluralName": "AnnotationsListPanelCfgs", - "machineName": "annotationslistpanelcfg", - "pluralMachineName": "annotationslistpanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/annotationslistpanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/annolist/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/annolist/panelcfg.gen.ts" + }, + "machineName": "annotationslistpanelcfg", + "maturity": "experimental", + "name": "AnnotationsListPanelCfg", + "pluralMachineName": "annotationslistpanelcfgs", + "pluralName": "AnnotationsListPanelCfgs", "schemaInterface": "PanelCfg" }, "apikey": { - "name": "APIKey", - "pluralName": "APIKeys", - "machineName": "apikey", - "pluralMachineName": "apikeys", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "azuremonitordataquery": { - "name": "AzureMonitorDataQuery", - "pluralName": "AzureMonitorDataQuerys", - "machineName": "azuremonitordataquery", - "pluralMachineName": "azuremonitordataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "core", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "apikey", + "maturity": "planned", + "name": "APIKey", + "pluralMachineName": "apikeys", + "pluralName": "APIKeys" + }, + "azuremonitordataquery": { + "category": "composable", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "azuremonitordataquery", + "maturity": "planned", + "name": "AzureMonitorDataQuery", + "pluralMachineName": "azuremonitordataquerys", + "pluralName": "AzureMonitorDataQuerys", "schemaInterface": "DataQuery" }, "azuremonitordatasourcecfg": { - "name": "AzureMonitorDataSourceCfg", - "pluralName": "AzureMonitorDataSourceCfgs", - "machineName": "azuremonitordatasourcecfg", - "pluralMachineName": "azuremonitordatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "azuremonitordatasourcecfg", + "maturity": "planned", + "name": "AzureMonitorDataSourceCfg", + "pluralMachineName": "azuremonitordatasourcecfgs", + "pluralName": "AzureMonitorDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "barchartpanelcfg": { - "name": "BarChartPanelCfg", - "pluralName": "BarChartPanelCfgs", - "machineName": "barchartpanelcfg", - "pluralMachineName": "barchartpanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/grafana-bi-squad" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/barchartpanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/barchart/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/barchart/panelcfg.gen.ts" + }, + "machineName": "barchartpanelcfg", + "maturity": "experimental", + "name": "BarChartPanelCfg", + "pluralMachineName": "barchartpanelcfgs", + "pluralName": "BarChartPanelCfgs", "schemaInterface": "PanelCfg" }, "bargaugepanelcfg": { - "name": "BarGaugePanelCfg", - "pluralName": "BarGaugePanelCfgs", - "machineName": "bargaugepanelcfg", - "pluralMachineName": "bargaugepanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/bargaugepanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/bargauge/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/bargauge/panelcfg.gen.ts" + }, + "machineName": "bargaugepanelcfg", + "maturity": "experimental", + "name": "BarGaugePanelCfg", + "pluralMachineName": "bargaugepanelcfgs", + "pluralName": "BarGaugePanelCfgs", "schemaInterface": "PanelCfg" }, "cloudwatchdataquery": { - "name": "CloudWatchDataQuery", - "pluralName": "CloudWatchDataQuerys", - "machineName": "cloudwatchdataquery", - "pluralMachineName": "cloudwatchdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "cloudwatchdataquery", + "maturity": "planned", + "name": "CloudWatchDataQuery", + "pluralMachineName": "cloudwatchdataquerys", + "pluralName": "CloudWatchDataQuerys", "schemaInterface": "DataQuery" }, "cloudwatchdatasourcecfg": { - "name": "CloudWatchDataSourceCfg", - "pluralName": "CloudWatchDataSourceCfgs", - "machineName": "cloudwatchdatasourcecfg", - "pluralMachineName": "cloudwatchdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "cloudwatchdatasourcecfg", + "maturity": "planned", + "name": "CloudWatchDataSourceCfg", + "pluralMachineName": "cloudwatchdatasourcecfgs", + "pluralName": "CloudWatchDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "dashboard": { - "name": "Dashboard", - "pluralName": "Dashboards", - "machineName": "dashboard", - "pluralMachineName": "dashboards", - "lineageIsGroup": false, - "maturity": "experimental", - "currentVersion": [ - 0, - 0 - ] - }, - "dashboarddataquery": { - "name": "DashboardDataQuery", - "pluralName": "DashboardDataQuerys", - "machineName": "dashboarddataquery", - "pluralMachineName": "dashboarddataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "core", + "codeowners": [ + "grafana/grafana-as-code", + "grafana/grafana-bi-squad", + "grafana/plugins-platform-frontend", + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 144, + "lineageIsGroup": false, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/dashboard/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/dashboard", + "schema": "https://github.com/grafana/grafana/tree/main/kinds/dashboard/dashboard_kind.cue", + "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/dashboard/x/dashboard_types.gen.ts" + }, + "machineName": "dashboard", + "maturity": "experimental", + "name": "Dashboard", + "pluralMachineName": "dashboards", + "pluralName": "Dashboards" + }, + "dashboarddataquery": { + "category": "composable", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "dashboarddataquery", + "maturity": "planned", + "name": "DashboardDataQuery", + "pluralMachineName": "dashboarddataquerys", + "pluralName": "DashboardDataQuerys", "schemaInterface": "DataQuery" }, "dashboarddatasourcecfg": { - "name": "DashboardDataSourceCfg", - "pluralName": "DashboardDataSourceCfgs", - "machineName": "dashboarddatasourcecfg", - "pluralMachineName": "dashboarddatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "dashboarddatasourcecfg", + "maturity": "planned", + "name": "DashboardDataSourceCfg", + "pluralMachineName": "dashboarddatasourcecfgs", + "pluralName": "DashboardDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "dashboardlistpanelcfg": { - "name": "DashboardListPanelCfg", - "pluralName": "DashboardListPanelCfgs", - "machineName": "dashboardlistpanelcfg", - "pluralMachineName": "dashboardlistpanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/dashboardlistpanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/dashlist/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/dashlist/panelcfg.gen.ts" + }, + "machineName": "dashboardlistpanelcfg", + "maturity": "experimental", + "name": "DashboardListPanelCfg", + "pluralMachineName": "dashboardlistpanelcfgs", + "pluralName": "DashboardListPanelCfgs", "schemaInterface": "PanelCfg" }, "datasource": { - "name": "DataSource", - "pluralName": "DataSources", - "machineName": "datasource", - "pluralMachineName": "datasources", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "debugpanelcfg": { - "name": "DebugPanelCfg", - "pluralName": "DebugPanelCfgs", - "machineName": "debugpanelcfg", - "pluralMachineName": "debugpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "core", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "datasource", + "maturity": "planned", + "name": "DataSource", + "pluralMachineName": "datasources", + "pluralName": "DataSources" + }, + "debugpanelcfg": { + "category": "composable", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "debugpanelcfg", + "maturity": "planned", + "name": "DebugPanelCfg", + "pluralMachineName": "debugpanelcfgs", + "pluralName": "DebugPanelCfgs", "schemaInterface": "PanelCfg" }, "elasticsearchdataquery": { - "name": "ElasticsearchDataQuery", - "pluralName": "ElasticsearchDataQuerys", - "machineName": "elasticsearchdataquery", - "pluralMachineName": "elasticsearchdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "elasticsearchdataquery", + "maturity": "planned", + "name": "ElasticsearchDataQuery", + "pluralMachineName": "elasticsearchdataquerys", + "pluralName": "ElasticsearchDataQuerys", "schemaInterface": "DataQuery" }, "elasticsearchdatasourcecfg": { - "name": "ElasticsearchDataSourceCfg", - "pluralName": "ElasticsearchDataSourceCfgs", - "machineName": "elasticsearchdatasourcecfg", - "pluralMachineName": "elasticsearchdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "elasticsearchdatasourcecfg", + "maturity": "planned", + "name": "ElasticsearchDataSourceCfg", + "pluralMachineName": "elasticsearchdatasourcecfgs", + "pluralName": "ElasticsearchDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "flamegraphpanelcfg": { - "name": "FlameGraphPanelCfg", - "pluralName": "FlameGraphPanelCfgs", - "machineName": "flamegraphpanelcfg", - "pluralMachineName": "flamegraphpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "flamegraphpanelcfg", + "maturity": "planned", + "name": "FlameGraphPanelCfg", + "pluralMachineName": "flamegraphpanelcfgs", + "pluralName": "FlameGraphPanelCfgs", "schemaInterface": "PanelCfg" }, "folder": { - "name": "Folder", - "pluralName": "Folders", - "machineName": "folder", - "pluralMachineName": "folders", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "gaugepanelcfg": { - "name": "GaugePanelCfg", - "pluralName": "GaugePanelCfgs", - "machineName": "gaugepanelcfg", - "pluralMachineName": "gaugepanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "core", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "folder", + "maturity": "planned", + "name": "Folder", + "pluralMachineName": "folders", + "pluralName": "Folders" + }, + "gaugepanelcfg": { + "category": "composable", + "codeowners": [ + "grafana/user-essentials" + ], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/gaugepanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/gauge/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/gauge/panelcfg.gen.ts" + }, + "machineName": "gaugepanelcfg", + "maturity": "experimental", + "name": "GaugePanelCfg", + "pluralMachineName": "gaugepanelcfgs", + "pluralName": "GaugePanelCfgs", "schemaInterface": "PanelCfg" }, "geomappanelcfg": { - "name": "GeomapPanelCfg", - "pluralName": "GeomapPanelCfgs", - "machineName": "geomappanelcfg", - "pluralMachineName": "geomappanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "geomappanelcfg", + "maturity": "planned", + "name": "GeomapPanelCfg", + "pluralMachineName": "geomappanelcfgs", + "pluralName": "GeomapPanelCfgs", "schemaInterface": "PanelCfg" }, "gettingstartedpanelcfg": { - "name": "GettingStartedPanelCfg", - "pluralName": "GettingStartedPanelCfgs", - "machineName": "gettingstartedpanelcfg", - "pluralMachineName": "gettingstartedpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "gettingstartedpanelcfg", + "maturity": "planned", + "name": "GettingStartedPanelCfg", + "pluralMachineName": "gettingstartedpanelcfgs", + "pluralName": "GettingStartedPanelCfgs", "schemaInterface": "PanelCfg" }, "googlecloudmonitoringdataquery": { - "name": "GoogleCloudMonitoringDataQuery", - "pluralName": "GoogleCloudMonitoringDataQuerys", - "machineName": "googlecloudmonitoringdataquery", - "pluralMachineName": "googlecloudmonitoringdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "googlecloudmonitoringdataquery", + "maturity": "planned", + "name": "GoogleCloudMonitoringDataQuery", + "pluralMachineName": "googlecloudmonitoringdataquerys", + "pluralName": "GoogleCloudMonitoringDataQuerys", "schemaInterface": "DataQuery" }, "googlecloudmonitoringdatasourcecfg": { - "name": "GoogleCloudMonitoringDataSourceCfg", - "pluralName": "GoogleCloudMonitoringDataSourceCfgs", - "machineName": "googlecloudmonitoringdatasourcecfg", - "pluralMachineName": "googlecloudmonitoringdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "googlecloudmonitoringdatasourcecfg", + "maturity": "planned", + "name": "GoogleCloudMonitoringDataSourceCfg", + "pluralMachineName": "googlecloudmonitoringdatasourcecfgs", + "pluralName": "GoogleCloudMonitoringDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "grafanadataquery": { - "name": "GrafanaDataQuery", - "pluralName": "GrafanaDataQuerys", - "machineName": "grafanadataquery", - "pluralMachineName": "grafanadataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "grafanadataquery", + "maturity": "planned", + "name": "GrafanaDataQuery", + "pluralMachineName": "grafanadataquerys", + "pluralName": "GrafanaDataQuerys", "schemaInterface": "DataQuery" }, "grafanadatasourcecfg": { - "name": "GrafanaDataSourceCfg", - "pluralName": "GrafanaDataSourceCfgs", - "machineName": "grafanadatasourcecfg", - "pluralMachineName": "grafanadatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "grafanadatasourcecfg", + "maturity": "planned", + "name": "GrafanaDataSourceCfg", + "pluralMachineName": "grafanadatasourcecfgs", + "pluralName": "GrafanaDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "graphitedataquery": { - "name": "GraphiteDataQuery", - "pluralName": "GraphiteDataQuerys", - "machineName": "graphitedataquery", - "pluralMachineName": "graphitedataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "graphitedataquery", + "maturity": "planned", + "name": "GraphiteDataQuery", + "pluralMachineName": "graphitedataquerys", + "pluralName": "GraphiteDataQuerys", "schemaInterface": "DataQuery" }, "graphitedatasourcecfg": { - "name": "GraphiteDataSourceCfg", - "pluralName": "GraphiteDataSourceCfgs", - "machineName": "graphitedatasourcecfg", - "pluralMachineName": "graphitedatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "graphitedatasourcecfg", + "maturity": "planned", + "name": "GraphiteDataSourceCfg", + "pluralMachineName": "graphitedatasourcecfgs", + "pluralName": "GraphiteDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "grapholdpanelcfg": { - "name": "GraphOldPanelCfg", - "pluralName": "GraphOldPanelCfgs", - "machineName": "grapholdpanelcfg", - "pluralMachineName": "grapholdpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "grapholdpanelcfg", + "maturity": "planned", + "name": "GraphOldPanelCfg", + "pluralMachineName": "grapholdpanelcfgs", + "pluralName": "GraphOldPanelCfgs", "schemaInterface": "PanelCfg" }, "histogrampanelcfg": { - "name": "HistogramPanelCfg", - "pluralName": "HistogramPanelCfgs", - "machineName": "histogrampanelcfg", - "pluralMachineName": "histogrampanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/grafana-bi-squad" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/histogrampanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/histogram/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/histogram/panelcfg.gen.ts" + }, + "machineName": "histogrampanelcfg", + "maturity": "experimental", + "name": "HistogramPanelCfg", + "pluralMachineName": "histogrampanelcfgs", + "pluralName": "HistogramPanelCfgs", "schemaInterface": "PanelCfg" }, "iconpanelcfg": { - "name": "IconPanelCfg", - "pluralName": "IconPanelCfgs", - "machineName": "iconpanelcfg", - "pluralMachineName": "iconpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "iconpanelcfg", + "maturity": "planned", + "name": "IconPanelCfg", + "pluralMachineName": "iconpanelcfgs", + "pluralName": "IconPanelCfgs", "schemaInterface": "PanelCfg" }, "jaegerdataquery": { - "name": "JaegerDataQuery", - "pluralName": "JaegerDataQuerys", - "machineName": "jaegerdataquery", - "pluralMachineName": "jaegerdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "jaegerdataquery", + "maturity": "planned", + "name": "JaegerDataQuery", + "pluralMachineName": "jaegerdataquerys", + "pluralName": "JaegerDataQuerys", "schemaInterface": "DataQuery" }, "jaegerdatasourcecfg": { - "name": "JaegerDataSourceCfg", - "pluralName": "JaegerDataSourceCfgs", - "machineName": "jaegerdatasourcecfg", - "pluralMachineName": "jaegerdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "jaegerdatasourcecfg", + "maturity": "planned", + "name": "JaegerDataSourceCfg", + "pluralMachineName": "jaegerdatasourcecfgs", + "pluralName": "JaegerDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "livepanelcfg": { - "name": "LivePanelCfg", - "pluralName": "LivePanelCfgs", - "machineName": "livepanelcfg", - "pluralMachineName": "livepanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "livepanelcfg", + "maturity": "planned", + "name": "LivePanelCfg", + "pluralMachineName": "livepanelcfgs", + "pluralName": "LivePanelCfgs", "schemaInterface": "PanelCfg" }, "logspanelcfg": { - "name": "LogsPanelCfg", - "pluralName": "LogsPanelCfgs", - "machineName": "logspanelcfg", - "pluralMachineName": "logspanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "logspanelcfg", + "maturity": "planned", + "name": "LogsPanelCfg", + "pluralMachineName": "logspanelcfgs", + "pluralName": "LogsPanelCfgs", "schemaInterface": "PanelCfg" }, "lokidataquery": { - "name": "LokiDataQuery", - "pluralName": "LokiDataQuerys", - "machineName": "lokidataquery", - "pluralMachineName": "lokidataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "lokidataquery", + "maturity": "planned", + "name": "LokiDataQuery", + "pluralMachineName": "lokidataquerys", + "pluralName": "LokiDataQuerys", "schemaInterface": "DataQuery" }, "lokidatasourcecfg": { - "name": "LokiDataSourceCfg", - "pluralName": "LokiDataSourceCfgs", - "machineName": "lokidatasourcecfg", - "pluralMachineName": "lokidatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "lokidatasourcecfg", + "maturity": "planned", + "name": "LokiDataSourceCfg", + "pluralMachineName": "lokidatasourcecfgs", + "pluralName": "LokiDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "microsoftsqlserverdataquery": { - "name": "MicrosoftSQLServerDataQuery", - "pluralName": "MicrosoftSQLServerDataQuerys", - "machineName": "microsoftsqlserverdataquery", - "pluralMachineName": "microsoftsqlserverdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "microsoftsqlserverdataquery", + "maturity": "planned", + "name": "MicrosoftSQLServerDataQuery", + "pluralMachineName": "microsoftsqlserverdataquerys", + "pluralName": "MicrosoftSQLServerDataQuerys", "schemaInterface": "DataQuery" }, "microsoftsqlserverdatasourcecfg": { - "name": "MicrosoftSQLServerDataSourceCfg", - "pluralName": "MicrosoftSQLServerDataSourceCfgs", - "machineName": "microsoftsqlserverdatasourcecfg", - "pluralMachineName": "microsoftsqlserverdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "microsoftsqlserverdatasourcecfg", + "maturity": "planned", + "name": "MicrosoftSQLServerDataSourceCfg", + "pluralMachineName": "microsoftsqlserverdatasourcecfgs", + "pluralName": "MicrosoftSQLServerDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "mysqldataquery": { - "name": "MySQLDataQuery", - "pluralName": "MySQLDataQuerys", - "machineName": "mysqldataquery", - "pluralMachineName": "mysqldataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "mysqldataquery", + "maturity": "planned", + "name": "MySQLDataQuery", + "pluralMachineName": "mysqldataquerys", + "pluralName": "MySQLDataQuerys", "schemaInterface": "DataQuery" }, "mysqldatasourcecfg": { - "name": "MySQLDataSourceCfg", - "pluralName": "MySQLDataSourceCfgs", - "machineName": "mysqldatasourcecfg", - "pluralMachineName": "mysqldatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "mysqldatasourcecfg", + "maturity": "planned", + "name": "MySQLDataSourceCfg", + "pluralMachineName": "mysqldatasourcecfgs", + "pluralName": "MySQLDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "newspanelcfg": { - "name": "NewsPanelCfg", - "pluralName": "NewsPanelCfgs", - "machineName": "newspanelcfg", - "pluralMachineName": "newspanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/newspanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/news/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/news/panelcfg.gen.ts" + }, + "machineName": "newspanelcfg", + "maturity": "experimental", + "name": "NewsPanelCfg", + "pluralMachineName": "newspanelcfgs", + "pluralName": "NewsPanelCfgs", "schemaInterface": "PanelCfg" }, "nodegraphpanelcfg": { - "name": "NodeGraphPanelCfg", - "pluralName": "NodeGraphPanelCfgs", - "machineName": "nodegraphpanelcfg", - "pluralMachineName": "nodegraphpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "nodegraphpanelcfg", + "maturity": "planned", + "name": "NodeGraphPanelCfg", + "pluralMachineName": "nodegraphpanelcfgs", + "pluralName": "NodeGraphPanelCfgs", "schemaInterface": "PanelCfg" }, "parcadataquery": { - "name": "ParcaDataQuery", - "pluralName": "ParcaDataQuerys", - "machineName": "parcadataquery", - "pluralMachineName": "parcadataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "parcadataquery", + "maturity": "planned", + "name": "ParcaDataQuery", + "pluralMachineName": "parcadataquerys", + "pluralName": "ParcaDataQuerys", "schemaInterface": "DataQuery" }, "parcadatasourcecfg": { - "name": "ParcaDataSourceCfg", - "pluralName": "ParcaDataSourceCfgs", - "machineName": "parcadatasourcecfg", - "pluralMachineName": "parcadatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "parcadatasourcecfg", + "maturity": "planned", + "name": "ParcaDataSourceCfg", + "pluralMachineName": "parcadatasourcecfgs", + "pluralName": "ParcaDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "phlaredataquery": { - "name": "PhlareDataQuery", - "pluralName": "PhlareDataQuerys", - "machineName": "phlaredataquery", - "pluralMachineName": "phlaredataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "phlaredataquery", + "maturity": "planned", + "name": "PhlareDataQuery", + "pluralMachineName": "phlaredataquerys", + "pluralName": "PhlareDataQuerys", "schemaInterface": "DataQuery" }, "phlaredatasourcecfg": { - "name": "PhlareDataSourceCfg", - "pluralName": "PhlareDataSourceCfgs", - "machineName": "phlaredatasourcecfg", - "pluralMachineName": "phlaredatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "phlaredatasourcecfg", + "maturity": "planned", + "name": "PhlareDataSourceCfg", + "pluralMachineName": "phlaredatasourcecfgs", + "pluralName": "PhlareDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "piechartpanelcfg": { - "name": "PieChartPanelCfg", - "pluralName": "PieChartPanelCfgs", - "machineName": "piechartpanelcfg", - "pluralMachineName": "piechartpanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/grafana-bi-squad" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/piechartpanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/piechart/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/piechart/panelcfg.gen.ts" + }, + "machineName": "piechartpanelcfg", + "maturity": "experimental", + "name": "PieChartPanelCfg", + "pluralMachineName": "piechartpanelcfgs", + "pluralName": "PieChartPanelCfgs", "schemaInterface": "PanelCfg" }, "playlist": { - "name": "Playlist", - "pluralName": "Playlists", - "machineName": "playlist", - "pluralMachineName": "playlists", - "lineageIsGroup": false, - "maturity": "merged", - "currentVersion": [ - 0, - 0 - ] - }, - "postgresqldataquery": { - "name": "PostgreSQLDataQuery", - "pluralName": "PostgreSQLDataQuerys", - "machineName": "postgresqldataquery", - "pluralMachineName": "postgresqldataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "core", + "codeowners": [ + "grafana/grafana-as-code", + "grafana/grafana-bi-squad", + "grafana/plugins-platform-frontend", + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/playlist/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/playlist", + "schema": "https://github.com/grafana/grafana/tree/main/kinds/playlist/playlist_kind.cue", + "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts" + }, + "machineName": "playlist", + "maturity": "merged", + "name": "Playlist", + "pluralMachineName": "playlists", + "pluralName": "Playlists" + }, + "postgresqldataquery": { + "category": "composable", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "postgresqldataquery", + "maturity": "planned", + "name": "PostgreSQLDataQuery", + "pluralMachineName": "postgresqldataquerys", + "pluralName": "PostgreSQLDataQuerys", "schemaInterface": "DataQuery" }, "postgresqldatasourcecfg": { - "name": "PostgreSQLDataSourceCfg", - "pluralName": "PostgreSQLDataSourceCfgs", - "machineName": "postgresqldatasourcecfg", - "pluralMachineName": "postgresqldatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "postgresqldatasourcecfg", + "maturity": "planned", + "name": "PostgreSQLDataSourceCfg", + "pluralMachineName": "postgresqldatasourcecfgs", + "pluralName": "PostgreSQLDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "prometheusdataquery": { - "name": "PrometheusDataQuery", - "pluralName": "PrometheusDataQuerys", - "machineName": "prometheusdataquery", - "pluralMachineName": "prometheusdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "prometheusdataquery", + "maturity": "planned", + "name": "PrometheusDataQuery", + "pluralMachineName": "prometheusdataquerys", + "pluralName": "PrometheusDataQuerys", "schemaInterface": "DataQuery" }, "prometheusdatasourcecfg": { - "name": "PrometheusDataSourceCfg", - "pluralName": "PrometheusDataSourceCfgs", - "machineName": "prometheusdatasourcecfg", - "pluralMachineName": "prometheusdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "prometheusdatasourcecfg", + "maturity": "planned", + "name": "PrometheusDataSourceCfg", + "pluralMachineName": "prometheusdatasourcecfgs", + "pluralName": "PrometheusDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "query": { - "name": "Query", - "pluralName": "Querys", - "machineName": "query", - "pluralMachineName": "querys", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "queryhistory": { - "name": "QueryHistory", - "pluralName": "QueryHistorys", - "machineName": "queryhistory", - "pluralMachineName": "queryhistorys", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "serviceaccount": { - "name": "ServiceAccount", - "pluralName": "ServiceAccounts", - "machineName": "serviceaccount", - "pluralMachineName": "serviceaccounts", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "statpanelcfg": { - "name": "StatPanelCfg", - "pluralName": "StatPanelCfgs", - "machineName": "statpanelcfg", - "pluralMachineName": "statpanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "core", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "query", + "maturity": "planned", + "name": "Query", + "pluralMachineName": "querys", + "pluralName": "Querys" + }, + "queryhistory": { + "category": "core", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "queryhistory", + "maturity": "planned", + "name": "QueryHistory", + "pluralMachineName": "queryhistorys", + "pluralName": "QueryHistorys" + }, + "serviceaccount": { + "category": "core", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "serviceaccount", + "maturity": "planned", + "name": "ServiceAccount", + "pluralMachineName": "serviceaccounts", + "pluralName": "ServiceAccounts" + }, + "statpanelcfg": { + "category": "composable", + "codeowners": [ + "grafana/user-essentials" + ], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/statpanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/stat/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/stat/panelcfg.gen.ts" + }, + "machineName": "statpanelcfg", + "maturity": "experimental", + "name": "StatPanelCfg", + "pluralMachineName": "statpanelcfgs", + "pluralName": "StatPanelCfgs", "schemaInterface": "PanelCfg" }, "tableoldpanelcfg": { - "name": "TableOldPanelCfg", - "pluralName": "TableOldPanelCfgs", - "machineName": "tableoldpanelcfg", - "pluralMachineName": "tableoldpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "tableoldpanelcfg", + "maturity": "planned", + "name": "TableOldPanelCfg", + "pluralMachineName": "tableoldpanelcfgs", + "pluralName": "TableOldPanelCfgs", "schemaInterface": "PanelCfg" }, "team": { - "name": "Team", - "pluralName": "Teams", - "machineName": "team", - "pluralMachineName": "teams", - "lineageIsGroup": false, - "maturity": "merged", - "currentVersion": [ - 0, - 0 - ] - }, - "tempodataquery": { - "name": "TempoDataQuery", - "pluralName": "TempoDataQuerys", - "machineName": "tempodataquery", - "pluralMachineName": "tempodataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "core", + "codeowners": [ + "grafana/grafana-as-code", + "grafana/grafana-bi-squad", + "grafana/plugins-platform-frontend", + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 7, + "lineageIsGroup": false, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/team/schema-reference", + "go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/team", + "schema": "https://github.com/grafana/grafana/tree/main/kinds/team/team_kind.cue", + "ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/team/x/team_types.gen.ts" + }, + "machineName": "team", + "maturity": "merged", + "name": "Team", + "pluralMachineName": "teams", + "pluralName": "Teams" + }, + "tempodataquery": { + "category": "composable", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "tempodataquery", + "maturity": "planned", + "name": "TempoDataQuery", + "pluralMachineName": "tempodataquerys", + "pluralName": "TempoDataQuerys", "schemaInterface": "DataQuery" }, "tempodatasourcecfg": { - "name": "TempoDataSourceCfg", - "pluralName": "TempoDataSourceCfgs", - "machineName": "tempodatasourcecfg", - "pluralMachineName": "tempodatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "tempodatasourcecfg", + "maturity": "planned", + "name": "TempoDataSourceCfg", + "pluralMachineName": "tempodatasourcecfgs", + "pluralName": "TempoDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "testdatadbdataquery": { - "name": "TestDataDBDataQuery", - "pluralName": "TestDataDBDataQuerys", - "machineName": "testdatadbdataquery", - "pluralMachineName": "testdatadbdataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "testdatadbdataquery", + "maturity": "planned", + "name": "TestDataDBDataQuery", + "pluralMachineName": "testdatadbdataquerys", + "pluralName": "TestDataDBDataQuerys", "schemaInterface": "DataQuery" }, "testdatadbdatasourcecfg": { - "name": "TestDataDBDataSourceCfg", - "pluralName": "TestDataDBDataSourceCfgs", - "machineName": "testdatadbdatasourcecfg", - "pluralMachineName": "testdatadbdatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "testdatadbdatasourcecfg", + "maturity": "planned", + "name": "TestDataDBDataSourceCfg", + "pluralMachineName": "testdatadbdatasourcecfgs", + "pluralName": "TestDataDBDataSourceCfgs", "schemaInterface": "DataSourceCfg" }, "textpanelcfg": { - "name": "TextPanelCfg", - "pluralName": "TextPanelCfgs", - "machineName": "textpanelcfg", - "pluralMachineName": "textpanelcfgs", - "lineageIsGroup": true, - "maturity": "experimental", + "category": "composable", + "codeowners": [ + "grafana/user-essentials" + ], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "https://grafana.com/docs/grafana/next/developers/kinds/composable/textpanelcfg/schema-reference", + "go": "n/a", + "schema": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/text/panelcfg.cue", + "ts": "https://github.com/grafana/grafana/tree/main/public/app/plugins/panel/text/panelcfg.gen.ts" + }, + "machineName": "textpanelcfg", + "maturity": "experimental", + "name": "TextPanelCfg", + "pluralMachineName": "textpanelcfgs", + "pluralName": "TextPanelCfgs", "schemaInterface": "PanelCfg" }, "thumb": { - "name": "Thumb", - "pluralName": "Thumbs", - "machineName": "thumb", - "pluralMachineName": "thumbs", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "tracespanelcfg": { - "name": "TracesPanelCfg", - "pluralName": "TracesPanelCfgs", - "machineName": "tracespanelcfg", - "pluralMachineName": "tracespanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "core", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "thumb", + "maturity": "planned", + "name": "Thumb", + "pluralMachineName": "thumbs", + "pluralName": "Thumbs" + }, + "tracespanelcfg": { + "category": "composable", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "tracespanelcfg", + "maturity": "planned", + "name": "TracesPanelCfg", + "pluralMachineName": "tracespanelcfgs", + "pluralName": "TracesPanelCfgs", "schemaInterface": "PanelCfg" }, "user": { - "name": "User", - "pluralName": "Users", - "machineName": "user", - "pluralMachineName": "users", - "lineageIsGroup": false, - "maturity": "planned", - "currentVersion": [ - 0, - 0 - ] - }, - "welcomepanelcfg": { - "name": "WelcomePanelCfg", - "pluralName": "WelcomePanelCfgs", - "machineName": "welcomepanelcfg", - "pluralMachineName": "welcomepanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "core", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "user", + "maturity": "planned", + "name": "User", + "pluralMachineName": "users", + "pluralName": "Users" + }, + "welcomepanelcfg": { + "category": "composable", + "codeowners": [], + "currentVersion": [ + 0, + 0 + ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "welcomepanelcfg", + "maturity": "planned", + "name": "WelcomePanelCfg", + "pluralMachineName": "welcomepanelcfgs", + "pluralName": "WelcomePanelCfgs", "schemaInterface": "PanelCfg" }, "xychartpanelcfg": { - "name": "XYChartPanelCfg", - "pluralName": "XYChartPanelCfgs", - "machineName": "xychartpanelcfg", - "pluralMachineName": "xychartpanelcfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "xychartpanelcfg", + "maturity": "planned", + "name": "XYChartPanelCfg", + "pluralMachineName": "xychartpanelcfgs", + "pluralName": "XYChartPanelCfgs", "schemaInterface": "PanelCfg" }, "zipkindataquery": { - "name": "ZipkinDataQuery", - "pluralName": "ZipkinDataQuerys", - "machineName": "zipkindataquery", - "pluralMachineName": "zipkindataquerys", - "lineageIsGroup": false, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": false, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "zipkindataquery", + "maturity": "planned", + "name": "ZipkinDataQuery", + "pluralMachineName": "zipkindataquerys", + "pluralName": "ZipkinDataQuerys", "schemaInterface": "DataQuery" }, "zipkindatasourcecfg": { - "name": "ZipkinDataSourceCfg", - "pluralName": "ZipkinDataSourceCfgs", - "machineName": "zipkindatasourcecfg", - "pluralMachineName": "zipkindatasourcecfgs", - "lineageIsGroup": true, - "maturity": "planned", + "category": "composable", + "codeowners": [], "currentVersion": [ 0, 0 ], + "grafanaMaturityCount": 0, + "lineageIsGroup": true, + "links": { + "docs": "n/a", + "go": "n/a", + "schema": "n/a", + "ts": "n/a" + }, + "machineName": "zipkindatasourcecfg", + "maturity": "planned", + "name": "ZipkinDataSourceCfg", + "pluralMachineName": "zipkindatasourcecfgs", + "pluralName": "ZipkinDataSourceCfgs", "schemaInterface": "DataSourceCfg" } }, @@ -1067,15 +1770,15 @@ "merged": { "name": "merged", "items": [ + "alertgroupspanelcfg", "playlist", "team" ], - "count": 2 + "count": 3 }, "planned": { "name": "planned", "items": [ - "alertgroupspanelcfg", "alertlistpanelcfg", "alertmanagerdataquery", "alertmanagerdatasourcecfg", @@ -1137,7 +1840,7 @@ "zipkindataquery", "zipkindatasourcecfg" ], - "count": 61 + "count": 60 }, "stable": { "name": "stable", diff --git a/pkg/kindsys/util.go b/pkg/kindsys/util.go new file mode 100644 index 00000000000..b7d03085e79 --- /dev/null +++ b/pkg/kindsys/util.go @@ -0,0 +1,24 @@ +package kindsys + +// Ptr returns a pointer to a value of an arbitrary type. +// +// This function is provided to compensate for Grafana's Go code generation that +// represents optional fields using pointers. +// +// Pointers are the only technically [correct, non-ambiguous] way of +// representing an optional field in Go's type system. However, Go does not +// allow taking the address of certain primitive types inline. That is, +// this is invalid Go code: +// +// var str *string +// str = &"colorless green ideas sleep furiously" +// +// This func allows making such declarations in a single line: +// +// var str *string +// str = kindsys.Ptr("colorless green ideas sleep furiously") +// +// [correct, non-ambiguous]: https://github.com/grafana/grok/issues/1 +func Ptr[T any](v T) *T { + return &v +} diff --git a/pkg/models/plugin_settings.go b/pkg/models/plugin_settings.go deleted file mode 100644 index e9e46191f03..00000000000 --- a/pkg/models/plugin_settings.go +++ /dev/null @@ -1,70 +0,0 @@ -package models - -import ( - "errors" - "time" -) - -var ( - ErrPluginSettingNotFound = errors.New("plugin setting not found") -) - -type PluginSetting struct { - Id int64 - PluginId string - OrgId int64 - Enabled bool - Pinned bool - JsonData map[string]interface{} - SecureJsonData map[string][]byte - PluginVersion string - - Created time.Time - Updated time.Time -} - -type PluginSettingInfo struct { - PluginID string `xorm:"plugin_id"` - OrgID int64 `xorm:"org_id"` - Enabled bool `xorm:"enabled"` - Pinned bool `xorm:"pinned"` - PluginVersion string `xorm:"plugin_id"` -} - -// ---------------------- -// COMMANDS - -// Also acts as api DTO -type UpdatePluginSettingCmd struct { - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - JsonData map[string]interface{} `json:"jsonData"` - SecureJsonData map[string]string `json:"secureJsonData"` - PluginVersion string `json:"version"` - - PluginId string `json:"-"` - OrgId int64 `json:"-"` - EncryptedSecureJsonData map[string][]byte `json:"-"` -} - -// specific command, will only update version -type UpdatePluginSettingVersionCmd struct { - PluginVersion string - PluginId string `json:"-"` - OrgId int64 `json:"-"` -} - -// --------------------- -// QUERIES - -type GetPluginSettingByIdQuery struct { - PluginId string - OrgId int64 - Result *PluginSetting -} - -type PluginStateChangedEvent struct { - PluginId string - OrgId int64 - Enabled bool -} diff --git a/pkg/plugins/backendplugin/coreplugin/core_plugin.go b/pkg/plugins/backendplugin/coreplugin/core_plugin.go index c59bdf92549..c237f36e26c 100644 --- a/pkg/plugins/backendplugin/coreplugin/core_plugin.go +++ b/pkg/plugins/backendplugin/coreplugin/core_plugin.go @@ -64,6 +64,10 @@ func (cp *corePlugin) IsDecommissioned() bool { return false } +func (cp *corePlugin) Target() backendplugin.Target { + return backendplugin.TargetInMemory +} + func (cp *corePlugin) CollectMetrics(_ context.Context, _ *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { return nil, backendplugin.ErrMethodNotImplemented } diff --git a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go index f489be62795..880293ef138 100644 --- a/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go +++ b/pkg/plugins/backendplugin/grpcplugin/grpc_plugin.go @@ -120,6 +120,10 @@ func (p *grpcPlugin) IsDecommissioned() bool { return p.decommissioned } +func (p *grpcPlugin) Target() backendplugin.Target { + return backendplugin.TargetLocal +} + func (p *grpcPlugin) getPluginClient() (pluginClient, bool) { p.mutex.RLock() if p.client == nil || p.client.Exited() || p.pluginClient == nil { diff --git a/pkg/plugins/backendplugin/ifaces.go b/pkg/plugins/backendplugin/ifaces.go index 7ee21d9dcc5..bfade386006 100644 --- a/pkg/plugins/backendplugin/ifaces.go +++ b/pkg/plugins/backendplugin/ifaces.go @@ -17,9 +17,19 @@ type Plugin interface { Exited() bool Decommission() error IsDecommissioned() bool + Target() Target backend.CollectMetricsHandler backend.CheckHealthHandler backend.QueryDataHandler backend.CallResourceHandler backend.StreamHandler } + +type Target string + +const ( + TargetNone Target = "none" + TargetUnknown Target = "unknown" + TargetInMemory Target = "in_memory" + TargetLocal Target = "local" +) diff --git a/pkg/plugins/backendplugin/instrumentation/instrumentation.go b/pkg/plugins/backendplugin/instrumentation/instrumentation.go index 423d8884dc8..843b85de2a5 100644 --- a/pkg/plugins/backendplugin/instrumentation/instrumentation.go +++ b/pkg/plugins/backendplugin/instrumentation/instrumentation.go @@ -6,11 +6,12 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/plugins/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/plugins/backendplugin" ) var ( @@ -18,20 +19,20 @@ var ( Namespace: "grafana", Name: "plugin_request_total", Help: "The total amount of plugin requests", - }, []string{"plugin_id", "endpoint", "status"}) + }, []string{"plugin_id", "endpoint", "status", "target"}) pluginRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ Namespace: "grafana", Name: "plugin_request_duration_milliseconds", Help: "Plugin request duration", Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, - }, []string{"plugin_id", "endpoint"}) + }, []string{"plugin_id", "endpoint", "target"}) ) var logger log.Logger = log.New("plugin.instrumentation") // instrumentPluginRequest instruments success rate and latency of `fn` -func instrumentPluginRequest(ctx context.Context, cfg *config.Cfg, pluginCtx *backend.PluginContext, endpoint string, fn func() error) error { +func instrumentPluginRequest(ctx context.Context, cfg Cfg, pluginCtx *backend.PluginContext, endpoint string, fn func() error) error { status := "ok" start := time.Now() @@ -44,8 +45,8 @@ func instrumentPluginRequest(ctx context.Context, cfg *config.Cfg, pluginCtx *ba } elapsed := time.Since(start) - pluginRequestDuration.WithLabelValues(pluginCtx.PluginID, endpoint).Observe(float64(elapsed / time.Millisecond)) - pluginRequestCounter.WithLabelValues(pluginCtx.PluginID, endpoint, status).Inc() + pluginRequestDuration.WithLabelValues(pluginCtx.PluginID, endpoint, string(cfg.Target)).Observe(float64(elapsed / time.Millisecond)) + pluginRequestCounter.WithLabelValues(pluginCtx.PluginID, endpoint, status, string(cfg.Target)).Inc() if cfg.LogDatasourceRequests { logParams := []interface{}{ @@ -77,22 +78,27 @@ func instrumentPluginRequest(ctx context.Context, cfg *config.Cfg, pluginCtx *ba return err } +type Cfg struct { + LogDatasourceRequests bool + Target backendplugin.Target +} + // InstrumentCollectMetrics instruments collectMetrics. -func InstrumentCollectMetrics(ctx context.Context, req *backend.PluginContext, cfg *config.Cfg, fn func() error) error { +func InstrumentCollectMetrics(ctx context.Context, req *backend.PluginContext, cfg Cfg, fn func() error) error { return instrumentPluginRequest(ctx, cfg, req, "collectMetrics", fn) } // InstrumentCheckHealthRequest instruments checkHealth. -func InstrumentCheckHealthRequest(ctx context.Context, req *backend.PluginContext, cfg *config.Cfg, fn func() error) error { +func InstrumentCheckHealthRequest(ctx context.Context, req *backend.PluginContext, cfg Cfg, fn func() error) error { return instrumentPluginRequest(ctx, cfg, req, "checkHealth", fn) } // InstrumentCallResourceRequest instruments callResource. -func InstrumentCallResourceRequest(ctx context.Context, req *backend.PluginContext, cfg *config.Cfg, fn func() error) error { +func InstrumentCallResourceRequest(ctx context.Context, req *backend.PluginContext, cfg Cfg, fn func() error) error { return instrumentPluginRequest(ctx, cfg, req, "callResource", fn) } // InstrumentQueryDataRequest instruments success rate and latency of query data requests. -func InstrumentQueryDataRequest(ctx context.Context, req *backend.PluginContext, cfg *config.Cfg, fn func() error) error { +func InstrumentQueryDataRequest(ctx context.Context, req *backend.PluginContext, cfg Cfg, fn func() error) error { return instrumentPluginRequest(ctx, cfg, req, "queryData", fn) } diff --git a/pkg/plugins/codegen/jenny_plugintstypes.go b/pkg/plugins/codegen/jenny_plugintstypes.go index e76db2bea62..5f320d7229f 100644 --- a/pkg/plugins/codegen/jenny_plugintstypes.go +++ b/pkg/plugins/codegen/jenny_plugintstypes.go @@ -3,6 +3,7 @@ package codegen import ( "fmt" "path/filepath" + "strings" "github.com/grafana/codejen" tsast "github.com/grafana/cuetsy/ts/ast" @@ -40,11 +41,10 @@ func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) { } } - slotname := decl.SchemaInterface.Name() v := decl.Lineage.Latest().Version() tsf.Nodes = append(tsf.Nodes, tsast.Raw{ - Data: fmt.Sprintf("export const %sModelVersion = Object.freeze([%v, %v]);", slotname, v[0], v[1]), + Data: fmt.Sprintf("export const %sModelVersion = Object.freeze([%v, %v]);", decl.SchemaInterface.Name(), v[0], v[1]), }) jf, err := j.inner.Generate(decl) @@ -56,7 +56,7 @@ func (j *ptsJenny) Generate(decl *pfs.PluginDecl) (*codejen.File, error) { Data: string(jf.Data), }) - path := filepath.Join(j.root, decl.PluginPath, "models.gen.ts") + path := filepath.Join(j.root, decl.PluginPath, fmt.Sprintf("%s.gen.ts", strings.ToLower(decl.SchemaInterface.Name()))) data := []byte(tsf.String()) data = data[:len(data)-1] // remove the additional line break added by the inner jenny diff --git a/pkg/plugins/licensing/licensing.go b/pkg/plugins/licensing/licensing.go index 2b17953ebf8..9a00e5828c2 100644 --- a/pkg/plugins/licensing/licensing.go +++ b/pkg/plugins/licensing/licensing.go @@ -3,16 +3,16 @@ package licensing import ( "fmt" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/setting" ) type Service struct { licensePath string - license models.Licensing + license licensing.Licensing } -func ProvideLicensing(cfg *setting.Cfg, l models.Licensing) *Service { +func ProvideLicensing(cfg *setting.Cfg, l licensing.Licensing) *Service { return &Service{ licensePath: cfg.EnterpriseLicensePath, license: l, @@ -21,7 +21,7 @@ func ProvideLicensing(cfg *setting.Cfg, l models.Licensing) *Service { func (l Service) Environment() []string { var env []string - if envProvider, ok := l.license.(models.LicenseEnvironment); ok { + if envProvider, ok := l.license.(licensing.LicenseEnvironment); ok { for k, v := range envProvider.Environment() { env = append(env, fmt.Sprintf("%s=%s", k, v)) } diff --git a/pkg/plugins/manager/client/client.go b/pkg/plugins/manager/client/client.go index b9262ac41d7..4663cddef02 100644 --- a/pkg/plugins/manager/client/client.go +++ b/pkg/plugins/manager/client/client.go @@ -35,14 +35,17 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return nil, fmt.Errorf("req cannot be nil") } - plugin, exists := s.plugin(ctx, req.PluginContext.PluginID) + p, exists := s.plugin(ctx, req.PluginContext.PluginID) if !exists { return nil, plugins.ErrPluginNotRegistered.Errorf("%w", backendplugin.ErrPluginNotRegistered) } var resp *backend.QueryDataResponse - err := instrumentation.InstrumentQueryDataRequest(ctx, &req.PluginContext, s.cfg, func() (innerErr error) { - resp, innerErr = plugin.QueryData(ctx, req) + err := instrumentation.InstrumentQueryDataRequest(ctx, &req.PluginContext, instrumentation.Cfg{ + LogDatasourceRequests: s.cfg.LogDatasourceRequests, + Target: p.Target(), + }, func() (innerErr error) { + resp, innerErr = p.QueryData(ctx, req) return }) @@ -83,7 +86,10 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq if !exists { return backendplugin.ErrPluginNotRegistered } - err := instrumentation.InstrumentCallResourceRequest(ctx, &req.PluginContext, s.cfg, func() error { + err := instrumentation.InstrumentCallResourceRequest(ctx, &req.PluginContext, instrumentation.Cfg{ + LogDatasourceRequests: s.cfg.LogDatasourceRequests, + Target: p.Target(), + }, func() error { removeConnectionHeaders(req.Headers) removeHopByHopHeaders(req.Headers) @@ -120,7 +126,10 @@ func (s *Service) CollectMetrics(ctx context.Context, req *backend.CollectMetric } var resp *backend.CollectMetricsResult - err := instrumentation.InstrumentCollectMetrics(ctx, &req.PluginContext, s.cfg, func() (innerErr error) { + err := instrumentation.InstrumentCollectMetrics(ctx, &req.PluginContext, instrumentation.Cfg{ + LogDatasourceRequests: s.cfg.LogDatasourceRequests, + Target: p.Target(), + }, func() (innerErr error) { resp, innerErr = p.CollectMetrics(ctx, req) return }) @@ -142,7 +151,10 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque } var resp *backend.CheckHealthResult - err := instrumentation.InstrumentCheckHealthRequest(ctx, &req.PluginContext, s.cfg, func() (innerErr error) { + err := instrumentation.InstrumentCheckHealthRequest(ctx, &req.PluginContext, instrumentation.Cfg{ + LogDatasourceRequests: s.cfg.LogDatasourceRequests, + Target: p.Target(), + }, func() (innerErr error) { resp, innerErr = p.CheckHealth(ctx, req) return }) diff --git a/pkg/plugins/plugincontext/plugincontext.go b/pkg/plugins/plugincontext/plugincontext.go index 97932755fb3..9abfd7ab7c6 100644 --- a/pkg/plugins/plugincontext/plugincontext.go +++ b/pkg/plugins/plugincontext/plugincontext.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/services/datasources" @@ -80,9 +79,9 @@ func (p *Provider) pluginContext(ctx context.Context, pluginID string, user *use ps, err := p.getCachedPluginSettings(ctx, pluginID, user) if err != nil { - // models.ErrPluginSettingNotFound is expected if there's no row found for plugin setting in database (if non-app plugin). + // pluginsettings.ErrPluginSettingNotFound is expected if there's no row found for plugin setting in database (if non-app plugin). // If it's not this expected error something is wrong with cache or database and we return the error to the client. - if !errors.Is(err, models.ErrPluginSettingNotFound) { + if !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) { return backend.PluginContext{}, false, fmt.Errorf("%v: %w", "Failed to get plugin settings", err) } } else { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 81bfbf11d6c..b9d1a210583 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -267,6 +267,16 @@ func (p *Plugin) Exited() bool { return false } +func (p *Plugin) Target() backendplugin.Target { + if !p.Backend { + return backendplugin.TargetNone + } + if p.client == nil { + return backendplugin.TargetUnknown + } + return p.client.Target() +} + func (p *Plugin) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { pluginClient, ok := p.Client() if !ok { diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index e8e6cf0b75e..65d2b5eed09 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -6,7 +6,6 @@ package server import ( "github.com/google/wire" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/server/backgroundsvcs" @@ -44,7 +43,7 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(auth.UserTokenService), new(*authimpl.UserAuthTokenService)), wire.Bind(new(auth.UserTokenBackgroundService), new(*authimpl.UserAuthTokenService)), licensing.ProvideService, - wire.Bind(new(models.Licensing), new(*licensing.OSSLicensingService)), + wire.Bind(new(licensing.Licensing), new(*licensing.OSSLicensingService)), setting.ProvideProvider, wire.Bind(new(setting.Provider), new(*setting.OSSImpl)), acimpl.ProvideService, @@ -54,7 +53,7 @@ var wireExtsBasicSet = wire.NewSet( thumbs.ProvideCrawlerAuthSetupService, wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)), validations.ProvideValidator, - wire.Bind(new(models.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)), + wire.Bind(new(validations.PluginRequestValidator), new(*validations.OSSPluginRequestValidator)), provisioning.ProvideService, wire.Bind(new(provisioning.ProvisioningService), new(*provisioning.ProvisioningServiceImpl)), backgroundsvcs.ProvideBackgroundServiceRegistry, diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index fe5e57f3473..06ab9b18d3d 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -11,13 +11,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/accesscontrol/database" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -278,7 +278,7 @@ func TestService_DeclarePluginRoles(t *testing.T) { func TestService_RegisterFixedRoles(t *testing.T) { tests := []struct { name string - token models.Licensing + token licensing.Licensing registrations []accesscontrol.RoleRegistration wantErr bool }{ diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index bf3d8fe217c..a4ede7777ab 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/serviceaccounts/retriever" "github.com/grafana/grafana/pkg/services/team" @@ -40,7 +41,7 @@ var ( func ProvideTeamPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, - ac accesscontrol.AccessControl, license models.Licensing, service accesscontrol.Service, + ac accesscontrol.AccessControl, license licensing.Licensing, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*TeamPermissionsService, error) { options := resourcepermissions.Options{ @@ -114,7 +115,7 @@ var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.Act func ProvideDashboardPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, - license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*dashboards.Dashboard, error) { @@ -193,7 +194,7 @@ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFol func ProvideFolderPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, accesscontrol accesscontrol.AccessControl, - license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, + license licensing.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*FolderPermissionsService, error) { options := resourcepermissions.Options{ @@ -284,7 +285,7 @@ type ServiceAccountPermissionsService struct { func ProvideServiceAccountPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql db.DB, ac accesscontrol.AccessControl, - license models.Licensing, serviceAccountRetrieverService *retriever.Service, service accesscontrol.Service, + license licensing.Licensing, serviceAccountRetrieverService *retriever.Service, service accesscontrol.Service, teamService team.Service, userService user.Service, ) (*ServiceAccountPermissionsService, error) { options := resourcepermissions.Options{ diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index 8566492fd33..af56564f4a1 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -7,8 +7,8 @@ import ( "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/team" "github.com/grafana/grafana/pkg/services/user" @@ -49,7 +49,7 @@ type Store interface { } func New( - options Options, cfg *setting.Cfg, router routing.RouteRegister, license models.Licensing, + options Options, cfg *setting.Cfg, router routing.RouteRegister, license licensing.Licensing, ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore db.DB, teamService team.Service, userService user.Service, ) (*Service, error) { @@ -104,7 +104,7 @@ type Service struct { service accesscontrol.Service store Store api *api - license models.Licensing + license licensing.Licensing options Options permissions []string diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 316b3771305..c20c821bb0d 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -15,13 +15,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/infra/usagestats" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/encryption" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util/ticker" @@ -32,7 +32,7 @@ import ( // are sent. type AlertEngine struct { RenderService rendering.Service - RequestValidator models.PluginRequestValidator + RequestValidator validations.PluginRequestValidator DataService legacydata.RequestHandler Cfg *setting.Cfg @@ -58,7 +58,7 @@ func (e *AlertEngine) IsDisabled() bool { } // ProvideAlertEngine returns a new AlertEngine. -func ProvideAlertEngine(renderer rendering.Service, requestValidator models.PluginRequestValidator, +func ProvideAlertEngine(renderer rendering.Service, requestValidator validations.PluginRequestValidator, dataService legacydata.RequestHandler, usageStatsService usagestats.Service, encryptionService encryption.Internal, notificationService *notifications.NotificationService, tracer tracing.Tracer, store AlertStore, cfg *setting.Cfg, dashAlertExtractor DashAlertExtractor, dashboardService dashboards.DashboardService, cacheService *localcache.CacheService, dsService datasources.DataSourceService, annotationsRepo annotations.Repository) *AlertEngine { diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index dff04f79e59..d5375cae94d 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -7,11 +7,11 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" alertmodels "github.com/grafana/grafana/pkg/services/alerting/models" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" ) @@ -37,7 +37,7 @@ type EvalContext struct { NoDataFound bool PrevAlertState alertmodels.AlertStateType - RequestValidator models.PluginRequestValidator + RequestValidator validations.PluginRequestValidator Ctx context.Context @@ -48,7 +48,7 @@ type EvalContext struct { } // NewEvalContext is the EvalContext constructor. -func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator models.PluginRequestValidator, +func NewEvalContext(alertCtx context.Context, rule *Rule, requestValidator validations.PluginRequestValidator, alertStore AlertStore, dashboardService dashboards.DashboardService, dsService datasources.DataSourceService, annotationRepo annotations.Repository) *EvalContext { return &EvalContext{ Ctx: alertCtx, diff --git a/pkg/services/datasourceproxy/datasourceproxy.go b/pkg/services/datasourceproxy/datasourceproxy.go index 0590376c741..c6346fa9682 100644 --- a/pkg/services/datasourceproxy/datasourceproxy.go +++ b/pkg/services/datasourceproxy/datasourceproxy.go @@ -17,12 +17,13 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/oauthtoken" "github.com/grafana/grafana/pkg/services/secrets" + "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) -func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator models.PluginRequestValidator, +func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator validations.PluginRequestValidator, pluginStore plugins.Store, cfg *setting.Cfg, httpClientProvider httpclient.Provider, oauthTokenService *oauthtoken.Service, dsService datasources.DataSourceService, tracer tracing.Tracer, secretsService secrets.Service) *DataSourceProxyService { @@ -41,7 +42,7 @@ func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator m type DataSourceProxyService struct { DataSourceCache datasources.CacheService - PluginRequestValidator models.PluginRequestValidator + PluginRequestValidator validations.PluginRequestValidator pluginStore plugins.Store Cfg *setting.Cfg HTTPClientProvider httpclient.Provider @@ -131,7 +132,7 @@ func (p *DataSourceProxyService) proxyDatasourceRequest(c *models.ReqContext, ds proxy.HandleRequest() } -var proxyPathRegexp = regexp.MustCompile(`^\/api\/datasources\/proxy\/([\d]+|uid\/[\w]+)\/?`) +var proxyPathRegexp = regexp.MustCompile(`^\/api\/datasources\/proxy\/([\d]+|uid\/[\w-]+)\/?`) func extractProxyPath(originalRawPath string) string { return proxyPathRegexp.ReplaceAllString(originalRawPath, "") diff --git a/pkg/services/datasourceproxy/datasourceproxy_test.go b/pkg/services/datasourceproxy/datasourceproxy_test.go index 6567ba579c5..57da4aa33c0 100644 --- a/pkg/services/datasourceproxy/datasourceproxy_test.go +++ b/pkg/services/datasourceproxy/datasourceproxy_test.go @@ -32,6 +32,14 @@ func TestDataProxy(t *testing.T) { "/api/datasources/proxy/uid/26MI0wZ7k/some/thing", "some/thing", }, + { + "/api/datasources/proxy/uid/pUWo-no4k/search", + "search", + }, + { + "/api/datasources/proxy/uid/pUWo_no4k/search", + "search", + }, { "/api/datasources/proxy/uid/26MI0wZ7k/api/services/afsd%2Fafsd/operations", "api/services/afsd%2Fafsd/operations", diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index ad96139ff58..19a9f91c6e3 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -6,10 +6,10 @@ import ( "net/http" "reflect" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/licensing" ) var ( @@ -18,7 +18,7 @@ var ( type FeatureManager struct { isDevMod bool - licensing models.Licensing + licensing licensing.Licensing flags map[string]*FeatureFlag enabled map[string]bool // only the "on" values config string // path to config file diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 06927320c33..77d111cedf0 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -397,6 +397,12 @@ var ( Description: "Rule backtesting API for alerting", State: FeatureStateAlpha, }, + { + Name: "editPanelCSVDragAndDrop", + Description: "Enables drag and drop for CSV and Excel files", + FrontendOnly: true, + State: FeatureStateAlpha, + }, { Name: "alertingNoNormalState", Description: "Stop maintaining state of alerts that are not firing", @@ -408,5 +414,11 @@ var ( Description: "Azure multiple resource picker", State: FeatureStateAlpha, }, + { + Name: "topNavCommandPalette", + Description: "Launch the Command Palette from the top navigation search box", + State: FeatureStateBeta, + FrontendOnly: true, + }, } ) diff --git a/pkg/services/featuremgmt/service.go b/pkg/services/featuremgmt/service.go index d3e5622f451..d963d9e2f28 100644 --- a/pkg/services/featuremgmt/service.go +++ b/pkg/services/featuremgmt/service.go @@ -5,12 +5,12 @@ import ( "os" "path/filepath" - "github.com/grafana/grafana/pkg/infra/log" - - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/setting" ) var ( @@ -22,7 +22,7 @@ var ( }, []string{"name"}) ) -func ProvideManagerService(cfg *setting.Cfg, licensing models.Licensing) (*FeatureManager, error) { +func ProvideManagerService(cfg *setting.Cfg, licensing licensing.Licensing) (*FeatureManager, error) { mgmt := &FeatureManager{ isDevMod: setting.Env != setting.Prod, licensing: licensing, diff --git a/pkg/services/featuremgmt/service_test.go b/pkg/services/featuremgmt/service_test.go index 1085ea91f2c..660dbda8136 100644 --- a/pkg/services/featuremgmt/service_test.go +++ b/pkg/services/featuremgmt/service_test.go @@ -3,9 +3,10 @@ package featuremgmt import ( "testing" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/licensing" + "github.com/grafana/grafana/pkg/setting" ) func TestFeatureService(t *testing.T) { @@ -47,7 +48,7 @@ func TestFeatureService(t *testing.T) { } var ( - _ models.Licensing = (*stubLicenseServier)(nil) + _ licensing.Licensing = (*stubLicenseServier)(nil) ) type stubLicenseServier struct { diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index af44efff9a3..3ff5c49df92 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -291,6 +291,10 @@ const ( // Rule backtesting API for alerting FlagAlertingBacktesting = "alertingBacktesting" + // FlagEditPanelCSVDragAndDrop + // Enables drag and drop for CSV and Excel files + FlagEditPanelCSVDragAndDrop = "editPanelCSVDragAndDrop" + // FlagAlertingNoNormalState // Stop maintaining state of alerts that are not firing FlagAlertingNoNormalState = "alertingNoNormalState" @@ -298,4 +302,8 @@ const ( // FlagAzureMultipleResourcePicker // Azure multiple resource picker FlagAzureMultipleResourcePicker = "azureMultipleResourcePicker" + + // FlagTopNavCommandPalette + // Launch the Command Palette from the top navigation search box + FlagTopNavCommandPalette = "topNavCommandPalette" ) diff --git a/pkg/services/licensing/licensingtest/fake.go b/pkg/services/licensing/licensingtest/fake.go index f5d8454ece1..7068217abea 100644 --- a/pkg/services/licensing/licensingtest/fake.go +++ b/pkg/services/licensing/licensingtest/fake.go @@ -3,10 +3,10 @@ package licensingtest import ( "github.com/stretchr/testify/mock" - "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/licensing" ) -var _ models.Licensing = new(FakeLicensing) +var _ licensing.Licensing = new(FakeLicensing) func NewFakeLicensing() *FakeLicensing { return &FakeLicensing{&mock.Mock{}} diff --git a/pkg/models/licensing.go b/pkg/services/licensing/models.go similarity index 96% rename from pkg/models/licensing.go rename to pkg/services/licensing/models.go index e49eb7a3288..4d2ad518f6b 100644 --- a/pkg/models/licensing.go +++ b/pkg/services/licensing/models.go @@ -1,4 +1,4 @@ -package models +package licensing type Licensing interface { // Expiry returns the unix epoch timestamp when the license expires, or 0 if no valid license is provided diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 5e7a3d024dd..a5a1215e7f7 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -218,14 +218,6 @@ func (s *ServiceImpl) getHomeNode(c *models.ReqContext, prefs *pref.Preference) homeUrl = homePage } - if prefs.HomeDashboardID != 0 { - slugQuery := dashboards.GetDashboardRefByIDQuery{ID: prefs.HomeDashboardID} - err := s.dashboardService.GetDashboardUIDByID(c.Req.Context(), &slugQuery) - if err == nil { - homeUrl = dashboards.GetDashboardURL(slugQuery.Result.UID, slugQuery.Result.Slug) - } - } - homeNode := &navtree.NavLink{ Text: "Home", Id: "home", @@ -279,7 +271,7 @@ func (s *ServiceImpl) getProfileNode(c *models.ReqContext) *navtree.NavLink { children := []*navtree.NavLink{ { - Text: "Preferences", Id: "profile/settings", Url: s.cfg.AppSubURL + "/profile", Icon: "sliders-v-alt", + Text: "Profile", Id: "profile/settings", Url: s.cfg.AppSubURL + "/profile", Icon: "sliders-v-alt", }, } diff --git a/pkg/services/ngalert/api/api_ruler_validation_test.go b/pkg/services/ngalert/api/api_ruler_validation_test.go index b5098d167f4..00d76433882 100644 --- a/pkg/services/ngalert/api/api_ruler_validation_test.go +++ b/pkg/services/ngalert/api/api_ruler_validation_test.go @@ -69,8 +69,8 @@ func validRule() apimodels.PostableExtendedRuleNode { }, }, UID: util.GenerateShortUID(), - NoDataState: allNoData[rand.Intn(len(allNoData)-1)], - ExecErrState: allExecError[rand.Intn(len(allExecError)-1)], + NoDataState: allNoData[rand.Intn(len(allNoData))], + ExecErrState: allExecError[rand.Intn(len(allExecError))], }, } } diff --git a/pkg/services/ngalert/api/util_test.go b/pkg/services/ngalert/api/util_test.go index 3ed32d797e6..ce79d0fed9f 100644 --- a/pkg/services/ngalert/api/util_test.go +++ b/pkg/services/ngalert/api/util_test.go @@ -163,14 +163,15 @@ func Test_containsProvisionedAlerts(t *testing.T) { t.Run("should return true if at least one rule is provisioned", func(t *testing.T) { _, rules := models2.GenerateUniqueAlertRules(rand.Intn(4)+2, models2.AlertRuleGen()) provenance := map[string]models2.Provenance{ - rules[rand.Intn(len(rules)-1)].UID: []models2.Provenance{models2.ProvenanceAPI, models2.ProvenanceFile}[rand.Intn(2)], + rules[rand.Intn(len(rules))].UID: []models2.Provenance{models2.ProvenanceAPI, models2.ProvenanceFile}[rand.Intn(2)], } require.Truef(t, containsProvisionedAlerts(provenance, rules), "the group of rules is expected to be considered as provisioned but it isn't. Provenances: %v", provenance) }) t.Run("should return false if map does not contain or has ProvenanceNone", func(t *testing.T) { _, rules := models2.GenerateUniqueAlertRules(rand.Intn(5)+1, models2.AlertRuleGen()) provenance := make(map[string]models2.Provenance) - for i := 0; i < rand.Intn(len(rules)); i++ { + numProvenanceNone := rand.Intn(len(rules)) + for i := 0; i < numProvenanceNone; i++ { provenance[rules[i].UID] = models2.ProvenanceNone } require.Falsef(t, containsProvisionedAlerts(provenance, rules), "the group of rules is not expected to be provisioned but it is. Provenances: %v", provenance) diff --git a/pkg/services/ngalert/models/testing.go b/pkg/services/ngalert/models/testing.go index 82b0ab7d443..6c1ed0c7897 100644 --- a/pkg/services/ngalert/models/testing.go +++ b/pkg/services/ngalert/models/testing.go @@ -25,7 +25,7 @@ func AlertRuleGen(mutators ...AlertRuleMutator) func() *AlertRule { NoData, OK, } - return s[rand.Intn(len(s)-1)] + return s[rand.Intn(len(s))] } randErrState := func() ExecutionErrorState { @@ -34,7 +34,7 @@ func AlertRuleGen(mutators ...AlertRuleMutator) func() *AlertRule { ErrorErrState, OkErrState, } - return s[rand.Intn(len(s)-1)] + return s[rand.Intn(len(s))] } interval := (rand.Int63n(6) + 1) * 10 diff --git a/pkg/services/ngalert/state/historian/annotation.go b/pkg/services/ngalert/state/historian/annotation.go index 544f6a45a17..03f1b73faaa 100644 --- a/pkg/services/ngalert/state/historian/annotation.go +++ b/pkg/services/ngalert/state/historian/annotation.go @@ -40,12 +40,17 @@ func NewAnnotationBackend(annotations annotations.Repository, dashboards dashboa } // RecordStates writes a number of state transitions for a given rule to state history. -func (h *AnnotationBackend) RecordStatesAsync(ctx context.Context, rule *ngmodels.AlertRule, states []state.StateTransition) { +func (h *AnnotationBackend) RecordStatesAsync(ctx context.Context, rule *ngmodels.AlertRule, states []state.StateTransition) <-chan error { logger := h.log.FromContext(ctx) // Build annotations before starting goroutine, to make sure all data is copied and won't mutate underneath us. annotations := h.buildAnnotations(rule, states, logger) panel := parsePanelKey(rule, logger) - go h.recordAnnotationsSync(ctx, panel, annotations, logger) + errCh := make(chan error, 1) + go func() { + defer close(errCh) + errCh <- h.recordAnnotationsSync(ctx, panel, annotations, logger) + }() + return errCh } func (h *AnnotationBackend) QueryStates(ctx context.Context, query ngmodels.HistoryQuery) (*data.Frame, error) { @@ -156,12 +161,12 @@ func (h *AnnotationBackend) buildAnnotations(rule *ngmodels.AlertRule, states [] return items } -func (h *AnnotationBackend) recordAnnotationsSync(ctx context.Context, panel *panelKey, annotations []annotations.Item, logger log.Logger) { +func (h *AnnotationBackend) recordAnnotationsSync(ctx context.Context, panel *panelKey, annotations []annotations.Item, logger log.Logger) error { if panel != nil { dashID, err := h.dashboards.getID(ctx, panel.orgID, panel.dashUID) if err != nil { logger.Error("Error getting dashboard for alert annotation", "dashboardUID", panel.dashUID, "error", err) - return + return fmt.Errorf("error getting dashboard for alert annotation: %w", err) } for i := range annotations { @@ -172,9 +177,11 @@ func (h *AnnotationBackend) recordAnnotationsSync(ctx context.Context, panel *pa if err := h.annotations.SaveMany(ctx, annotations); err != nil { logger.Error("Error saving alert annotation batch", "error", err) + return fmt.Errorf("error saving alert annotation batch: %w", err) } logger.Debug("Done saving alert annotation batch") + return nil } func buildAnnotationTextAndData(rule *ngmodels.AlertRule, currentState *state.State) (string, *simplejson.Json) { diff --git a/pkg/services/ngalert/state/historian/annotation_test.go b/pkg/services/ngalert/state/historian/annotation_test.go index bee69be9d56..25497a6d6b6 100644 --- a/pkg/services/ngalert/state/historian/annotation_test.go +++ b/pkg/services/ngalert/state/historian/annotation_test.go @@ -19,7 +19,7 @@ func TestAnnotationHistorian_Integration(t *testing.T) { t.Run("alert annotations are queryable", func(t *testing.T) { anns := createTestAnnotationBackendSut(t) items := []annotations.Item{createAnnotation()} - anns.recordAnnotationsSync(context.Background(), nil, items, log.NewNopLogger()) + require.NoError(t, anns.recordAnnotationsSync(context.Background(), nil, items, log.NewNopLogger())) q := models.HistoryQuery{ RuleUID: "my-rule", diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index e18b58d3305..dcb6dd2da67 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -2,15 +2,28 @@ package historian import ( "context" + "encoding/json" + "fmt" + "sort" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/state" ) +const ( + OrgIDLabel = "orgID" + RuleUIDLabel = "ruleUID" + GroupLabel = "group" + FolderUIDLabel = "folderUID" +) + type remoteLokiClient interface { ping() error + push([]stream) error } type RemoteLokiBackend struct { @@ -30,11 +43,111 @@ func (h *RemoteLokiBackend) TestConnection() error { return h.client.ping() } -func (h *RemoteLokiBackend) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) { +func (h *RemoteLokiBackend) RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []state.StateTransition) <-chan error { logger := h.log.FromContext(ctx) - logger.Debug("Remote Loki state history backend was called with states") + streams := h.statesToStreams(rule, states, logger) + return h.recordStreamsAsync(ctx, streams, logger) } func (h *RemoteLokiBackend) QueryStates(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { return data.NewFrame("states"), nil } + +func (h *RemoteLokiBackend) statesToStreams(rule *models.AlertRule, states []state.StateTransition, logger log.Logger) []stream { + buckets := make(map[string][]row) // label repr -> entries + for _, state := range states { + if !shouldRecord(state) { + continue + } + + labels := removePrivateLabels(state.State.Labels) + labels[OrgIDLabel] = fmt.Sprint(rule.OrgID) + labels[RuleUIDLabel] = fmt.Sprint(rule.UID) + labels[GroupLabel] = fmt.Sprint(rule.RuleGroup) + labels[FolderUIDLabel] = fmt.Sprint(rule.NamespaceUID) + repr := labels.String() + + entry := lokiEntry{ + SchemaVersion: 1, + Previous: state.PreviousFormatted(), + Current: state.Formatted(), + Values: valuesAsDataBlob(state.State), + } + jsn, err := json.Marshal(entry) + if err != nil { + logger.Error("Failed to construct history record for state, skipping", "error", err) + continue + } + line := string(jsn) + + buckets[repr] = append(buckets[repr], row{ + At: state.State.LastEvaluationTime, + Val: line, + }) + } + + result := make([]stream, 0, len(buckets)) + for repr, rows := range buckets { + labels, err := data.LabelsFromString(repr) + if err != nil { + logger.Error("Failed to parse frame labels, skipping state history batch: %w", err) + continue + } + result = append(result, stream{ + Stream: labels, + Values: rows, + }) + } + + return result +} + +func (h *RemoteLokiBackend) recordStreamsAsync(ctx context.Context, streams []stream, logger log.Logger) <-chan error { + errCh := make(chan error, 1) + go func() { + defer close(errCh) + if err := h.recordStreams(ctx, streams, logger); err != nil { + logger.Error("Failed to save alert state history batch", "error", err) + errCh <- fmt.Errorf("failed to save alert state history batch: %w", err) + } + }() + return errCh +} + +func (h *RemoteLokiBackend) recordStreams(ctx context.Context, streams []stream, logger log.Logger) error { + if err := h.client.push(streams); err != nil { + return err + } + logger.Debug("Done saving alert state history batch") + return nil +} + +type lokiEntry struct { + SchemaVersion int `json:"schemaVersion"` + Previous string `json:"previous"` + Current string `json:"current"` + Values *simplejson.Json `json:"values"` +} + +func valuesAsDataBlob(state *state.State) *simplejson.Json { + jsonData := simplejson.New() + + switch state.State { + case eval.Error: + if state.Error == nil { + jsonData.Set("error", nil) + } else { + jsonData.Set("error", state.Error.Error()) + } + case eval.NoData: + jsonData.Set("noData", true) + default: + keys := make([]string, 0, len(state.Values)) + for k := range state.Values { + keys = append(keys, k) + } + sort.Strings(keys) + jsonData.Set("values", simplejson.NewFromAny(state.Values)) + } + return jsonData +} diff --git a/pkg/services/ngalert/state/historian/loki_http.go b/pkg/services/ngalert/state/historian/loki_http.go index bea1693b907..457516b0b4f 100644 --- a/pkg/services/ngalert/state/historian/loki_http.go +++ b/pkg/services/ngalert/state/historian/loki_http.go @@ -1,7 +1,10 @@ package historian import ( + "bytes" + "encoding/json" "fmt" + "io" "net/http" "net/url" "time" @@ -37,18 +40,10 @@ func newLokiClient(cfg LokiConfig, logger log.Logger) *httpLokiClient { func (c *httpLokiClient) ping() error { uri := c.cfg.Url.JoinPath("/loki/api/v1/labels") req, err := http.NewRequest(http.MethodGet, uri.String(), nil) - - if c.cfg.BasicAuthUser != "" || c.cfg.BasicAuthPassword != "" { - req.SetBasicAuth(c.cfg.BasicAuthUser, c.cfg.BasicAuthPassword) - } - - if c.cfg.TenantID != "" { - req.Header.Add("X-Scope-OrgID", c.cfg.TenantID) - } - if err != nil { return fmt.Errorf("error creating request: %w", err) } + c.setAuthAndTenantHeaders(req) res, err := c.client.Do(req) if res != nil { @@ -68,3 +63,71 @@ func (c *httpLokiClient) ping() error { c.log.Debug("Ping request to Loki endpoint succeeded", "status", res.StatusCode) return nil } + +type stream struct { + Stream map[string]string `json:"stream"` + Values []row `json:"values"` +} + +type row struct { + At time.Time + Val string +} + +func (r *row) MarshalJSON() ([]byte, error) { + return json.Marshal([2]string{ + fmt.Sprintf("%d", r.At.UnixNano()), r.Val, + }) +} + +func (c *httpLokiClient) push(s []stream) error { + body := struct { + Streams []stream `json:"streams"` + }{Streams: s} + enc, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("failed to serialize Loki payload: %w", err) + } + + uri := c.cfg.Url.JoinPath("/loki/api/v1/push") + req, err := http.NewRequest(http.MethodPost, uri.String(), bytes.NewBuffer(enc)) + if err != nil { + return fmt.Errorf("failed to create Loki request: %w", err) + } + + c.setAuthAndTenantHeaders(req) + req.Header.Add("content-type", "application/json") + + resp, err := c.client.Do(req) + if resp != nil { + defer func() { + if err := resp.Body.Close(); err != nil { + c.log.Warn("Failed to close response body", "err", err) + } + }() + } + if err != nil { + return fmt.Errorf("failed to send request: %w", err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + byt, _ := io.ReadAll(resp.Body) + if len(byt) > 0 { + c.log.Error("Error response from Loki", "response", string(byt), "status", resp.StatusCode) + } else { + c.log.Error("Error response from Loki with an empty body", "status", resp.StatusCode) + } + return fmt.Errorf("received a non-200 response from loki, status: %d", resp.StatusCode) + } + return nil +} + +func (c *httpLokiClient) setAuthAndTenantHeaders(req *http.Request) { + if c.cfg.BasicAuthUser != "" || c.cfg.BasicAuthPassword != "" { + req.SetBasicAuth(c.cfg.BasicAuthUser, c.cfg.BasicAuthPassword) + } + + if c.cfg.TenantID != "" { + req.Header.Add("X-Scope-OrgID", c.cfg.TenantID) + } +} diff --git a/pkg/services/ngalert/state/historian/loki_http_test.go b/pkg/services/ngalert/state/historian/loki_http_test.go index 4a76b2e65f4..89947ccd8fc 100644 --- a/pkg/services/ngalert/state/historian/loki_http_test.go +++ b/pkg/services/ngalert/state/historian/loki_http_test.go @@ -12,25 +12,27 @@ import ( func TestLokiHTTPClient(t *testing.T) { t.Skip() - url, err := url.Parse("https://logs-prod-eu-west-0.grafana.net") - require.NoError(t, err) + t.Run("smoke test pinging Loki", func(t *testing.T) { + url, err := url.Parse("https://logs-prod-eu-west-0.grafana.net") + require.NoError(t, err) - client := newLokiClient(LokiConfig{ - Url: url, - }, log.NewNopLogger()) + client := newLokiClient(LokiConfig{ + Url: url, + }, log.NewNopLogger()) - // Unauthorized request should fail against Grafana Cloud. - err = client.ping() - require.Error(t, err) + // Unauthorized request should fail against Grafana Cloud. + err = client.ping() + require.Error(t, err) - client.cfg.BasicAuthUser = "" - client.cfg.BasicAuthPassword = "" + client.cfg.BasicAuthUser = "" + client.cfg.BasicAuthPassword = "" - // When running on prem, you might need to set the tenant id, - // so the x-scope-orgid header is set. - // client.cfg.TenantID = "" + // When running on prem, you might need to set the tenant id, + // so the x-scope-orgid header is set. + // client.cfg.TenantID = "" - // Authorized request should fail against Grafana Cloud. - err = client.ping() - require.NoError(t, err) + // Authorized request should fail against Grafana Cloud. + err = client.ping() + require.NoError(t, err) + }) } diff --git a/pkg/services/ngalert/state/historian/noop.go b/pkg/services/ngalert/state/historian/noop.go index 76c40643772..a4b7ce1126b 100644 --- a/pkg/services/ngalert/state/historian/noop.go +++ b/pkg/services/ngalert/state/historian/noop.go @@ -14,5 +14,8 @@ func NewNopHistorian() *NoOpHistorian { return &NoOpHistorian{} } -func (f *NoOpHistorian) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) { +func (f *NoOpHistorian) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) <-chan error { + errCh := make(chan error) + close(errCh) + return errCh } diff --git a/pkg/services/ngalert/state/historian/sql.go b/pkg/services/ngalert/state/historian/sql.go index 09cfe436ac7..8e5d0cf2294 100644 --- a/pkg/services/ngalert/state/historian/sql.go +++ b/pkg/services/ngalert/state/historian/sql.go @@ -19,7 +19,10 @@ func NewSqlBackend() *SqlBackend { } } -func (h *SqlBackend) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) { +func (h *SqlBackend) RecordStatesAsync(ctx context.Context, _ *models.AlertRule, _ []state.StateTransition) <-chan error { + errCh := make(chan error) + close(errCh) + return errCh } func (h *SqlBackend) QueryStates(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { diff --git a/pkg/services/ngalert/state/persist.go b/pkg/services/ngalert/state/persist.go index 1dd8a0366ab..1665444d248 100644 --- a/pkg/services/ngalert/state/persist.go +++ b/pkg/services/ngalert/state/persist.go @@ -22,8 +22,10 @@ type RuleReader interface { // Historian maintains an audit log of alert state history. type Historian interface { - // RecordStates writes a number of state transitions for a given rule to state history. - RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) + // RecordStates writes a number of state transitions for a given rule to state history. It returns a channel that + // is closed when writing the state transitions has completed. If an error has occurred, the channel will contain a + // non-nil error. + RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) <-chan error } // ImageCapturer captures images. diff --git a/pkg/services/ngalert/state/testing.go b/pkg/services/ngalert/state/testing.go index 917c785eed4..199d07d7060 100644 --- a/pkg/services/ngalert/state/testing.go +++ b/pkg/services/ngalert/state/testing.go @@ -62,7 +62,10 @@ func (f *FakeRuleReader) ListAlertRules(_ context.Context, q *models.ListAlertRu type FakeHistorian struct{} -func (f *FakeHistorian) RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) { +func (f *FakeHistorian) RecordStatesAsync(ctx context.Context, rule *models.AlertRule, states []StateTransition) <-chan error { + errCh := make(chan error) + close(errCh) + return errCh } // NotAvailableImageService is a service that returns ErrScreenshotsUnavailable. diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index 6f066d9907a..d3e63758e88 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -328,7 +328,7 @@ func TestCalculateAutomaticChanges(t *testing.T) { group2 := models.GenerateGroupKey(orgID) rules2 := models.GenerateAlertRules(4, models.AlertRuleGen(withGroupKey(group2), models.WithSequentialGroupIndex())) - movedIndex := rand.Intn(len(rules2) - 1) + movedIndex := rand.Intn(len(rules2)) movedRule := rules2[movedIndex] copyRule := models.CopyRule(movedRule) copyRule.RuleGroup = group.RuleGroup diff --git a/pkg/services/plugindashboards/service/dashboard_updater.go b/pkg/services/plugindashboards/service/dashboard_updater.go index 022e839879e..11a76ee07fb 100644 --- a/pkg/services/plugindashboards/service/dashboard_updater.go +++ b/pkg/services/plugindashboards/service/dashboard_updater.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboardimport" @@ -132,7 +131,7 @@ func (du *DashboardUpdater) syncPluginDashboards(ctx context.Context, plugin plu } } -func (du *DashboardUpdater) handlePluginStateChanged(ctx context.Context, event *models.PluginStateChangedEvent) error { +func (du *DashboardUpdater) handlePluginStateChanged(ctx context.Context, event *pluginsettings.PluginStateChangedEvent) error { du.logger.Info("Plugin state changed", "pluginId", event.PluginId, "enabled", event.Enabled) if event.Enabled { diff --git a/pkg/services/plugindashboards/service/dashboard_updater_test.go b/pkg/services/plugindashboards/service/dashboard_updater_test.go index 80af3070b14..5b9e160e9f6 100644 --- a/pkg/services/plugindashboards/service/dashboard_updater_test.go +++ b/pkg/services/plugindashboards/service/dashboard_updater_test.go @@ -5,9 +5,10 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/tracing" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/dashboardimport" "github.com/grafana/grafana/pkg/services/dashboards" @@ -15,7 +16,6 @@ import ( "github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/pluginsettings/service" - "github.com/stretchr/testify/require" ) func TestDashboardUpdater(t *testing.T) { @@ -202,7 +202,7 @@ func TestDashboardUpdater(t *testing.T) { t.Run("handlePluginStateChanged", func(t *testing.T) { scenario(t, "When app plugin is disabled that doesn't have any imported dashboards shouldn't delete any", scenarioInput{}, func(ctx *scenarioContext) { - err := ctx.bus.Publish(context.Background(), &models.PluginStateChangedEvent{ + err := ctx.bus.Publish(context.Background(), &pluginsettings.PluginStateChangedEvent{ PluginId: "test", OrgId: 2, Enabled: false, @@ -250,7 +250,7 @@ func TestDashboardUpdater(t *testing.T) { }, }, }, func(ctx *scenarioContext) { - err := ctx.bus.Publish(context.Background(), &models.PluginStateChangedEvent{ + err := ctx.bus.Publish(context.Background(), &pluginsettings.PluginStateChangedEvent{ PluginId: "test", OrgId: 2, Enabled: false, @@ -307,7 +307,7 @@ func TestDashboardUpdater(t *testing.T) { }, }, }, func(ctx *scenarioContext) { - err := ctx.bus.Publish(context.Background(), &models.PluginStateChangedEvent{ + err := ctx.bus.Publish(context.Background(), &pluginsettings.PluginStateChangedEvent{ PluginId: "test", OrgId: 2, Enabled: true, @@ -478,8 +478,8 @@ type scenarioContext struct { dashboardPluginService *dashboardPluginServiceMock dashboardService *dashboardServiceMock importDashboardArgs []*dashboardimport.ImportDashboardRequest - getPluginSettingsByIdArgs []*models.GetPluginSettingByIdQuery - updatePluginSettingVersionArgs []*models.UpdatePluginSettingVersionCmd + getPluginSettingsByIdArgs []*pluginsettings.GetPluginSettingByIdQuery + updatePluginSettingVersionArgs []*pluginsettings.UpdatePluginSettingVersionCmd dashboardUpdater *DashboardUpdater } @@ -492,8 +492,8 @@ func scenario(t *testing.T, desc string, input scenarioInput, f func(ctx *scenar t: t, bus: bus.ProvideBus(tracer), importDashboardArgs: []*dashboardimport.ImportDashboardRequest{}, - getPluginSettingsByIdArgs: []*models.GetPluginSettingByIdQuery{}, - updatePluginSettingVersionArgs: []*models.UpdatePluginSettingVersionCmd{}, + getPluginSettingsByIdArgs: []*pluginsettings.GetPluginSettingByIdQuery{}, + updatePluginSettingVersionArgs: []*pluginsettings.UpdatePluginSettingVersionCmd{}, } getPlugin := func(ctx context.Context, pluginID string) (plugins.PluginDTO, bool) { diff --git a/pkg/services/pluginsettings/fake.go b/pkg/services/pluginsettings/fake.go index 4a8f3cf4122..8504d69c275 100644 --- a/pkg/services/pluginsettings/fake.go +++ b/pkg/services/pluginsettings/fake.go @@ -3,8 +3,6 @@ package pluginsettings import ( "context" "time" - - "github.com/grafana/grafana/pkg/models" ) type FakePluginSettings struct { @@ -33,7 +31,7 @@ func (ps *FakePluginSettings) GetPluginSettingByPluginID(ctx context.Context, ar if res, ok := ps.Plugins[args.PluginID]; ok { return res, nil } - return nil, models.ErrPluginSettingNotFound + return nil, ErrPluginSettingNotFound } // UpdatePluginSetting updates a Plugin Setting @@ -66,7 +64,7 @@ func (ps *FakePluginSettings) UpdatePluginSettingPluginVersion(ctx context.Conte res.PluginVersion = args.PluginVersion return nil } - return models.ErrPluginSettingNotFound + return ErrPluginSettingNotFound } // DecryptedValues decrypts the encrypted secureJSONData of the provided plugin setting and diff --git a/pkg/services/pluginsettings/models.go b/pkg/services/pluginsettings/models.go index 52fec91129a..8b53a83e30d 100644 --- a/pkg/services/pluginsettings/models.go +++ b/pkg/services/pluginsettings/models.go @@ -1,9 +1,14 @@ package pluginsettings import ( + "errors" "time" ) +var ( + ErrPluginSettingNotFound = errors.New("plugin setting not found") +) + type DTO struct { ID int64 OrgID int64 @@ -49,3 +54,63 @@ type GetByPluginIDArgs struct { PluginID string OrgID int64 } + +type PluginSetting struct { + Id int64 + PluginId string + OrgId int64 + Enabled bool + Pinned bool + JsonData map[string]interface{} + SecureJsonData map[string][]byte + PluginVersion string + + Created time.Time + Updated time.Time +} + +type PluginSettingInfo struct { + PluginID string `xorm:"plugin_id"` + OrgID int64 `xorm:"org_id"` + Enabled bool `xorm:"enabled"` + Pinned bool `xorm:"pinned"` + PluginVersion string `xorm:"plugin_id"` +} + +// ---------------------- +// COMMANDS + +// Also acts as api DTO +type UpdatePluginSettingCmd struct { + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + JsonData map[string]interface{} `json:"jsonData"` + SecureJsonData map[string]string `json:"secureJsonData"` + PluginVersion string `json:"version"` + + PluginId string `json:"-"` + OrgId int64 `json:"-"` + EncryptedSecureJsonData map[string][]byte `json:"-"` +} + +// specific command, will only update version +type UpdatePluginSettingVersionCmd struct { + PluginVersion string + PluginId string `json:"-"` + OrgId int64 `json:"-"` +} + +// --------------------- +// QUERIES + +type GetPluginSettingByIdQuery struct { + PluginId string + OrgId int64 + Result *PluginSetting +} + +type PluginStateChangedEvent struct { + PluginId string + OrgId int64 + Enabled bool +} diff --git a/pkg/services/pluginsettings/service/service.go b/pkg/services/pluginsettings/service/service.go index 8ec61036d16..8c898822da8 100644 --- a/pkg/services/pluginsettings/service/service.go +++ b/pkg/services/pluginsettings/service/service.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" ) @@ -64,7 +63,7 @@ func (s *Service) GetPluginSettings(ctx context.Context, args *pluginsettings.Ge } func (s *Service) GetPluginSettingByPluginID(ctx context.Context, args *pluginsettings.GetByPluginIDArgs) (*pluginsettings.DTO, error) { - query := &models.GetPluginSettingByIdQuery{ + query := &pluginsettings.GetPluginSettingByIdQuery{ OrgId: args.OrgID, PluginId: args.PluginID, } @@ -93,7 +92,7 @@ func (s *Service) UpdatePluginSetting(ctx context.Context, args *pluginsettings. return err } - return s.updatePluginSetting(ctx, &models.UpdatePluginSettingCmd{ + return s.updatePluginSetting(ctx, &pluginsettings.UpdatePluginSettingCmd{ Enabled: args.Enabled, Pinned: args.Pinned, JsonData: args.JSONData, @@ -106,7 +105,7 @@ func (s *Service) UpdatePluginSetting(ctx context.Context, args *pluginsettings. } func (s *Service) UpdatePluginSettingPluginVersion(ctx context.Context, args *pluginsettings.UpdatePluginVersionArgs) error { - return s.updatePluginSettingVersion(ctx, &models.UpdatePluginSettingVersionCmd{ + return s.updatePluginSettingVersion(ctx, &pluginsettings.UpdatePluginSettingVersionCmd{ PluginVersion: args.PluginVersion, PluginId: args.PluginID, OrgId: args.OrgID, @@ -135,7 +134,7 @@ func (s *Service) DecryptedValues(ps *pluginsettings.DTO) map[string]string { return json } -func (s *Service) getPluginSettingsInfo(ctx context.Context, orgID int64) ([]*models.PluginSettingInfo, error) { +func (s *Service) getPluginSettingsInfo(ctx context.Context, orgID int64) ([]*pluginsettings.PluginSettingInfo, error) { sql := `SELECT org_id, plugin_id, enabled, pinned, plugin_version FROM plugin_setting ` params := make([]interface{}, 0) @@ -144,7 +143,7 @@ func (s *Service) getPluginSettingsInfo(ctx context.Context, orgID int64) ([]*mo params = append(params, orgID) } - var rslt []*models.PluginSettingInfo + var rslt []*pluginsettings.PluginSettingInfo err := s.db.WithDbSession(ctx, func(sess *db.Session) error { return sess.SQL(sql, params...).Find(&rslt) }) @@ -155,23 +154,23 @@ func (s *Service) getPluginSettingsInfo(ctx context.Context, orgID int64) ([]*mo return rslt, nil } -func (s *Service) getPluginSettingById(ctx context.Context, query *models.GetPluginSettingByIdQuery) error { +func (s *Service) getPluginSettingById(ctx context.Context, query *pluginsettings.GetPluginSettingByIdQuery) error { return s.db.WithDbSession(ctx, func(sess *db.Session) error { - pluginSetting := models.PluginSetting{OrgId: query.OrgId, PluginId: query.PluginId} + pluginSetting := pluginsettings.PluginSetting{OrgId: query.OrgId, PluginId: query.PluginId} has, err := sess.Get(&pluginSetting) if err != nil { return err } else if !has { - return models.ErrPluginSettingNotFound + return pluginsettings.ErrPluginSettingNotFound } query.Result = &pluginSetting return nil }) } -func (s *Service) updatePluginSetting(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error { +func (s *Service) updatePluginSetting(ctx context.Context, cmd *pluginsettings.UpdatePluginSettingCmd) error { return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { - var pluginSetting models.PluginSetting + var pluginSetting pluginsettings.PluginSetting exists, err := sess.Where("org_id=? and plugin_id=?", cmd.OrgId, cmd.PluginId).Get(&pluginSetting) if err != nil { @@ -180,7 +179,7 @@ func (s *Service) updatePluginSetting(ctx context.Context, cmd *models.UpdatePlu sess.UseBool("enabled") sess.UseBool("pinned") if !exists { - pluginSetting = models.PluginSetting{ + pluginSetting = pluginsettings.PluginSetting{ PluginId: cmd.PluginId, OrgId: cmd.OrgId, Enabled: cmd.Enabled, @@ -193,7 +192,7 @@ func (s *Service) updatePluginSetting(ctx context.Context, cmd *models.UpdatePlu } // add state change event on commit success - sess.PublishAfterCommit(&models.PluginStateChangedEvent{ + sess.PublishAfterCommit(&pluginsettings.PluginStateChangedEvent{ PluginId: cmd.PluginId, OrgId: cmd.OrgId, Enabled: cmd.Enabled, @@ -209,7 +208,7 @@ func (s *Service) updatePluginSetting(ctx context.Context, cmd *models.UpdatePlu // add state change event on commit success if pluginSetting.Enabled != cmd.Enabled { - sess.PublishAfterCommit(&models.PluginStateChangedEvent{ + sess.PublishAfterCommit(&pluginsettings.PluginStateChangedEvent{ PluginId: cmd.PluginId, OrgId: cmd.OrgId, Enabled: cmd.Enabled, @@ -227,7 +226,7 @@ func (s *Service) updatePluginSetting(ctx context.Context, cmd *models.UpdatePlu }) } -func (s *Service) updatePluginSettingVersion(ctx context.Context, cmd *models.UpdatePluginSettingVersionCmd) error { +func (s *Service) updatePluginSettingVersion(ctx context.Context, cmd *pluginsettings.UpdatePluginSettingVersionCmd) error { return s.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error { _, err := sess.Exec("UPDATE plugin_setting SET plugin_version=? WHERE org_id=? AND plugin_id=?", cmd.PluginVersion, cmd.OrgId, cmd.PluginId) return err diff --git a/pkg/services/pluginsettings/service/service_test.go b/pkg/services/pluginsettings/service/service_test.go index 99fb21f5266..35b6cd1fe75 100644 --- a/pkg/services/pluginsettings/service/service_test.go +++ b/pkg/services/pluginsettings/service/service_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" @@ -106,7 +105,7 @@ func TestIntegrationPluginSettings(t *testing.T) { secureJsonData, err := secretsService.EncryptJsonData(context.Background(), map[string]string{"secureKey": "secureValue"}, secrets.WithoutScope()) require.NoError(t, err) - existing := models.PluginSetting{ + existing := pluginsettings.PluginSetting{ OrgId: 1, PluginId: "existing", Enabled: false, @@ -167,8 +166,8 @@ func TestIntegrationPluginSettings(t *testing.T) { }) t.Run("UpdatePluginSetting should update existing plugin settings and publish PluginStateChangedEvent", func(t *testing.T) { - var pluginStateChangedEvent *models.PluginStateChangedEvent - store.Bus().AddEventListener(func(_ context.Context, evt *models.PluginStateChangedEvent) error { + var pluginStateChangedEvent *pluginsettings.PluginStateChangedEvent + store.Bus().AddEventListener(func(_ context.Context, evt *pluginsettings.PluginStateChangedEvent) error { pluginStateChangedEvent = evt return nil }) @@ -225,8 +224,8 @@ func TestIntegrationPluginSettings(t *testing.T) { t.Run("Non-existing plugin settings", func(t *testing.T) { t.Run("UpdatePluginSetting should insert plugin settings and publish PluginStateChangedEvent", func(t *testing.T) { - var pluginStateChangedEvent *models.PluginStateChangedEvent - store.Bus().AddEventListener(func(_ context.Context, evt *models.PluginStateChangedEvent) error { + var pluginStateChangedEvent *pluginsettings.PluginStateChangedEvent + store.Bus().AddEventListener(func(_ context.Context, evt *pluginsettings.PluginStateChangedEvent) error { pluginStateChangedEvent = evt return nil }) diff --git a/pkg/services/provisioning/plugins/config_reader.go b/pkg/services/provisioning/plugins/config_reader.go index 13ea247da78..bb066cb2e0c 100644 --- a/pkg/services/provisioning/plugins/config_reader.go +++ b/pkg/services/provisioning/plugins/config_reader.go @@ -8,9 +8,10 @@ import ( "path/filepath" "strings" + "gopkg.in/yaml.v3" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" - "gopkg.in/yaml.v3" ) type configReader interface { diff --git a/pkg/services/provisioning/plugins/mocks/Store.go b/pkg/services/provisioning/plugins/mocks/Store.go deleted file mode 100644 index 698da31a9b3..00000000000 --- a/pkg/services/provisioning/plugins/mocks/Store.go +++ /dev/null @@ -1,58 +0,0 @@ -// Code generated by mockery v2.10.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - models "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/org" - mock "github.com/stretchr/testify/mock" -) - -// Store is an autogenerated mock type for the Store type -type Store struct { - mock.Mock -} - -// GetOrgByNameHandler provides a mock function with given fields: ctx, query -func (_m *Store) GetOrgByNameHandler(ctx context.Context, query *org.GetOrgByNameQuery) error { - ret := _m.Called(ctx, query) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *org.GetOrgByNameQuery) error); ok { - r0 = rf(ctx, query) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// GetPluginSettingById provides a mock function with given fields: ctx, query -func (_m *Store) GetPluginSettingById(ctx context.Context, query *models.GetPluginSettingByIdQuery) error { - ret := _m.Called(ctx, query) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.GetPluginSettingByIdQuery) error); ok { - r0 = rf(ctx, query) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// UpdatePluginSetting provides a mock function with given fields: ctx, cmd -func (_m *Store) UpdatePluginSetting(ctx context.Context, cmd *models.UpdatePluginSettingCmd) error { - ret := _m.Called(ctx, cmd) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.UpdatePluginSettingCmd) error); ok { - r0 = rf(ctx, cmd) - } else { - r0 = ret.Error(0) - } - - return r0 -} diff --git a/pkg/services/provisioning/plugins/plugin_provisioner.go b/pkg/services/provisioning/plugins/plugin_provisioner.go index 26d81c2f745..c154603eb0c 100644 --- a/pkg/services/provisioning/plugins/plugin_provisioner.go +++ b/pkg/services/provisioning/plugins/plugin_provisioner.go @@ -5,7 +5,6 @@ import ( "errors" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/pluginsettings" @@ -51,7 +50,7 @@ func (ap *PluginProvisioner) apply(ctx context.Context, cfg *pluginsAsConfig) er PluginID: app.PluginID, }) if err != nil { - if !errors.Is(err, models.ErrPluginSettingNotFound) { + if !errors.Is(err, pluginsettings.ErrPluginSettingNotFound) { return err } } else { diff --git a/pkg/services/provisioning/plugins/plugin_provisioner_test.go b/pkg/services/provisioning/plugins/plugin_provisioner_test.go index 5d4f8be3745..b805756a048 100644 --- a/pkg/services/provisioning/plugins/plugin_provisioner_test.go +++ b/pkg/services/provisioning/plugins/plugin_provisioner_test.go @@ -5,13 +5,12 @@ import ( "errors" "testing" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/pluginsettings" - - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - "github.com/stretchr/testify/require" ) func TestPluginProvisioner(t *testing.T) { @@ -91,7 +90,7 @@ func (m *mockStore) GetPluginSettingByPluginID(_ context.Context, args *pluginse }, nil } - return nil, models.ErrPluginSettingNotFound + return nil, pluginsettings.ErrPluginSettingNotFound } func (m *mockStore) UpdatePluginSetting(_ context.Context, args *pluginsettings.UpdateArgs) error { diff --git a/pkg/services/query/query.go b/pkg/services/query/query.go index 2c0f730c0a9..9befc139afb 100644 --- a/pkg/services/query/query.go +++ b/pkg/services/query/query.go @@ -6,20 +6,21 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "golang.org/x/sync/errgroup" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins/adapters" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/grafanads" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util/errutil" - "golang.org/x/sync/errgroup" ) const ( @@ -33,7 +34,7 @@ func ProvideService( cfg *setting.Cfg, dataSourceCache datasources.CacheService, expressionService *expr.Service, - pluginRequestValidator models.PluginRequestValidator, + pluginRequestValidator validations.PluginRequestValidator, dataSourceService datasources.DataSourceService, pluginClient plugins.Client, ) *Service { @@ -54,7 +55,7 @@ type Service struct { cfg *setting.Cfg dataSourceCache datasources.CacheService expressionService *expr.Service - pluginRequestValidator models.PluginRequestValidator + pluginRequestValidator validations.PluginRequestValidator dataSourceService datasources.DataSourceService pluginClient plugins.Client log log.Logger diff --git a/pkg/services/thumbs/service.go b/pkg/services/thumbs/service.go index a8ef7037cb6..da15dc8a1d2 100644 --- a/pkg/services/thumbs/service.go +++ b/pkg/services/thumbs/service.go @@ -10,9 +10,6 @@ import ( "github.com/segmentio/encoding/json" - "github.com/grafana/grafana/pkg/services/datasources/permissions" - "github.com/grafana/grafana/pkg/services/searchV2" - "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -20,10 +17,13 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/datasources/permissions" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/live" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" ) @@ -60,7 +60,7 @@ type thumbService struct { dashboardService dashboards.DashboardService dsUidsLookup getDatasourceUidsForDashboard dsPermissionsService permissions.DatasourcePermissionsService - licensing models.Licensing + licensing licensing.Licensing searchService searchV2.SearchService } @@ -78,7 +78,7 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, lockService *serverlock.ServerLockService, renderService rendering.Service, gl *live.GrafanaLive, store db.DB, authSetupService CrawlerAuthSetupService, dashboardService dashboards.DashboardService, dashboardThumbsService DashboardThumbService, searchService searchV2.SearchService, - dsPermissionsService permissions.DatasourcePermissionsService, licensing models.Licensing) Service { + dsPermissionsService permissions.DatasourcePermissionsService, licensing licensing.Licensing) Service { if !features.IsEnabled(featuremgmt.FlagDashboardPreviews) { return &dummyService{} } diff --git a/pkg/models/validations.go b/pkg/services/validations/service.go similarity index 92% rename from pkg/models/validations.go rename to pkg/services/validations/service.go index 389e5466f80..9b0d48a8f3d 100644 --- a/pkg/models/validations.go +++ b/pkg/services/validations/service.go @@ -1,4 +1,4 @@ -package models +package validations import ( "net/http" diff --git a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go index d0d97c8cf99..9b6f58b60a4 100644 --- a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go +++ b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go @@ -634,6 +634,10 @@ func (tp *testPlugin) IsDecommissioned() bool { return false } +func (tp *testPlugin) Target() backendplugin.Target { + return backendplugin.TargetNone +} + func (tp *testPlugin) CollectMetrics(_ context.Context, _ *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { return nil, backendplugin.ErrMethodNotImplemented } diff --git a/pkg/util/xorm/dialect_mysql.go b/pkg/util/xorm/dialect_mysql.go index cf1dbb6f214..5f167630d41 100644 --- a/pkg/util/xorm/dialect_mysql.go +++ b/pkg/util/xorm/dialect_mysql.go @@ -5,7 +5,6 @@ package xorm import ( - "crypto/tls" "errors" "fmt" "regexp" @@ -163,16 +162,7 @@ var ( type mysql struct { core.Base - net string - addr string - params map[string]string - loc *time.Location - timeout time.Duration - tls *tls.Config - allowAllFiles bool - allowOldPasswords bool - clientFoundRows bool - rowFormat string + rowFormat string } func (db *mysql) Init(d *core.DB, uri *core.Uri, drivername, dataSourceName string) error { @@ -192,7 +182,6 @@ func (db *mysql) SetParams(params map[string]string) { fallthrough case "COMPRESSED": db.rowFormat = t - break default: break } @@ -339,7 +328,7 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column } col.Name = strings.Trim(columnName, "` ") col.Comment = comment - if "YES" == isNullable { + if isNullable == "YES" { col.Nullable = true } @@ -397,15 +386,12 @@ func (db *mysql) GetColumns(tableName string) ([]string, map[string]*core.Column if _, ok := core.SqlTypes[colType]; ok { col.SQLType = core.SQLType{Name: colType, DefaultLength: len1, DefaultLength2: len2} } else { - return nil, nil, fmt.Errorf("Unknown colType %v", colType) + return nil, nil, fmt.Errorf("unknown colType %v", colType) } if colKey == "PRI" { col.IsPrimaryKey = true } - if colKey == "UNI" { - // col.is - } if extra == "auto_increment" { col.IsAutoIncrement = true @@ -478,7 +464,7 @@ func (db *mysql) GetIndexes(tableName string) (map[string]*core.Index, error) { continue } - if "YES" == nonUnique || nonUnique == "1" { + if nonUnique == "YES" || nonUnique == "1" { indexType = core.IndexType } else { indexType = core.UniqueType @@ -574,7 +560,7 @@ func (p *mymysqlDriver) Parse(driverName, dataSourceName string) (*core.Uri, err // Parse protocol part of URI p := strings.SplitN(pd[0], ":", 2) if len(p) != 2 { - return nil, errors.New("Wrong protocol part of URI") + return nil, errors.New("wrong protocol part of URI") } db.Proto = p[0] options := strings.Split(p[1], ",") @@ -606,7 +592,7 @@ func (p *mymysqlDriver) Parse(driverName, dataSourceName string) (*core.Uri, err // Parse database part of URI dup := strings.SplitN(pd[0], "/", 3) if len(dup) != 3 { - return nil, errors.New("Wrong database part of URI") + return nil, errors.New("wrong database part of URI") } db.DbName = dup[0] db.User = dup[1] @@ -625,7 +611,6 @@ func (p *mysqlDriver) Parse(driverName, dataSourceName string) (*core.Uri, error `\/(?P.*?)` + // /dbname `(?:\?(?P[^\?]*))?$`) // [?param1=value1¶mN=valueN] matches := dsnPattern.FindStringSubmatch(dataSourceName) - // tlsConfigRegister := make(map[string]*tls.Config) names := dsnPattern.SubexpNames() uri := &core.Uri{DbType: core.MYSQL} diff --git a/pkg/util/xorm/engine.go b/pkg/util/xorm/engine.go index dc91346f887..3c1267ae194 100644 --- a/pkg/util/xorm/engine.go +++ b/pkg/util/xorm/engine.go @@ -246,13 +246,6 @@ func (engine *Engine) NoCache() *Session { return session.NoCache() } -// NoCascade If you do not want to auto cascade load object -func (engine *Engine) NoCascade() *Session { - session := engine.NewSession() - session.isAutoClose = true - return session.NoCascade() -} - // NewDB provides an interface to operate database directly func (engine *Engine) NewDB() (*core.DB, error) { return core.OpenDialect(engine.dialect) @@ -375,13 +368,6 @@ func (engine *Engine) DBMetas() ([]*core.Table, error) { return tables, nil } -// Cascade use cascade or not -func (engine *Engine) Cascade(trueOrFalse ...bool) *Session { - session := engine.NewSession() - session.isAutoClose = true - return session.Cascade(trueOrFalse...) -} - // Where method provide a condition query func (engine *Engine) Where(query interface{}, args ...interface{}) *Session { session := engine.NewSession() diff --git a/pkg/util/xorm/session.go b/pkg/util/xorm/session.go index 04c18028c91..60bffa3bf70 100644 --- a/pkg/util/xorm/session.go +++ b/pkg/util/xorm/session.go @@ -7,7 +7,6 @@ package xorm import ( "context" "database/sql" - "errors" "fmt" "hash/crc32" "reflect" @@ -144,12 +143,6 @@ func (session *Session) Alias(alias string) *Session { return session } -// NoCascade indicate that no cascade load child object -func (session *Session) NoCascade() *Session { - session.statement.UseCascade = false - return session -} - // ForUpdate Set Read/Write locking for UPDATE func (session *Session) ForUpdate() *Session { session.statement.IsForUpdate = true @@ -199,14 +192,6 @@ func (session *Session) Charset(charset string) *Session { return session } -// Cascade indicates if loading sub Struct -func (session *Session) Cascade(trueOrFalse ...bool) *Session { - if len(trueOrFalse) >= 1 { - session.statement.UseCascade = trueOrFalse[0] - } - return session -} - // MustLogSQL means record SQL or not and don't follow engine's setting func (session *Session) MustLogSQL(log ...bool) *Session { if len(log) > 0 { @@ -652,37 +637,6 @@ func (session *Session) slice2Bean(scanResults []interface{}, fields []string, b fieldValue.Set(x.Elem()) } } - } else if session.statement.UseCascade { - table, err := session.engine.autoMapType(*fieldValue) - if err != nil { - return nil, err - } - - hasAssigned = true - if len(table.PrimaryKeys) != 1 { - return nil, errors.New("unsupported non or composited primary key cascade") - } - var pk = make(core.PK, len(table.PrimaryKeys)) - pk[0], err = asKind(vv, rawValueType) - if err != nil { - return nil, err - } - - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - structInter := reflect.New(fieldValue.Type()) - has, err := session.ID(pk).NoCascade().get(structInter.Interface()) - if err != nil { - return nil, err - } - if has { - fieldValue.Set(structInter.Elem()) - } else { - return nil, errors.New("cascade obj is not exist") - } - } } case reflect.Ptr: // !nashtsai! TODO merge duplicated codes above diff --git a/pkg/util/xorm/session_convert.go b/pkg/util/xorm/session_convert.go index 2bbc248e99c..27d9a5e08d3 100644 --- a/pkg/util/xorm/session_convert.go +++ b/pkg/util/xorm/session_convert.go @@ -7,7 +7,6 @@ package xorm import ( "database/sql" "database/sql/driver" - "errors" "fmt" "reflect" "strconv" @@ -211,40 +210,6 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, } v = x fieldValue.Set(reflect.ValueOf(v).Convert(fieldType)) - } else if session.statement.UseCascade { - table, err := session.engine.autoMapType(*fieldValue) - if err != nil { - return err - } - - // TODO: current only support 1 primary key - if len(table.PrimaryKeys) > 1 { - return errors.New("unsupported composited primary key cascade") - } - - var pk = make(core.PK, len(table.PrimaryKeys)) - rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) - pk[0], err = str2PK(string(data), rawValueType) - if err != nil { - return err - } - - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - structInter := reflect.New(fieldValue.Type()) - has, err := session.ID(pk).NoCascade().get(structInter.Interface()) - if err != nil { - return err - } - if has { - v = structInter.Elem().Interface() - fieldValue.Set(reflect.ValueOf(v)) - } else { - return errors.New("cascade obj is not exist") - } - } } } case reflect.Ptr: @@ -493,42 +458,7 @@ func (session *Session) bytes2Value(col *core.Column, fieldValue *reflect.Value, v = x fieldValue.Set(reflect.ValueOf(&x)) default: - if session.statement.UseCascade { - structInter := reflect.New(fieldType.Elem()) - table, err := session.engine.autoMapType(structInter.Elem()) - if err != nil { - return err - } - - if len(table.PrimaryKeys) > 1 { - return errors.New("unsupported composited primary key cascade") - } - - var pk = make(core.PK, len(table.PrimaryKeys)) - rawValueType := table.ColumnType(table.PKColumns()[0].FieldName) - pk[0], err = str2PK(string(data), rawValueType) - if err != nil { - return err - } - - if !isPKZero(pk) { - // !nashtsai! TODO for hasOne relationship, it's preferred to use join query for eager fetch - // however, also need to consider adding a 'lazy' attribute to xorm tag which allow hasOne - // property to be fetched lazily - has, err := session.ID(pk).NoCascade().get(structInter.Interface()) - if err != nil { - return err - } - if has { - v = structInter.Interface() - fieldValue.Set(reflect.ValueOf(v)) - } else { - return errors.New("cascade obj is not exist") - } - } - } else { - return fmt.Errorf("unsupported struct type in Scan: %s", fieldValue.Type().String()) - } + return fmt.Errorf("unsupported struct type in Scan: %s", fieldValue.Type().String()) } default: return fmt.Errorf("unsupported type in Scan: %s", fieldValue.Type().String()) diff --git a/pkg/util/xorm/statement.go b/pkg/util/xorm/statement.go index 5c252529124..e952f25ced7 100644 --- a/pkg/util/xorm/statement.go +++ b/pkg/util/xorm/statement.go @@ -35,7 +35,6 @@ type Statement struct { tableName string RawSQL string RawParams []interface{} - UseCascade bool UseAutoJoin bool StoreEngine string Charset string @@ -66,7 +65,6 @@ func (statement *Statement) Init() { statement.Start = 0 statement.LimitN = nil statement.OrderStr = "" - statement.UseCascade = true statement.JoinStr = "" statement.joinArgs = make([]interface{}, 0) statement.GroupByStr = "" diff --git a/public/api-merged.json b/public/api-merged.json index 10c3730b5c2..56fd778555b 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -3058,7 +3058,7 @@ "tags": [ "provisioning" ], - "summary": "Updates an existing template.", + "summary": "Updates an existing notification template.", "operationId": "RoutePutTemplate", "parameters": [ { @@ -5240,7 +5240,7 @@ }, "/folders": { "get": { - "description": "Returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID.", + "description": "Returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID\nand returns the immediate subfolders.", "tags": [ "folders" ], @@ -5266,7 +5266,7 @@ { "type": "string", "description": "The parent folder UID", - "name": "parent_uid", + "name": "parentUid", "in": "query" } ], @@ -12218,9 +12218,6 @@ "hasAcl": { "type": "boolean" }, - "hasPublicDashboard": { - "type": "boolean" - }, "isFolder": { "type": "boolean" }, @@ -13179,7 +13176,7 @@ "type": "integer", "format": "int64" }, - "parent_uid": { + "parentUid": { "type": "string" }, "title": { @@ -14447,34 +14444,6 @@ "$ref": "#/definitions/Matcher" } }, - "NotificationTemplate": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "provenance": { - "$ref": "#/definitions/Provenance" - }, - "template": { - "type": "string" - } - } - }, - "NotificationTemplateContent": { - "type": "object", - "properties": { - "template": { - "type": "string" - } - } - }, - "NotificationTemplates": { - "type": "array", - "items": { - "$ref": "#/definitions/NotificationTemplate" - } - }, "Metadata": { "description": "Metadata contains user accesses for a given resource\nEx: map[string]bool{\"create\":true, \"delete\": true}", "type": "object", @@ -14647,6 +14616,34 @@ "format": "int64", "title": "NoticeSeverity is a type for the Severity property of a Notice." }, + "NotificationTemplate": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, + "template": { + "type": "string" + } + } + }, + "NotificationTemplateContent": { + "type": "object", + "properties": { + "template": { + "type": "string" + } + } + }, + "NotificationTemplates": { + "type": "array", + "items": { + "$ref": "#/definitions/NotificationTemplate" + } + }, "NotificationTestCommand": { "type": "object", "properties": { @@ -18578,7 +18575,6 @@ } }, "alertGroups": { - "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" @@ -18739,14 +18735,12 @@ } }, "gettableAlerts": { - "description": "GettableAlerts gettable alerts", "type": "array", "items": { "$ref": "#/definitions/gettableAlert" } }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -18795,7 +18789,6 @@ } }, "gettableSilences": { - "description": "GettableSilences gettable silences", "type": "array", "items": { "$ref": "#/definitions/gettableSilence" @@ -18982,6 +18975,7 @@ } }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": [ "active", diff --git a/public/api-spec.json b/public/api-spec.json index 14bca1499d2..b342294edea 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -4568,7 +4568,7 @@ }, "/folders": { "get": { - "description": "Returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID.", + "description": "Returns all folders that the authenticated user has permission to view.\nIf nested folders are enabled, it expects an additional query parameter with the parent folder UID\nand returns the immediate subfolders.", "tags": [ "folders" ], @@ -4594,7 +4594,7 @@ { "type": "string", "description": "The parent folder UID", - "name": "parent_uid", + "name": "parentUid", "in": "query" } ], @@ -11159,9 +11159,6 @@ "hasAcl": { "type": "boolean" }, - "hasPublicDashboard": { - "type": "boolean" - }, "isFolder": { "type": "boolean" }, @@ -11919,7 +11916,7 @@ "type": "integer", "format": "int64" }, - "parent_uid": { + "parentUid": { "type": "string" }, "title": { diff --git a/public/app/core/components/AppChrome/NavLandingPage.tsx b/public/app/core/components/AppChrome/NavLandingPage.tsx index 461e16cdd19..0143eebbaf0 100644 --- a/public/app/core/components/AppChrome/NavLandingPage.tsx +++ b/public/app/core/components/AppChrome/NavLandingPage.tsx @@ -51,7 +51,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'grid', gap: theme.spacing(3), gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', - gridAutoRows: '130px', + gridAutoRows: '138px', padding: theme.spacing(2, 0), }), }); diff --git a/public/app/core/components/AppChrome/NavLandingPageCard.tsx b/public/app/core/components/AppChrome/NavLandingPageCard.tsx index 8ed4a426729..23f582bdac3 100644 --- a/public/app/core/components/AppChrome/NavLandingPageCard.tsx +++ b/public/app/core/components/AppChrome/NavLandingPageCard.tsx @@ -15,7 +15,7 @@ export function NavLandingPageCard({ description, text, url }: Props) { return ( {text} - {description} + {description} ); } @@ -25,4 +25,12 @@ const getStyles = (theme: GrafanaTheme2) => ({ marginBottom: 0, gridTemplateRows: '1fr 0 2fr', }), + // Limit descriptions to 3 lines max before ellipsing + // Some plugin descriptions can be very long + description: css({ + WebkitLineClamp: 3, + WebkitBoxOrient: 'vertical', + display: '-webkit-box', + overflow: 'hidden', + }), }); diff --git a/public/app/core/components/AppChrome/TopSearchBar.tsx b/public/app/core/components/AppChrome/TopSearchBar.tsx index b5a54d5b9e1..bde15eea252 100644 --- a/public/app/core/components/AppChrome/TopSearchBar.tsx +++ b/public/app/core/components/AppChrome/TopSearchBar.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Dropdown, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { config } from 'app/core/config'; import { contextSrv } from 'app/core/core'; import { useSelector } from 'app/types'; @@ -14,6 +15,7 @@ import { QuickAdd } from './QuickAdd/QuickAdd'; import { SignInLink } from './TopBar/SignInLink'; import { TopNavBarMenu } from './TopBar/TopNavBarMenu'; import { TopSearchBarSection } from './TopBar/TopSearchBarSection'; +import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger'; import { TopSearchBarInput } from './TopSearchBarInput'; import { TOP_BAR_LEVEL_HEIGHT } from './types'; @@ -24,6 +26,13 @@ export function TopSearchBar() { const helpNode = navIndex['help']; const profileNode = navIndex['profile']; + const search = + config.featureToggles.commandPalette && config.featureToggles.topNavCommandPalette ? ( + + ) : ( + + ); + return (
@@ -32,9 +41,9 @@ export function TopSearchBar() { - - - + + {search} + {helpNode && ( @@ -70,7 +79,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ justifyContent: 'space-between', [theme.breakpoints.up('sm')]: { - gridTemplateColumns: '1fr 1fr 1fr', + gridTemplateColumns: '2fr minmax(200px, 1fr) 2fr', // search should not be smaller than 200px display: 'grid', justifyContent: 'flex-start', diff --git a/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx b/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx new file mode 100644 index 00000000000..db5b77eecff --- /dev/null +++ b/public/app/core/components/AppChrome/TopSearchBarCommandPaletteTrigger.tsx @@ -0,0 +1,109 @@ +import { css } from '@emotion/css'; +import { useKBar, VisualState } from 'kbar'; +import React, { useState } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { getInputStyles, Icon, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui'; +import { focusCss } from '@grafana/ui/src/themes/mixins'; +import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange'; +import { t } from 'app/core/internationalization'; + +export function TopSearchBarCommandPaletteTrigger() { + const theme = useTheme2(); + const { query: kbar } = useKBar((kbarState) => ({ + kbarSearchQuery: kbarState.searchQuery, + kbarIsOpen: kbarState.visualState === VisualState.showing, + })); + + const breakpoint = theme.breakpoints.values.sm; + + const [isSmallScreen, setIsSmallScreen] = useState(window.matchMedia(`(max-width: ${breakpoint}px)`).matches); + + useMediaQueryChange({ + breakpoint, + onChange: (e) => { + setIsSmallScreen(e.matches); + }, + }); + + const onOpenSearch = () => { + kbar.toggle(); + }; + + if (isSmallScreen) { + return ( + + ); + } + + return ; +} + +function PretendTextInput({ onClick }: { onClick: () => void }) { + const styles = useStyles2(getStyles); + + // We want the desktop command palette trigger to look like a search box, + // but it actually behaves like a button - you active it and it performs an + // action. You don't actually type into it. + + return ( +
+
+
+ +
+ + +
+
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => { + const baseStyles = getInputStyles({ theme }); + + return { + wrapper: baseStyles.wrapper, + inputWrapper: baseStyles.inputWrapper, + prefix: baseStyles.prefix, + fakeInput: css([ + baseStyles.input, + { + textAlign: 'left', + paddingLeft: 28, + color: theme.colors.text.disabled, + + // We want the focus styles to appear only when tabbing through, not when clicking the button + // (and when focus is restored after command palette closes) + '&:focus': { + outline: 'unset', + boxShadow: 'unset', + }, + + '&:focus-visible': css` + ${focusCss(theme)} + `, + }, + ]), + + button: css({ + // height: 32, + width: '100%', + textAlign: 'center', + + '> *': { + width: '100%', + textAlign: 'center', + justifyContent: 'center', + gap: '1ch', + }, + }), + }; +}; diff --git a/public/app/core/components/NavBar/navBarItem-translations.ts b/public/app/core/components/NavBar/navBarItem-translations.ts index cf58d991c7c..f47058741eb 100644 --- a/public/app/core/components/NavBar/navBarItem-translations.ts +++ b/public/app/core/components/NavBar/navBarItem-translations.ts @@ -126,7 +126,7 @@ export function getNavTitle(navId: string | undefined) { case 'help': return t('nav.help.title', 'Help'); case 'profile/settings': - return t('nav.profile/settings.title', 'Preferences'); + return t('nav.profile/settings.title', 'Profile'); case 'profile/notifications': return t('nav.profile/notifications.title', 'Notification history'); case 'profile/password': diff --git a/public/app/core/logsModel.ts b/public/app/core/logsModel.ts index 0f27b512b46..c3ea25df244 100644 --- a/public/app/core/logsModel.ts +++ b/public/app/core/logsModel.ts @@ -198,11 +198,13 @@ function isLogsData(series: DataFrame) { * Convert dataFrame into LogsModel which consists of creating separate array of log rows and metrics series. Metrics * series can be either already included in the dataFrame or will be computed from the log rows. * @param dataFrame - * @param intervalMs In case there are no metrics series, we use this for computing it from log rows. + * @param intervalMs Optional. In case there are no metrics series, we use this for computing it from log rows. + * @param absoluteRange Optional. Used to store absolute range of executed queries in logs model. This is used for pagination. + * @param queries Optional. Used to store executed queries in logs model. This is used for pagination. */ export function dataFrameToLogsModel( dataFrame: DataFrame[], - intervalMs: number | undefined, + intervalMs?: number, absoluteRange?: AbsoluteTimeRange, queries?: DataQuery[] ): LogsModel { @@ -748,7 +750,7 @@ export function queryLogsVolume( datasource: DataSourceApi, diff --git a/public/app/core/navigation/GrafanaRoute.tsx b/public/app/core/navigation/GrafanaRoute.tsx index 9cfa4f6e224..1bfe2765013 100644 --- a/public/app/core/navigation/GrafanaRoute.tsx +++ b/public/app/core/navigation/GrafanaRoute.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useEffect } from 'react'; +import React, { Suspense, useEffect, useLayoutEffect } from 'react'; // @ts-ignore import Drop from 'tether-drop'; @@ -18,9 +18,11 @@ export function GrafanaRoute(props: Props) { chrome.setMatchedRoute(props.route); - useEffect(() => { + useLayoutEffect(() => { keybindings.clearAndInitGlobalBindings(); + }, [keybindings]); + useEffect(() => { updateBodyClassNames(props.route); cleanupDOM(); navigationLogger('GrafanaRoute', false, 'Mounted', props.match); diff --git a/public/app/core/utils/sheet.ts b/public/app/core/utils/sheet.ts new file mode 100644 index 00000000000..5771b772283 --- /dev/null +++ b/public/app/core/utils/sheet.ts @@ -0,0 +1,12 @@ +import { read, utils } from 'xlsx'; + +import { ArrayDataFrame, DataFrame } from '@grafana/data'; + +export function readSpreadsheet(file: ArrayBuffer): DataFrame[] { + const wb = read(file, { type: 'buffer' }); + return wb.SheetNames.map((name) => { + const frame = new ArrayDataFrame(utils.sheet_to_json(wb.Sheets[name])); + frame.name = name; + return frame; + }); +} diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx index a8a96dfacc1..60acdd0011a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx @@ -9,7 +9,7 @@ import { QueryOperationRow } from 'app/core/components/QueryOperationRow/QueryOp import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; -import { EmptyQueryWrapper, QueryWrapper } from './QueryWrapper'; +import { AlertQueryOptions, EmptyQueryWrapper, QueryWrapper } from './QueryWrapper'; import { errorFromSeries, getThresholdsForQueries } from './util'; interface Props { @@ -51,6 +51,21 @@ export class QueryRows extends PureComponent { ); }; + onChangeQueryOptions = (options: AlertQueryOptions, index: number) => { + const { queries, onQueriesChange } = this.props; + onQueriesChange( + queries.map((item, itemIndex) => { + if (itemIndex !== index) { + return item; + } + return { + ...item, + model: { ...item.model, maxDataPoints: options.maxDataPoints }, + }; + }) + ); + }; + onChangeDataSource = (settings: DataSourceInstanceSettings, index: number) => { const { queries, onQueriesChange } = this.props; @@ -170,6 +185,7 @@ export class QueryRows extends PureComponent { onChangeDataSource={this.onChangeDataSource} onDuplicateQuery={this.props.onDuplicateQuery} onChangeTimeRange={this.onChangeTimeRange} + onChangeQueryOptions={this.onChangeQueryOptions} thresholds={thresholdByRefId[query.refId]?.config} thresholdsType={thresholdByRefId[query.refId]?.mode} onRunQueries={this.props.onRunQueries} diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx index a373e91ffee..37154731ce3 100644 --- a/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/QueryWrapper.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { cloneDeep } from 'lodash'; -import React, { FC, useState } from 'react'; +import React, { ChangeEvent, FC, useState } from 'react'; import { CoreApp, @@ -14,7 +14,15 @@ import { ThresholdsConfig, } from '@grafana/data'; import { Stack } from '@grafana/experimental'; -import { RelativeTimeRangePicker, useStyles2, Tooltip, Icon, GraphTresholdsStyleMode } from '@grafana/ui'; +import { + GraphTresholdsStyleMode, + Icon, + InlineFormLabel, + Input, + RelativeTimeRangePicker, + Tooltip, + useStyles2, +} from '@grafana/ui'; import { isExpressionQuery } from 'app/features/expressions/guards'; import { QueryEditorRow } from 'app/features/query/components/QueryEditorRow'; import { AlertQuery } from 'app/types/unified-alerting-dto'; @@ -25,6 +33,12 @@ import { AlertConditionIndicator } from '../expressions/AlertConditionIndicator' import { VizWrapper } from './VizWrapper'; +export const DEFAULT_MAX_DATA_POINTS = 43200; + +export interface AlertQueryOptions { + maxDataPoints?: number | undefined; +} + interface Props { data: PanelData; error?: Error; @@ -43,6 +57,7 @@ interface Props { onChangeThreshold?: (thresholds: ThresholdsConfig, index: number) => void; condition: string | null; onSetCondition: (refId: string) => void; + onChangeQueryOptions: (options: AlertQueryOptions, index: number) => void; } export const QueryWrapper: FC = ({ @@ -63,6 +78,7 @@ export const QueryWrapper: FC = ({ onChangeThreshold, condition, onSetCondition, + onChangeQueryOptions, }) => { const styles = useStyles2(getStyles); const isExpression = isExpressionQuery(query.model); @@ -96,11 +112,16 @@ export const QueryWrapper: FC = ({ // TODO add a warning label here too when the data looks like time series data and is used as an alert condition function HeaderExtras({ query, error, index }: { query: AlertQuery; error?: Error; index: number }) { + const queryOptions: AlertQueryOptions = { maxDataPoints: query.model.maxDataPoints }; + const alertQueryOptions: AlertQueryOptions = { + maxDataPoints: queryOptions.maxDataPoints, + }; + if (isExpressionQuery(query.model)) { return null; } else { return ( - + {onChangeTimeRange && ( = ({ onChange={(range) => onChangeTimeRange(range, index)} /> )} +
+ onChangeQueryOptions(options, index)} + /> +
onSetCondition(query.refId)} enabled={condition === query.refId} @@ -159,6 +186,53 @@ export const EmptyQueryWrapper = ({ children }: React.PropsWithChildren<{}>) => return
{children}
; }; +function MaxDataPointsOption({ + options, + onChange, +}: { + options: AlertQueryOptions; + onChange: (options: AlertQueryOptions) => void; +}) { + const value = options.maxDataPoints ?? ''; + + const onMaxDataPointsBlur = (event: ChangeEvent) => { + const maxDataPointsNumber = parseInt(event.target.value, 10); + + const maxDataPoints = isNaN(maxDataPointsNumber) || maxDataPointsNumber === 0 ? undefined : maxDataPointsNumber; + + if (maxDataPoints !== options.maxDataPoints) { + onChange({ + ...options, + maxDataPoints, + }); + } + }; + + return ( + + + The maximum data points per series. Used directly by some data sources and used in calculation of auto + interval. With streaming data this value is used for the rolling buffer. + + } + > + Max data points + + + + ); +} + const getStyles = (theme: GrafanaTheme2) => ({ wrapper: css` label: AlertingQueryWrapper; @@ -166,6 +240,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ border: 1px solid ${theme.colors.border.medium}; border-radius: ${theme.shape.borderRadius(1)}; `, + queryOptions: css` + margin-bottom: -${theme.spacing(2)}; + `, dsTooltip: css` display: flex; align-items: center; diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts index c4bdd8a2e77..fdd6c8396e8 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/reducer.ts @@ -1,6 +1,6 @@ import { createAction, createReducer } from '@reduxjs/toolkit'; -import { DataQuery, RelativeTimeRange, getDefaultRelativeTimeRange } from '@grafana/data'; +import { DataQuery, getDefaultRelativeTimeRange, RelativeTimeRange } from '@grafana/data'; import { getNextRefIdChar } from 'app/core/utils/query'; import { findDataSourceFromExpressionRecursive } from 'app/features/alerting/utils/dataSourceFromExpression'; import { @@ -43,6 +43,7 @@ export const updateExpressionRefId = createAction<{ oldRefId: string; newRefId: export const rewireExpressions = createAction<{ oldRefId: string; newRefId: string }>('rewireExpressions'); export const updateExpressionType = createAction<{ refId: string; type: ExpressionQueryType }>('updateExpressionType'); export const updateExpressionTimeRange = createAction('updateExpressionTimeRange'); +export const updateMaxDataPoints = createAction<{ refId: string; maxDataPoints: number }>('updateMaxDataPoints'); export const queriesAndExpressionsReducer = createReducer(initialState, (builder) => { // data queries actions @@ -70,6 +71,19 @@ export const queriesAndExpressionsReducer = createReducer(initialState, (builder .addCase(setDataQueries, (state, { payload }) => { const expressionQueries = state.queries.filter((query) => isExpressionQuery(query.model)); state.queries = [...payload, ...expressionQueries]; + }) + .addCase(updateMaxDataPoints, (state, action) => { + state.queries = state.queries.map((query) => { + return query.refId === action.payload.refId + ? { + ...query, + model: { + ...query.model, + maxDataPoints: action.payload.maxDataPoints, + }, + } + : query; + }); }); // expressions actions diff --git a/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx index 64442d51147..b8192b4285f 100644 --- a/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx +++ b/public/app/features/connections/tabs/ConnectData/CardGrid/CardGrid.tsx @@ -15,7 +15,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ card: css` height: 90px; padding: 0px 24px; - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.6); + margin-bottom: 0; `, cardContent: css` display: flex; diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes.tsx index 21c178b987c..3d010865c96 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes.tsx @@ -4,7 +4,48 @@ import { UseFormRegister } from 'react-hook-form'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { Checkbox, FieldSet, HorizontalGroup, LinkButton, VerticalGroup } from '@grafana/ui/src'; -import { SharePublicDashboardInputs } from './SharePublicDashboard'; +import { SharePublicDashboardAcknowledgmentInputs, SharePublicDashboardInputs } from './SharePublicDashboard'; + +type Acknowledge = { + type: keyof SharePublicDashboardAcknowledgmentInputs; + description: string; + testId: string; + info: { + href: string; + tooltip: string; + }; +}; +const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; + +const ACKNOWLEDGES: Acknowledge[] = [ + { + type: 'publicAcknowledgment', + description: 'Your entire dashboard will be public', + testId: selectors.WillBePublicCheckbox, + info: { + href: 'https://grafana.com/docs/grafana/latest/dashboards/dashboard-public/', + tooltip: 'Learn more about public dashboards', + }, + }, + { + type: 'dataSourcesAcknowledgment', + description: 'Publishing currently only works with a subset of datasources', + testId: selectors.LimitedDSCheckbox, + info: { + href: 'https://grafana.com/docs/grafana/latest/datasources/', + tooltip: 'Learn more about public datasources', + }, + }, + { + type: 'usageAcknowledgment', + description: 'Making a dashboard public causes queries to run each time it is viewed, which may increase costs', + testId: selectors.CostIncreaseCheckbox, + info: { + href: 'https://grafana.com/docs/grafana/latest/enterprise/query-caching/', + tooltip: 'Learn more about query caching', + }, + }, +]; export const AcknowledgeCheckboxes = ({ disabled, @@ -12,65 +53,30 @@ export const AcknowledgeCheckboxes = ({ }: { disabled: boolean; register: UseFormRegister; -}) => { - const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; - - return ( - <> -

Before you click Save, please acknowledge the following information:

-
- - +}) => ( + <> +

Before you click Save, please acknowledge the following information:

+
+ + {ACKNOWLEDGES.map((acknowledge) => ( + - - - - - - - - - - -
- - ); -}; + ))} +
+
+ +); diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx index 30ce9afe4e7..270671fe812 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx @@ -45,7 +45,7 @@ import { ShareModal } from '../ShareModal'; interface Props extends ShareModalTabProps {} -type SharePublicDashboardAcknowledgmentInputs = { +export type SharePublicDashboardAcknowledgmentInputs = { publicAcknowledgment: boolean; dataSourcesAcknowledgment: boolean; usageAcknowledgment: boolean; diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index 531f779caa0..9c0bc278815 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -97,7 +97,7 @@ const PublicDashboardPage = (props: Props) => { layout={PageLayoutType.Custom} toolbar={} > - {dashboardState.initError && } + {dashboardState.initError && }
@@ -108,6 +108,7 @@ const PublicDashboardPage = (props: Props) => { const getStyles = (theme: GrafanaTheme2) => ({ gridContainer: css({ + flex: 1, padding: theme.spacing(0, 2, 2, 2), overflow: 'auto', }), diff --git a/public/app/features/explore/Explore.test.tsx b/public/app/features/explore/Explore.test.tsx index baf7bcec799..528fbff1d53 100644 --- a/public/app/features/explore/Explore.test.tsx +++ b/public/app/features/explore/Explore.test.tsx @@ -87,6 +87,9 @@ const dummyProps: Props = { isFromCompactUrl: false, eventBus: new EventBusSrv(), showRawPrometheus: false, + showLogsSample: false, + logsSample: { enabled: false }, + setSupplementaryQueryEnabled: jest.fn(), }; jest.mock('@grafana/runtime/src/services/dataSourceSrv', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 7c5df5d230b..82bf6d498a0 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -15,6 +15,7 @@ import { RawTimeRange, EventBus, SplitOpenOptions, + SupplementaryQueryType, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime'; @@ -44,6 +45,7 @@ import { ExploreToolbar } from './ExploreToolbar'; import { FlameGraphExploreContainer } from './FlameGraphExploreContainer'; import { GraphContainer } from './Graph/GraphContainer'; import LogsContainer from './LogsContainer'; +import { LogsSamplePanel } from './LogsSamplePanel'; import { NoData } from './NoData'; import { NoDataSourceCallToAction } from './NoDataSourceCallToAction'; import { NodeGraphContainer } from './NodeGraphContainer'; @@ -56,7 +58,14 @@ import TableContainer from './TableContainer'; import { TraceViewContainer } from './TraceView/TraceViewContainer'; import { changeSize } from './state/explorePane'; import { splitOpen } from './state/main'; -import { addQueryRow, modifyQueries, scanStart, scanStopAction, setQueries } from './state/query'; +import { + addQueryRow, + modifyQueries, + scanStart, + scanStopAction, + setQueries, + setSupplementaryQueryEnabled, +} from './state/query'; import { isSplit } from './state/selectors'; import { makeAbsoluteTime, updateTimeRange } from './state/time'; @@ -357,6 +366,22 @@ export class Explore extends React.PureComponent { ); } + renderLogsSamplePanel() { + const { logsSample, timeZone, setSupplementaryQueryEnabled, exploreId, datasourceInstance } = this.props; + + return ( + + setSupplementaryQueryEnabled(exploreId, enabled, SupplementaryQueryType.LogsSample) + } + /> + ); + } + renderNodeGraphPanel() { const { exploreId, showTrace, queryResponse, datasourceInstance } = this.props; const datasourceType = datasourceInstance ? datasourceInstance?.type : 'unknown'; @@ -416,6 +441,7 @@ export class Explore extends React.PureComponent { showFlameGraph, timeZone, isFromCompactUrl, + showLogsSample, } = this.props; const { openDrawer } = this.state; const styles = getStyles(theme); @@ -486,6 +512,7 @@ export class Explore extends React.PureComponent { {this.renderFlameGraphPanel()} )} {showTrace && {this.renderTraceViewPanel()}} + {showLogsSample && {this.renderLogsSamplePanel()}} {showNoData && {this.renderNoData()}} )} @@ -528,6 +555,7 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { queries, isLive, graphResult, + tableResult, logsResult, showLogs, showMetrics, @@ -540,8 +568,13 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { loading, isFromCompactUrl, showRawPrometheus, + supplementaryQueries, } = item; + const logsSample = supplementaryQueries[SupplementaryQueryType.LogsSample]; + // We want to show logs sample only if there are no log results and if there is already graph or table result + const showLogsSample = !!(logsSample.dataProvider !== undefined && !logsResult && (graphResult || tableResult)); + return { datasourceInstance, datasourceMissing, @@ -564,6 +597,8 @@ function mapStateToProps(state: StoreState, { exploreId }: ExploreProps) { splitted: isSplit(state), loading, isFromCompactUrl: isFromCompactUrl || false, + logsSample, + showLogsSample, }; } @@ -577,6 +612,7 @@ const mapDispatchToProps = { makeAbsoluteTime, addQueryRow, splitOpen, + setSupplementaryQueryEnabled, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/explore/LogsSample.test.tsx b/public/app/features/explore/LogsSample.test.tsx new file mode 100644 index 00000000000..967457fdb86 --- /dev/null +++ b/public/app/features/explore/LogsSample.test.tsx @@ -0,0 +1,103 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React, { ComponentProps } from 'react'; + +import { ArrayVector, FieldType, LoadingState, MutableDataFrame } from '@grafana/data'; + +import { LogsSamplePanel } from './LogsSamplePanel'; + +jest.mock('@grafana/runtime', () => { + return { + ...jest.requireActual('@grafana/runtime'), + reportInteraction: jest.fn(), + }; +}); + +const createProps = (propOverrides?: Partial>) => { + const props = { + queryResponse: undefined, + enabled: true, + timeZone: 'timeZone', + datasourceInstance: undefined, + setLogsSampleEnabled: jest.fn(), + }; + + return { ...props, ...propOverrides }; +}; + +const sampleDataFrame = new MutableDataFrame({ + meta: { + custom: { frameType: 'LabeledTimeValues' }, + }, + fields: [ + { + name: 'labels', + type: FieldType.other, + values: new ArrayVector([ + { place: 'luna', source: 'data' }, + { place: 'luna', source: 'data' }, + ]), + }, + { + name: 'Time', + type: FieldType.time, + values: new ArrayVector(['2022-02-22T09:28:11.352440161Z', '2022-02-22T14:42:50.991981292Z']), + }, + { + name: 'Line', + type: FieldType.string, + values: new ArrayVector(['line1 ', 'line2']), + }, + ], +}); + +describe('LogsSamplePanel', () => { + it('shows empty panel if no data', () => { + render(); + expect(screen.getByText('Logs sample')).toBeInTheDocument(); + }); + + it('shows loading message', () => { + render(); + expect(screen.getByText('Logs sample is loading...')).toBeInTheDocument(); + }); + + it('shows no data message', () => { + render(); + expect(screen.getByText('No logs sample data.')).toBeInTheDocument(); + }); + + it('shows logs sample data', () => { + render( + + ); + expect(screen.getByText('2022-02-22 04:28:11')).toBeInTheDocument(); + expect(screen.getByText('line1')).toBeInTheDocument(); + expect(screen.getByText('2022-02-22 09:42:50')).toBeInTheDocument(); + expect(screen.getByText('line2')).toBeInTheDocument(); + }); + + it('shows log details', async () => { + render( + + ); + const line = screen.getByText('line1'); + expect(screen.queryByText('foo')).not.toBeInTheDocument(); + await userEvent.click(line); + expect(await screen.findByText('Fields')).toBeInTheDocument(); + expect(await screen.findByText('place')).toBeInTheDocument(); + expect(await screen.findByText('luna')).toBeInTheDocument(); + }); + + it('shows warning message', () => { + render( + + ); + expect(screen.getByText('Failed to load logs sample for this query')).toBeInTheDocument(); + expect(screen.getByText('Test error message')).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/explore/LogsSamplePanel.tsx b/public/app/features/explore/LogsSamplePanel.tsx new file mode 100644 index 00000000000..72dbc4d8bc0 --- /dev/null +++ b/public/app/features/explore/LogsSamplePanel.tsx @@ -0,0 +1,67 @@ +import React from 'react'; + +import { DataQueryResponse, DataSourceApi, LoadingState, LogsDedupStrategy } from '@grafana/data'; +import { reportInteraction } from '@grafana/runtime'; +import { TimeZone } from '@grafana/schema'; +import { Collapse } from '@grafana/ui'; +import { dataFrameToLogsModel } from 'app/core/logsModel'; +import store from 'app/core/store'; + +import { LogRows } from '../logs/components/LogRows'; + +import { SupplementaryResultError } from './SupplementaryResultError'; +import { SETTINGS_KEYS } from './utils/logs'; + +type Props = { + queryResponse: DataQueryResponse | undefined; + enabled: boolean; + timeZone: TimeZone; + datasourceInstance: DataSourceApi | null | undefined; + setLogsSampleEnabled: (enabled: boolean) => void; +}; + +export function LogsSamplePanel(props: Props) { + const { queryResponse, timeZone, enabled, setLogsSampleEnabled, datasourceInstance } = props; + + const onToggleLogsSampleCollapse = (isOpen: boolean) => { + setLogsSampleEnabled(isOpen); + reportInteraction('grafana_explore_logs_sample_toggle_clicked', { + datasourceType: datasourceInstance?.type ?? 'unknown', + type: isOpen ? 'open' : 'close', + }); + }; + + let LogsSamplePanelContent: JSX.Element | null; + + if (queryResponse === undefined) { + LogsSamplePanelContent = null; + } else if (queryResponse.error !== undefined) { + LogsSamplePanelContent = ( + + ); + } else if (queryResponse.state === LoadingState.Loading) { + LogsSamplePanelContent = Logs sample is loading...; + } else if (queryResponse.data.length === 0 || queryResponse.data[0].length === 0) { + LogsSamplePanelContent = No logs sample data.; + } else { + const logs = dataFrameToLogsModel(queryResponse.data); + LogsSamplePanelContent = ( + + ); + } + + return ( + + {LogsSamplePanelContent} + + ); +} diff --git a/public/app/features/explore/LogsVolumePanel.tsx b/public/app/features/explore/LogsVolumePanel.tsx index eecda2d8807..d5c3423ea75 100644 --- a/public/app/features/explore/LogsVolumePanel.tsx +++ b/public/app/features/explore/LogsVolumePanel.tsx @@ -1,9 +1,8 @@ import { css } from '@emotion/css'; -import React, { useState } from 'react'; +import React from 'react'; import { AbsoluteTimeRange, - DataQueryError, DataQueryResponse, GrafanaTheme2, LoadingState, @@ -11,9 +10,10 @@ import { TimeZone, EventBus, } from '@grafana/data'; -import { Alert, Button, Collapse, InlineField, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; +import { Button, Collapse, InlineField, TooltipDisplayMode, useStyles2, useTheme2 } from '@grafana/ui'; import { ExploreGraph } from './Graph/ExploreGraph'; +import { SupplementaryResultError } from './SupplementaryResultError'; type Props = { logsVolumeData: DataQueryResponse | undefined; @@ -29,35 +29,6 @@ type Props = { eventBus: EventBus; }; -const SHORT_ERROR_MESSAGE_LIMIT = 100; - -function ErrorAlert(props: { error: DataQueryError }) { - const [isOpen, setIsOpen] = useState(false); - // generic get-error-message-logic, taken from - // /public/app/features/explore/ErrorContainer.tsx - const message = props.error.message || props.error.data?.message || ''; - - const showButton = !isOpen && message.length > SHORT_ERROR_MESSAGE_LIMIT; - - return ( - - {showButton ? ( - - ) : ( - message - )} - - ); -} - function createVisualisationData( logLinesBased: DataQueryResponse | undefined, logLinesBasedVisibleRange: AbsoluteTimeRange | undefined, @@ -110,7 +81,7 @@ export function LogsVolumePanel(props: Props) { const { logsVolumeData, fullRangeData, range } = data; if (logsVolumeData.error !== undefined) { - return ; + return ; } let LogsVolumePanelContent; diff --git a/public/app/features/explore/SupplementaryResultError.test.tsx b/public/app/features/explore/SupplementaryResultError.test.tsx new file mode 100644 index 00000000000..c735d1b9818 --- /dev/null +++ b/public/app/features/explore/SupplementaryResultError.test.tsx @@ -0,0 +1,30 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { SupplementaryResultError } from './SupplementaryResultError'; + +describe('SupplementaryResultError', () => { + it('shows short warning message', () => { + const error = { data: { message: 'Test error message' } }; + const title = 'Error loading supplementary query'; + + render(); + expect(screen.getByText(title)).toBeInTheDocument(); + expect(screen.getByText(error.data.message)).toBeInTheDocument(); + }); + + it('shows long warning message', () => { + // we make a long message + const messagePart = 'One two three four five six seven eight nine ten.'; + const message = messagePart.repeat(3); + const error = { data: { message } }; + const title = 'Error loading supplementary query'; + + render(); + expect(screen.getByText(title)).toBeInTheDocument(); + expect(screen.queryByText(message)).not.toBeInTheDocument(); + const button = screen.getByText('Show details'); + button.click(); + expect(screen.getByText(message)).toBeInTheDocument(); + }); +}); diff --git a/public/app/features/explore/SupplementaryResultError.tsx b/public/app/features/explore/SupplementaryResultError.tsx new file mode 100644 index 00000000000..237c2072db5 --- /dev/null +++ b/public/app/features/explore/SupplementaryResultError.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; + +import { DataQueryError } from '@grafana/data'; +import { Alert, Button } from '@grafana/ui'; + +type Props = { + error: DataQueryError; + title: string; +}; +export function SupplementaryResultError(props: Props) { + const [isOpen, setIsOpen] = useState(false); + const SHORT_ERROR_MESSAGE_LIMIT = 100; + const { error, title } = props; + // generic get-error-message-logic, taken from + // /public/app/features/explore/ErrorContainer.tsx + const message = error.message || error.data?.message || ''; + const showButton = !isOpen && message.length > SHORT_ERROR_MESSAGE_LIMIT; + + return ( + + {showButton ? ( + + ) : ( + message + )} + + ); +} diff --git a/public/app/features/explore/state/query.test.ts b/public/app/features/explore/state/query.test.ts index 5e039079985..bfb5223a52b 100644 --- a/public/app/features/explore/state/query.test.ts +++ b/public/app/features/explore/state/query.test.ts @@ -517,7 +517,7 @@ describe('reducer', () => { mockDataProvider = () => { return of({ state: LoadingState.Done, error: undefined, data: [{}] }); }; - // turn logs volume off (but keep log sample on) + // turn logs volume off (but keep logs sample on) dispatch(setSupplementaryQueryEnabled(ExploreId.left, false, SupplementaryQueryType.LogsVolume)); expect(getState().explore[ExploreId.left].supplementaryQueries[SupplementaryQueryType.LogsVolume].enabled).toBe( false diff --git a/public/app/features/explore/utils/supplementaryQueries.ts b/public/app/features/explore/utils/supplementaryQueries.ts index 3847d6775f1..cef393d2401 100644 --- a/public/app/features/explore/utils/supplementaryQueries.ts +++ b/public/app/features/explore/utils/supplementaryQueries.ts @@ -17,8 +17,7 @@ export const loadSupplementaryQueries = (): SupplementaryQueries => { // We default to true for all supp queries let supplementaryQueries: SupplementaryQueries = { [SupplementaryQueryType.LogsVolume]: { enabled: true }, - // This is set to false temporarily, until we have UI to display logs sample and a way how to enable/disable it - [SupplementaryQueryType.LogsSample]: { enabled: false }, + [SupplementaryQueryType.LogsSample]: { enabled: true }, }; for (const type of supplementaryQueryTypes) { diff --git a/public/app/features/geo/editor/locationEditor.ts b/public/app/features/geo/editor/locationEditor.ts index 5f7d6da044f..a1deb38f8a3 100644 --- a/public/app/features/geo/editor/locationEditor.ts +++ b/public/app/features/geo/editor/locationEditor.ts @@ -1,11 +1,5 @@ -import { - Field, - FieldType, - FrameGeometrySource, - FrameGeometrySourceMode, - PanelOptionsEditorBuilder, - DataFrame, -} from '@grafana/data'; +import { Field, FieldType, PanelOptionsEditorBuilder, DataFrame } from '@grafana/data'; +import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; import { GazetteerPathEditor } from 'app/features/geo/editor/GazetteerPathEditor'; import { LocationModeEditor } from './locationModeEditor'; diff --git a/public/app/features/geo/editor/locationModeEditor.tsx b/public/app/features/geo/editor/locationModeEditor.tsx index 34cc551f4cd..204db9200c9 100644 --- a/public/app/features/geo/editor/locationModeEditor.tsx +++ b/public/app/features/geo/editor/locationModeEditor.tsx @@ -1,14 +1,9 @@ import { css } from '@emotion/css'; import React, { useEffect, useState } from 'react'; -import { - StandardEditorProps, - FrameGeometrySourceMode, - DataFrame, - FrameGeometrySource, - GrafanaTheme2, -} from '@grafana/data'; +import { StandardEditorProps, DataFrame, GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; import { Alert, HorizontalGroup, Icon, Select, useStyles2 } from '@grafana/ui'; import { FrameGeometryField, getGeometryField, getLocationMatchers } from '../utils/location'; diff --git a/public/app/features/geo/utils/location.test.ts b/public/app/features/geo/utils/location.test.ts index 18ce5d83b1a..548fa4fd666 100644 --- a/public/app/features/geo/utils/location.test.ts +++ b/public/app/features/geo/utils/location.test.ts @@ -1,7 +1,8 @@ import { Point } from 'ol/geom'; import { toLonLat } from 'ol/proj'; -import { toDataFrame, FieldType, FrameGeometrySourceMode } from '@grafana/data'; +import { toDataFrame, FieldType } from '@grafana/data'; +import { FrameGeometrySourceMode } from '@grafana/schema'; import { getGeometryField, getLocationFields, getLocationMatchers } from './location'; diff --git a/public/app/features/geo/utils/location.ts b/public/app/features/geo/utils/location.ts index 42661776f5a..5c0828a834f 100644 --- a/public/app/features/geo/utils/location.ts +++ b/public/app/features/geo/utils/location.ts @@ -1,8 +1,6 @@ import { Geometry } from 'ol/geom'; import { - FrameGeometrySource, - FrameGeometrySourceMode, FieldMatcher, getFieldMatcher, FieldMatcherID, @@ -11,6 +9,7 @@ import { getFieldDisplayName, FieldType, } from '@grafana/data'; +import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; import { getGeoFieldFromGazetteer, pointFieldFromGeohash, pointFieldFromLonLat } from '../format/utils'; import { getGazetteer, Gazetteer } from '../gazetteer/gazetteer'; diff --git a/public/app/features/inspector/InspectDataTab.tsx b/public/app/features/inspector/InspectDataTab.tsx index df9239a76fa..b615a008614 100644 --- a/public/app/features/inspector/InspectDataTab.tsx +++ b/public/app/features/inspector/InspectDataTab.tsx @@ -108,7 +108,7 @@ export class InspectDataTab extends PureComponent { area: 'inspector', }); - const logsModel = dataFrameToLogsModel(data || [], undefined); + const logsModel = dataFrameToLogsModel(data || []); downloadLogsModelAsTxt(logsModel, panel ? panel.getDisplayTitle() : 'Explore'); }; diff --git a/public/app/features/logs/components/LogRowMessage.tsx b/public/app/features/logs/components/LogRowMessage.tsx index 37dffc7228b..e0fd1d69fb7 100644 --- a/public/app/features/logs/components/LogRowMessage.tsx +++ b/public/app/features/logs/components/LogRowMessage.tsx @@ -32,7 +32,7 @@ interface Props extends Themeable2 { logsSortOrder?: LogsSortOrder | null; } -const getStyles = (theme: GrafanaTheme2, showContextButton: boolean, isInDashboard: boolean | undefined) => { +const getStyles = (theme: GrafanaTheme2, showContextButton: boolean, isInExplore: boolean) => { const outlineColor = tinycolor(theme.components.dashboard.background).setAlpha(0.7).toRgbString(); return { @@ -74,7 +74,7 @@ const getStyles = (theme: GrafanaTheme2, showContextButton: boolean, isInDashboa `, logRowMenuCell: css` position: absolute; - right: ${isInDashboard ? '40px' : `calc(75px + ${theme.spacing()} + ${showContextButton ? '80px' : '40px'})`}; + right: ${!isInExplore ? '40px' : `calc(75px + ${theme.spacing()} + ${showContextButton ? '80px' : '40px'})`}; margin-top: -${theme.spacing(0.125)}; `, logLine: css` @@ -169,7 +169,7 @@ class UnThemedLogRowMessage extends PureComponent { const { hasAnsi, raw } = row; const restructuredEntry = restructureLog(raw, prettifyLogMessage); const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false; - const styles = getStyles(theme, shouldShowContextToggle, app === CoreApp.Dashboard); + const styles = getStyles(theme, shouldShowContextToggle, app === CoreApp.Explore); return ( <> diff --git a/public/app/features/profile/UserProfileEditForm.tsx b/public/app/features/profile/UserProfileEditForm.tsx index 530121c5c9d..1a2f76935c5 100644 --- a/public/app/features/profile/UserProfileEditForm.tsx +++ b/public/app/features/profile/UserProfileEditForm.tsx @@ -31,7 +31,7 @@ export const UserProfileEditForm: FC = ({ user, isSavingUser, updateProfi
{({ register, errors }) => { return ( -
Profile}> +
{ await getTestContext(); const { name, email, username, saveProfile } = getSelectors(); - expect(screen.getByText(/profile/i)).toBeInTheDocument(); expect(name()).toBeInTheDocument(); expect(name()).toHaveValue('Test User'); expect(email()).toBeInTheDocument(); diff --git a/public/app/features/scenes/builders/panelBuilders.ts b/public/app/features/scenes/builders/panelBuilders.ts index d0c1247148d..cb61d937e3b 100644 --- a/public/app/features/scenes/builders/panelBuilders.ts +++ b/public/app/features/scenes/builders/panelBuilders.ts @@ -1,6 +1,6 @@ import { VizPanel, VizPanelState } from '@grafana/scenes'; import { GraphFieldConfig, TableFieldOptions } from '@grafana/schema'; -import { PanelOptions as BarGaugePanelOptions } from 'app/plugins/panel/bargauge/models.gen'; +import { PanelOptions as BarGaugePanelOptions } from 'app/plugins/panel/bargauge/panelcfg.gen'; import { PanelOptions as TablePanelOptions } from 'app/plugins/panel/table/models.gen'; import { TimeSeriesOptions } from 'app/plugins/panel/timeseries/types'; diff --git a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx index e499d1845e6..7c29bb96faf 100644 --- a/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx +++ b/public/app/features/transformers/spatial/SpatialTransformerEditor.tsx @@ -3,8 +3,6 @@ import React, { useEffect } from 'react'; import { DataTransformerID, - FrameGeometrySource, - FrameGeometrySourceMode, GrafanaTheme2, PanelOptionsEditorBuilder, PluginState, @@ -12,6 +10,7 @@ import { TransformerRegistryItem, TransformerUIProps, } from '@grafana/data'; +import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; import { useTheme2 } from '@grafana/ui'; import { addLocationFields } from 'app/features/geo/editor/locationEditor'; diff --git a/public/app/features/transformers/spatial/models.gen.ts b/public/app/features/transformers/spatial/models.gen.ts index 7b30638ec4c..4def42da1c8 100644 --- a/public/app/features/transformers/spatial/models.gen.ts +++ b/public/app/features/transformers/spatial/models.gen.ts @@ -1,4 +1,4 @@ -import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/data'; +import { FrameGeometrySource, FrameGeometrySourceMode } from '@grafana/schema'; // This file should be generated by cue schema diff --git a/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx b/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx index c520d8a2ea0..0df75f024f1 100644 --- a/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/LogsCheatSheet.tsx @@ -237,6 +237,7 @@ export default class LogsCheatSheet extends PureComponent< region: this.props.query.region, id: this.props.query.refId ?? 'A', logGroupNames: 'logGroupNames' in this.props.query ? this.props.query.logGroupNames : [], + logGroups: 'logGroups' in this.props.query ? this.props.query.logGroups : [], }) } > diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/AdvancedResourcePicker.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/AdvancedResourcePicker.test.tsx new file mode 100644 index 00000000000..91eaa57842d --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/AdvancedResourcePicker.test.tsx @@ -0,0 +1,50 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import AdvancedResourcePicker from './AdvancedResourcePicker'; + +describe('AdvancedResourcePicker', () => { + it('should set a parameter as an object', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + + const subsInput = await screen.findByTestId('input-advanced-resource-picker-1'); + await userEvent.type(subsInput, 'd'); + expect(onChange).toHaveBeenCalledWith(['d']); + + rerender(); + expect(screen.getByDisplayValue('/subscriptions/def-123')).toBeInTheDocument(); + }); + + it('should initialize with an empty resource', () => { + const onChange = jest.fn(); + render(); + expect(onChange).toHaveBeenCalledWith(['']); + }); + + it('should add a resource', async () => { + const onChange = jest.fn(); + render(); + const addButton = await screen.findByText('Add resource URI'); + addButton.click(); + expect(onChange).toHaveBeenCalledWith(['/subscriptions/def-123', '']); + }); + + it('should remove a resource', async () => { + const onChange = jest.fn(); + render(); + const removeButton = await screen.findByTestId('remove-resource'); + removeButton.click(); + expect(onChange).toHaveBeenCalledWith([]); + }); + + it('should render multiple resources', async () => { + render( + + ); + + expect(screen.getByDisplayValue('/subscriptions/def-123')).toBeInTheDocument(); + expect(screen.getByDisplayValue('/subscriptions/def-456')).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/AdvancedResourcePicker.tsx new file mode 100644 index 00000000000..fc5d7347507 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/AdvancedResourcePicker.tsx @@ -0,0 +1,97 @@ +import { css } from '@emotion/css'; +import React, { useEffect } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { AccessoryButton } from '@grafana/experimental'; +import { Icon, Input, Tooltip, Label, Button, useStyles2 } from '@grafana/ui'; + +export interface ResourcePickerProps { + resources: T[]; + onChange: (resources: T[]) => void; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + resourceList: css({ width: '100%', display: 'flex', marginBlock: theme.spacing(1) }), +}); + +const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps) => { + const styles = useStyles2(getStyles); + + useEffect(() => { + // Ensure there is at least one resource + if (resources.length === 0) { + onChange(['']); + } + }, [resources, onChange]); + + const onResourceChange = (index: number, resource: string) => { + const newResources = [...resources]; + newResources[index] = resource; + onChange(newResources); + }; + + const removeResource = (index: number) => { + const newResources = [...resources]; + newResources.splice(index, 1); + onChange(newResources); + }; + + const addResource = () => { + onChange(resources.concat('')); + }; + + return ( + <> + + {resources.map((resource, index) => ( +
+
+ onResourceChange(index, event.currentTarget.value)} + placeholder="ex: /subscriptions/$subId" + data-testid={`input-advanced-resource-picker-${index + 1}`} + /> + removeResource(index)} + data-testid={`remove-resource`} + hidden={resources.length === 1} + /> +
+
+ ))} + + + ); +}; + +export default AdvancedResourcePicker; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx index 9513ee1f6d3..b1c7d95d64c 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -10,6 +10,7 @@ import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; import { parseResourceDetails } from '../ResourcePicker/utils'; +import AdvancedResourcePicker from './AdvancedResourcePicker'; import FormatAsField from './FormatAsField'; import QueryField from './QueryField'; import useMigrations from './useMigrations'; @@ -75,6 +76,12 @@ const LogsQueryEditor: React.FC = ({ resources={query.azureLogAnalytics?.resources ?? []} queryType="logs" disableRow={disableRow} + renderAdvanced={(resources, onChange) => ( + // It's required to cast resources because the resource picker + // specifies the type to string | AzureMetricResource. + // eslint-disable-next-line + + )} /> diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AdvancedResourcePicker.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AdvancedResourcePicker.test.tsx new file mode 100644 index 00000000000..57ff4eaa53a --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AdvancedResourcePicker.test.tsx @@ -0,0 +1,104 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import AdvancedResourcePicker from './AdvancedResourcePicker'; + +describe('AdvancedResourcePicker', () => { + it('should set a parameter as an object', async () => { + const onChange = jest.fn(); + const { rerender } = render(); + + const subsInput = await screen.findByLabelText('Subscription'); + await userEvent.type(subsInput, 'd'); + expect(onChange).toHaveBeenCalledWith([{ subscription: 'd' }]); + + rerender(); + expect(screen.getByLabelText('Subscription').outerHTML).toMatch('value="def-123"'); + }); + + it('should initialize with an empty resource', () => { + const onChange = jest.fn(); + render(); + expect(onChange).toHaveBeenCalledWith([{}]); + }); + + it('should add a resource', async () => { + const onChange = jest.fn(); + render(); + const addButton = await screen.findByText('Add resource'); + addButton.click(); + expect(onChange).toHaveBeenCalledWith([ + { subscription: 'def-123' }, + { subscription: 'def-123', resourceGroup: '', resourceName: '' }, + ]); + }); + + it('should remove a resource', async () => { + const onChange = jest.fn(); + render(); + const removeButton = await screen.findByTestId('remove-resource'); + removeButton.click(); + expect(onChange).toHaveBeenCalledWith([]); + }); + + it('should update all resources when editing the subscription', async () => { + const onChange = jest.fn(); + render( + + ); + const subsInput = await screen.findByLabelText('Subscription'); + await userEvent.type(subsInput, 'd'); + expect(onChange).toHaveBeenCalledWith([{ subscription: 'def-123d' }, { subscription: 'def-123d' }]); + }); + + it('should update all resources when editing the namespace', async () => { + const onChange = jest.fn(); + render( + + ); + const subsInput = await screen.findByLabelText('Namespace'); + await userEvent.type(subsInput, 'b'); + expect(onChange).toHaveBeenCalledWith([{ metricNamespace: 'aab' }, { metricNamespace: 'aab' }]); + }); + + it('should update all resources when editing the region', async () => { + const onChange = jest.fn(); + render(); + const subsInput = await screen.findByLabelText('Region'); + await userEvent.type(subsInput, 'b'); + expect(onChange).toHaveBeenCalledWith([{ region: 'aab' }, { region: 'aab' }]); + }); + + it('should render multiple resources', async () => { + render( + + ); + + expect(screen.getByDisplayValue('sub1')).toBeInTheDocument(); + expect(screen.getByDisplayValue('ns1')).toBeInTheDocument(); + expect(screen.getByDisplayValue('rg1')).toBeInTheDocument(); + expect(screen.getByDisplayValue('res1')).toBeInTheDocument(); + expect(screen.getByDisplayValue('rg2')).toBeInTheDocument(); + expect(screen.getByDisplayValue('res2')).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AdvancedResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AdvancedResourcePicker.tsx new file mode 100644 index 00000000000..94bc7c88823 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/AdvancedResourcePicker.tsx @@ -0,0 +1,163 @@ +import { css } from '@emotion/css'; +import React, { useEffect } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { AccessoryButton } from '@grafana/experimental'; +import { Input, Label, InlineField, Button, useStyles2 } from '@grafana/ui'; + +import { selectors } from '../../e2e/selectors'; +import { AzureMetricResource } from '../../types'; + +export interface ResourcePickerProps { + resources: T[]; + onChange: (resources: T[]) => void; +} + +const getStyles = (theme: GrafanaTheme2) => ({ + resourceList: css({ display: 'flex', columnGap: theme.spacing(1), flexWrap: 'wrap', marginBottom: theme.spacing(1) }), + resource: css({ flex: '0 0 auto' }), + resourceLabel: css({ padding: theme.spacing(1) }), + resourceGroupAndName: css({ display: 'flex', columnGap: theme.spacing(0.5) }), +}); + +const AdvancedResourcePicker = ({ resources, onChange }: ResourcePickerProps) => { + const styles = useStyles2(getStyles); + + useEffect(() => { + // Ensure there is at least one resource + if (resources.length === 0) { + onChange([{}]); + } + }, [resources, onChange]); + + const onResourceChange = (index: number, resource: AzureMetricResource) => { + const newResources = [...resources]; + newResources[index] = resource; + onChange(newResources); + }; + + const removeResource = (index: number) => { + const newResources = [...resources]; + newResources.splice(index, 1); + onChange(newResources); + }; + + const addResource = () => { + onChange( + resources.concat({ + subscription: resources[0]?.subscription, + metricNamespace: resources[0]?.metricNamespace, + resourceGroup: '', + resourceName: '', + }) + ); + }; + + const onCommonPropChange = (r: Partial) => { + onChange(resources.map((resource) => ({ ...resource, ...r }))); + }; + + return ( + <> + + onCommonPropChange({ subscription: event.currentTarget.value })} + placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee" + /> + + + onCommonPropChange({ metricNamespace: event.currentTarget.value })} + placeholder="Microsoft.Insights/metricNamespaces" + /> + + + onCommonPropChange({ region: event.currentTarget.value })} + placeholder="northeurope" + /> + +
+ {resources.map((resource, index) => ( +
+ {resources.length !== 1 && } + +
+ + onResourceChange(index, { ...resource, resourceGroup: event.currentTarget.value }) + } + placeholder="resource-group" + /> + removeResource(index)} + hidden={resources.length === 1} + data-testid={'remove-resource'} + /> +
+
+ + + onResourceChange(index, { ...resource, resourceName: event.currentTarget.value })} + placeholder="name" + /> + +
+ ))} +
+ + + ); +}; + +export default AdvancedResourcePicker; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx index 262840b382d..ec2e079fbb5 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/MetricsQueryEditor/MetricsQueryEditor.tsx @@ -6,11 +6,12 @@ import { config } from '@grafana/runtime'; import { multiResourceCompatibleTypes } from '../../azureMetadata'; import type Datasource from '../../datasource'; -import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types'; +import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish, AzureMetricResource } from '../../types'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; import { parseResourceDetails } from '../ResourcePicker/utils'; +import AdvancedResourcePicker from './AdvancedResourcePicker'; import AggregationField from './AggregationField'; import DimensionFields from './DimensionFields'; import LegendFormatField from './LegendFormatField'; @@ -88,6 +89,12 @@ const MetricsQueryEditor: React.FC = ({ resources={resources ?? []} queryType={'metrics'} disableRow={disableRow} + renderAdvanced={(resources, onChange) => ( + // It's required to cast resources because the resource picker + // specifies the type to string | AzureMetricResource. + // eslint-disable-next-line + + )} /> extends AzureQueryEditorFieldProps { inlineField?: boolean; labelWidth?: number; disableRow: (row: ResourceRow, selectedRows: ResourceRowGroup) => boolean; + renderAdvanced: (resources: T[], onChange: (resources: T[]) => void) => React.ReactNode; } const ResourceField: React.FC> = ({ @@ -32,6 +33,7 @@ const ResourceField: React.FC> inlineField, labelWidth, disableRow, + renderAdvanced, }) => { const styles = useStyles2(getStyles); const [pickerIsOpen, setPickerIsOpen] = useState(false); @@ -71,6 +73,7 @@ const ResourceField: React.FC> selectableEntryTypes={selectableEntryTypes} queryType={queryType} disableRow={disableRow} + renderAdvanced={renderAdvanced} /> diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/AdvancedMulti.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/AdvancedMulti.test.tsx new file mode 100644 index 00000000000..d6a5d9e2cce --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/AdvancedMulti.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import AdvancedMulti from './AdvancedMulti'; + +describe('AdvancedMulti', () => { + it('should expand and render a section', async () => { + const onChange = jest.fn(); + const renderAdvanced = jest.fn().mockReturnValue(
details!
); + render(); + const advancedSection = screen.getByText('Advanced'); + advancedSection.click(); + + expect(await screen.findByText('details!')).toBeInTheDocument(); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/AdvancedMulti.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/AdvancedMulti.tsx new file mode 100644 index 00000000000..9c7b9b35e51 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/AdvancedMulti.tsx @@ -0,0 +1,33 @@ +import React, { useState } from 'react'; + +import { Collapse } from '@grafana/ui'; + +import { selectors } from '../../e2e/selectors'; +import { AzureMetricResource } from '../../types'; +import { Space } from '../Space'; + +export interface ResourcePickerProps { + resources: T[]; + onChange: (resources: T[]) => void; + renderAdvanced: (resources: T[], onChange: (resources: T[]) => void) => React.ReactNode; +} + +const AdvancedMulti = ({ resources, onChange, renderAdvanced }: ResourcePickerProps) => { + const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resources.length && JSON.stringify(resources).includes('$')); + + return ( +
+ setIsAdvancedOpen(!isAdvancedOpen)} + > + {renderAdvanced(resources, onChange)} + + +
+ ); +}; + +export default AdvancedMulti; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx index 0c201f583ab..3bad6927b27 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.test.tsx @@ -59,7 +59,7 @@ const queryType: ResourcePickerQueryType = 'logs'; const defaultProps = { templateVariables: [], - resources: [noResourceURI], + resources: [], resourcePickerData: createMockResourcePickerData(), onCancel: noop, onApply: noop, @@ -71,6 +71,7 @@ const defaultProps = { ], queryType, disableRow: jest.fn(), + renderAdvanced: jest.fn(), }; describe('AzureMonitor ResourcePicker', () => { @@ -141,6 +142,7 @@ describe('AzureMonitor ResourcePicker', () => { expect(subscriptionCheckbox).not.toBeChecked(); subscriptionCheckbox.click(); const applyButton = screen.getByRole('button', { name: 'Apply' }); + expect(applyButton).toBeEnabled(); applyButton.click(); expect(onApply).toBeCalledTimes(1); expect(onApply).toBeCalledWith(['/subscriptions/def-123']); @@ -174,26 +176,56 @@ describe('AzureMonitor ResourcePicker', () => { expect(onApply).toBeCalledWith([]); }); - it('should call onApply with a new subscription when a user clicks on the checkbox in the row', async () => { + it('should call onApply with a new resource when a user clicks on the checkbox in the row', async () => { const onApply = jest.fn(); - render(); - const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); - expect(subscriptionCheckbox).toBeInTheDocument(); - expect(subscriptionCheckbox).not.toBeChecked(); - subscriptionCheckbox.click(); + render(); + + const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' }); + expect(subscriptionButton).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument(); + subscriptionButton.click(); + + const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' }); + resourceGroupButton.click(); + const checkbox = await screen.findByLabelText('web-server'); + await userEvent.click(checkbox); + expect(checkbox).toBeChecked(); const applyButton = screen.getByRole('button', { name: 'Apply' }); applyButton.click(); + expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]); + expect(onApply).toBeCalledWith([ + { + metricNamespace: 'Microsoft.Compute/virtualMachines', + region: 'northeurope', + resourceGroup: 'dev-3', + resourceName: 'web-server', + subscription: 'def-456', + }, + ]); }); it('should call onApply removing a resource element', async () => { const onApply = jest.fn(); - render(); - const subscriptionCheckbox = await screen.findAllByLabelText('Primary Subscription'); - expect(subscriptionCheckbox).toHaveLength(2); - expect(subscriptionCheckbox.at(0)).toBeChecked(); - subscriptionCheckbox.at(0)?.click(); + render( + + ); + const checkbox = await screen.findAllByLabelText('web-server'); + expect(checkbox).toHaveLength(2); + expect(checkbox.at(0)).toBeChecked(); + checkbox.at(0)?.click(); const applyButton = screen.getByRole('button', { name: 'Apply' }); applyButton.click(); expect(onApply).toBeCalledTimes(1); @@ -202,7 +234,7 @@ describe('AzureMonitor ResourcePicker', () => { it('should call onApply with a new subscription uri when a user types it in the selection box', async () => { const onApply = jest.fn(); - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -222,7 +254,7 @@ describe('AzureMonitor ResourcePicker', () => { it('should call onApply with a new subscription when a user types it in the selection box', async () => { const onApply = jest.fn(); - render(); + render(); const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription'); expect(subscriptionCheckbox).toBeInTheDocument(); expect(subscriptionCheckbox).not.toBeChecked(); @@ -232,20 +264,41 @@ describe('AzureMonitor ResourcePicker', () => { const advancedInput = await screen.findByLabelText('Subscription'); await userEvent.type(advancedInput, 'def-123'); + const nsInput = await screen.findByLabelText('Namespace'); + await userEvent.type(nsInput, 'ns'); + const rgInput = await screen.findByLabelText('Resource Group'); + await userEvent.type(rgInput, 'rg'); + const rnInput = await screen.findByLabelText('Resource Name'); + await userEvent.type(rnInput, 'rn'); const applyButton = screen.getByRole('button', { name: 'Apply' }); applyButton.click(); expect(onApply).toBeCalledTimes(1); - expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]); + expect(onApply).toBeCalledWith([ + { subscription: 'def-123', metricNamespace: 'ns', resourceGroup: 'rg', resourceName: 'rn' }, + ]); }); it('should show unselect a subscription if the value is manually edited', async () => { - render(); - const subscriptionCheckboxes = await screen.findAllByLabelText('Dev Subscription'); - expect(subscriptionCheckboxes.length).toBe(2); - expect(subscriptionCheckboxes[0]).toBeChecked(); - expect(subscriptionCheckboxes[1]).toBeChecked(); + render( + + ); + const checkboxes = await screen.findAllByLabelText('web-server'); + expect(checkboxes.length).toBe(2); + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[1]).toBeChecked(); const advancedSection = screen.getByText('Advanced'); advancedSection.click(); @@ -253,7 +306,7 @@ describe('AzureMonitor ResourcePicker', () => { const advancedInput = await screen.findByLabelText('Subscription'); await userEvent.type(advancedInput, 'def-123'); - const updatedCheckboxes = await screen.findAllByLabelText('Dev Subscription'); + const updatedCheckboxes = await screen.findAllByLabelText('web-server'); expect(updatedCheckboxes.length).toBe(1); expect(updatedCheckboxes[0]).not.toBeChecked(); }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx index 6eb36f5776f..c99550162c3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/ResourcePicker.tsx @@ -2,6 +2,7 @@ import { cx } from '@emotion/css'; import React, { useCallback, useEffect, useState } from 'react'; import { useEffectOnce } from 'react-use'; +import { config } from '@grafana/runtime'; import { Alert, Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { selectors } from '../../e2e/selectors'; @@ -11,6 +12,7 @@ import messageFromError from '../../utils/messageFromError'; import { Space } from '../Space'; import Advanced from './Advanced'; +import AdvancedMulti from './AdvancedMulti'; import NestedRow from './NestedRow'; import Search from './Search'; import getStyles from './styles'; @@ -26,6 +28,7 @@ interface ResourcePickerProps { onApply: (resources: T[]) => void; onCancel: () => void; disableRow: (row: ResourceRow, selectedRows: ResourceRowGroup) => boolean; + renderAdvanced: (resources: T[], onChange: (resources: T[]) => void) => React.ReactNode; } const ResourcePicker = ({ @@ -36,6 +39,7 @@ const ResourcePicker = ({ selectableEntryTypes, queryType, disableRow, + renderAdvanced, }: ResourcePickerProps) => { const styles = useStyles2(getStyles); @@ -71,13 +75,18 @@ const ResourcePicker = ({ loadInitialData(); }); + // Avoid using empty resources + const isValid = (r: string | AzureMetricResource) => + typeof r === 'string' ? r !== '' : r.subscription && r.resourceGroup && r.resourceName && r.metricNamespace; + // set selected row data whenever row or selection changes useEffect(() => { if (!internalSelected) { setSelectedRows([]); } - const found = internalSelected && findRows(rows, resourcesToStrings(internalSelected)); + const sanitized = internalSelected.filter((r) => isValid(r)); + const found = internalSelected && findRows(rows, resourcesToStrings(sanitized)); if (found && found.length) { return setSelectedRows(found); } @@ -106,15 +115,11 @@ const ResourcePicker = ({ [resourcePickerData, rows, queryType] ); - const resourceIsString = resources?.length && typeof resources[0] === 'string'; const handleSelectionChanged = useCallback( (row: ResourceRow, isSelected: boolean) => { if (isSelected) { - const newRes = resourceIsString ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0]; - const newSelected = (internalSelected ? internalSelected.concat(newRes) : [newRes]).filter((r) => { - // avoid setting empty resources - return typeof r === 'string' ? r !== '' : r.subscription; - }); + const newRes = queryType === 'logs' ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0]; + const newSelected = internalSelected ? internalSelected.concat(newRes) : [newRes]; setInternalSelected(newSelected); } else { const newInternalSelected = internalSelected?.filter((r) => { @@ -123,14 +128,14 @@ const ResourcePicker = ({ setInternalSelected(newInternalSelected); } }, - [resourceIsString, internalSelected, setInternalSelected] + [queryType, internalSelected, setInternalSelected] ); const handleApply = useCallback(() => { if (internalSelected) { - onApply(resourceIsString ? internalSelected : parseMultipleResourceDetails(internalSelected)); + onApply(queryType === 'logs' ? internalSelected : parseMultipleResourceDetails(internalSelected)); } - }, [resourceIsString, internalSelected, onApply]); + }, [queryType, internalSelected, onApply]); const handleSearch = useCallback( async (searchWord: string) => { @@ -239,11 +244,20 @@ const ResourcePicker = ({ )} - setInternalSelected(r)} /> + {config.featureToggles.azureMultipleResourcePicker ? ( + setInternalSelected(r)} + renderAdvanced={renderAdvanced} + /> + ) : ( + setInternalSelected(r)} /> + )} +