Merge remote-tracking branch 'origin/main' into andre/traces-and-profiling-datasources-tracing

# Conflicts:
#	pkg/tsdb/grafana-pyroscope-datasource/pyroscopeClient.go
This commit is contained in:
André Pereira
2023-10-12 13:41:19 +01:00
359 changed files with 7713 additions and 2981 deletions
+33 -40
View File
@@ -173,7 +173,8 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"packages/grafana-data/src/field/fieldOverrides.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"packages/grafana-data/src/field/overrides/processors.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -1091,6 +1092,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"packages/grafana-ui/src/components/uPlot/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -1330,11 +1334,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Do not use any type assertions.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Do not use any type assertions.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Do not use any type assertions.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Do not use any type assertions.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Do not use any type assertions.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Do not use any type assertions.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Do not use any type assertions.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
@@ -1385,15 +1389,10 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "72"],
[0, 0, 0, "Do not use any type assertions.", "73"],
[0, 0, 0, "Unexpected any. Specify a different type.", "74"],
[0, 0, 0, "Unexpected any. Specify a different type.", "75"],
[0, 0, 0, "Do not use any type assertions.", "76"],
[0, 0, 0, "Unexpected any. Specify a different type.", "77"],
[0, 0, 0, "Do not use any type assertions.", "78"],
[0, 0, 0, "Unexpected any. Specify a different type.", "79"],
[0, 0, 0, "Do not use any type assertions.", "80"],
[0, 0, 0, "Unexpected any. Specify a different type.", "81"],
[0, 0, 0, "Do not use any type assertions.", "82"],
[0, 0, 0, "Unexpected any. Specify a different type.", "83"]
[0, 0, 0, "Do not use any type assertions.", "75"],
[0, 0, 0, "Unexpected any. Specify a different type.", "76"],
[0, 0, 0, "Do not use any type assertions.", "77"],
[0, 0, 0, "Unexpected any. Specify a different type.", "78"]
],
"public/app/core/components/OptionsUI/slider.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
@@ -1762,9 +1761,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/features/admin/ldap/LdapPage.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/alerting/AlertTab.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -2910,10 +2906,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "1"],
[0, 0, 0, "Styles should be written using objects.", "2"]
],
"public/app/features/canvas/elements/rectangle.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/features/canvas/elements/server/server.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],
@@ -3007,6 +2999,10 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"public/app/features/dashboard-scene/serialization/angularMigration.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@@ -3016,7 +3012,10 @@ exports[`better eslint`] = {
[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"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
[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.", "7"]
],
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@@ -3579,18 +3578,19 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Do not use any type assertions.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Do not use any type assertions.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Do not use any type assertions.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Do not use any type assertions.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Do not use any type assertions.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Do not use any type assertions.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Do not use any type assertions.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Do not use any type assertions.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"]
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"]
],
"public/app/features/dashboard/state/TimeModel.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -3807,9 +3807,6 @@ exports[`better eslint`] = {
"public/app/features/explore/ExplorePaneContainer.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/explore/ExploreQueryInspector.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/explore/FeatureTogglePage.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
@@ -3990,11 +3987,7 @@ exports[`better eslint`] = {
"public/app/features/explore/TraceView/TraceView.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"]
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/features/explore/TraceView/components/TracePageHeader/Actions/ActionButton.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
+101 -101
View File
@@ -24,7 +24,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- ./bin/build verify-drone
@@ -74,14 +74,14 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- go install github.com/bazelbuild/buildtools/buildifier@latest
- buildifier --lint=warn -mode=check -r .
depends_on:
- compile-build-cmd
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: lint-starlark
trigger:
event:
@@ -316,7 +316,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -325,21 +325,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -348,7 +348,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend-integration
trigger:
event:
@@ -397,7 +397,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- apk add --update curl jq bash
@@ -424,7 +424,7 @@ steps:
- apk add --update make
- make gen-go
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- apk add --update make build-base
@@ -433,16 +433,16 @@ steps:
- wire-install
environment:
CGO_ENABLED: "1"
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: lint-backend
- commands:
- go run scripts/modowners/modowners.go check go.mod
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: validate-modfile
- commands:
- apk add --update make
- make swagger-validate
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: validate-openapi-spec
trigger:
event:
@@ -498,7 +498,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -508,7 +508,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -517,14 +517,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- yarn install --immutable
@@ -587,7 +587,7 @@ steps:
token:
from_secret: drone_token
- commands:
- /src/grafana-build package --distro=linux/amd64,linux/arm64 --go-version=1.20.8
- /src/grafana-build package --distro=linux/amd64,linux/arm64 --go-version=1.20.10
--yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD
> packages.txt
depends_on:
@@ -871,7 +871,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
@@ -885,7 +885,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -894,14 +894,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- dockerize -wait tcp://postgres:5432 -timeout 120s
@@ -922,7 +922,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql57:3306 -timeout 120s
@@ -943,7 +943,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-5.7-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
@@ -964,7 +964,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-8.0-integration-tests
- commands:
- dockerize -wait tcp://redis:6379 -timeout 120s
@@ -979,7 +979,7 @@ steps:
- wait-for-redis
environment:
REDIS_URL: redis://redis:6379/0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: redis-integration-tests
- commands:
- dockerize -wait tcp://memcached:11211 -timeout 120s
@@ -994,7 +994,7 @@ steps:
- wait-for-memcached
environment:
MEMCACHED_HOSTS: memcached:11211
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: memcached-integration-tests
- commands:
- dockerize -wait tcp://mimir:8080 -timeout 120s
@@ -1007,7 +1007,7 @@ steps:
depends_on:
- wire-install
- wait-for-remote-alertmanager
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: remote-alertmanager-integration-tests
trigger:
event:
@@ -1093,7 +1093,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
trigger:
event:
@@ -1133,7 +1133,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- apt-get update -yq && apt-get install shellcheck
@@ -1240,7 +1240,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -1251,7 +1251,7 @@ steps:
- CODEGEN_VERIFY=1 make gen-cue
depends_on:
- clone-enterprise
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -1261,14 +1261,14 @@ steps:
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on:
- clone-enterprise
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- apk add --update build-base
@@ -1276,7 +1276,7 @@ steps:
- go test -v -run=^$ -benchmem -timeout=1h -count=8 -bench=. ${GO_PACKAGES}
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: sqlite-benchmark-integration-tests
- commands:
- apk add --update build-base
@@ -1288,7 +1288,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: postgres-benchmark-integration-tests
- commands:
- apk add --update build-base
@@ -1299,7 +1299,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-5.7-benchmark-integration-tests
- commands:
- apk add --update build-base
@@ -1310,7 +1310,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-8.0-benchmark-integration-tests
trigger:
event:
@@ -1386,7 +1386,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
trigger:
branch: main
@@ -1558,7 +1558,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -1567,21 +1567,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -1590,7 +1590,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend-integration
trigger:
branch: main
@@ -1634,13 +1634,13 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- apk add --update make
- make gen-go
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- apk add --update make build-base
@@ -1649,16 +1649,16 @@ steps:
- wire-install
environment:
CGO_ENABLED: "1"
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: lint-backend
- commands:
- go run scripts/modowners/modowners.go check go.mod
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: validate-modfile
- commands:
- apk add --update make
- make swagger-validate
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: validate-openapi-spec
- commands:
- ./bin/build verify-drone
@@ -1714,7 +1714,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
@@ -1724,7 +1724,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -1733,14 +1733,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- yarn install --immutable
@@ -1771,7 +1771,7 @@ steps:
image: node:18.12.0-alpine
name: build-frontend-packages
- commands:
- /src/grafana-build package --distro=linux/amd64,linux/arm64 --go-version=1.20.8
- /src/grafana-build package --distro=linux/amd64,linux/arm64 --go-version=1.20.10
--yarn-cache=$$YARN_CACHE_FOLDER --build-id=$$DRONE_BUILD_NUMBER --grafana-dir=$$PWD
> packages.txt
depends_on:
@@ -2153,7 +2153,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- echo $DRONE_RUNNER_NAME
@@ -2167,7 +2167,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -2176,14 +2176,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- dockerize -wait tcp://postgres:5432 -timeout 120s
@@ -2204,7 +2204,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql57:3306 -timeout 120s
@@ -2225,7 +2225,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-5.7-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
@@ -2246,7 +2246,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-8.0-integration-tests
- commands:
- dockerize -wait tcp://redis:6379 -timeout 120s
@@ -2261,7 +2261,7 @@ steps:
- wait-for-redis
environment:
REDIS_URL: redis://redis:6379/0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: redis-integration-tests
- commands:
- dockerize -wait tcp://memcached:11211 -timeout 120s
@@ -2276,7 +2276,7 @@ steps:
- wait-for-memcached
environment:
MEMCACHED_HOSTS: memcached:11211
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: memcached-integration-tests
- commands:
- dockerize -wait tcp://mimir:8080 -timeout 120s
@@ -2289,7 +2289,7 @@ steps:
depends_on:
- wire-install
- wait-for-remote-alertmanager
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: remote-alertmanager-integration-tests
trigger:
branch: main
@@ -2511,7 +2511,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- ./bin/build artifacts docker fetch --edition oss
@@ -2607,7 +2607,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- ./bin/build artifacts packages --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET}
@@ -2676,7 +2676,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- yarn install --immutable
@@ -2705,7 +2705,7 @@ steps:
NPM_TOKEN:
from_secret: npm_token
failure: ignore
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: release-npm-packages
trigger:
event:
@@ -2741,7 +2741,7 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- depends_on:
- compile-build-cmd
@@ -2846,7 +2846,7 @@ steps:
from_secret: gcp_key_base64
GITHUB_TOKEN:
from_secret: github_token
GO_VERSION: 1.20.8
GO_VERSION: 1.20.10
GPG_PASSPHRASE:
from_secret: packages_gpg_passphrase
GPG_PRIVATE_KEY:
@@ -2902,13 +2902,13 @@ steps:
depends_on: []
environment:
CGO_ENABLED: 0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: compile-build-cmd
- commands:
- ./bin/build whatsnew-checker
depends_on:
- compile-build-cmd
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: whats-new-checker
trigger:
event:
@@ -3008,7 +3008,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -3017,21 +3017,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -3040,7 +3040,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend-integration
trigger:
event:
@@ -3095,7 +3095,7 @@ steps:
from_secret: gcp_key_base64
GITHUB_TOKEN:
from_secret: github_token
GO_VERSION: 1.20.8
GO_VERSION: 1.20.10
GPG_PASSPHRASE:
from_secret: packages_gpg_passphrase
GPG_PRIVATE_KEY:
@@ -3276,7 +3276,7 @@ steps:
from_secret: gcp_key_base64
GITHUB_TOKEN:
from_secret: github_token
GO_VERSION: 1.20.8
GO_VERSION: 1.20.10
GPG_PASSPHRASE:
from_secret: packages_gpg_passphrase
GPG_PRIVATE_KEY:
@@ -3421,7 +3421,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -3430,21 +3430,21 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- apk add --update build-base shared-mime-info shared-mime-info-lang
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend
- commands:
- apk add --update build-base
@@ -3453,7 +3453,7 @@ steps:
| grep -o '\(.*\)/' | sort -u)
depends_on:
- wire-install
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: test-backend-integration
trigger:
cron:
@@ -3506,7 +3506,7 @@ steps:
from_secret: gcp_key_base64
GITHUB_TOKEN:
from_secret: github_token
GO_VERSION: 1.20.8
GO_VERSION: 1.20.10
GPG_PASSPHRASE:
from_secret: packages_gpg_passphrase
GPG_PRIVATE_KEY:
@@ -3650,7 +3650,7 @@ steps:
from_secret: gcp_key_base64
GITHUB_TOKEN:
from_secret: github_token
GO_VERSION: 1.20.8
GO_VERSION: 1.20.10
GPG_PASSPHRASE:
from_secret: packages_gpg_passphrase
GPG_PRIVATE_KEY:
@@ -3746,20 +3746,20 @@ steps:
- commands: []
depends_on:
- clone
image: golang:1.20.8-windowsservercore-1809
image: golang:1.20.10-windowsservercore-1809
name: windows-init
- commands:
- go install github.com/google/wire/cmd/wire@v0.5.0
- wire gen -tags oss ./pkg/server
depends_on:
- windows-init
image: golang:1.20.8-windowsservercore-1809
image: golang:1.20.10-windowsservercore-1809
name: wire-install
- commands:
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
depends_on:
- wire-install
image: golang:1.20.8-windowsservercore-1809
image: golang:1.20.10-windowsservercore-1809
name: test-backend
trigger:
event:
@@ -3851,7 +3851,7 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-cue
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-cue
- commands:
- '# It is required that generated jsonnet is committed and in sync with its inputs.'
@@ -3860,14 +3860,14 @@ steps:
- apk add --update make
- CODEGEN_VERIFY=1 make gen-jsonnet
depends_on: []
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: verify-gen-jsonnet
- commands:
- apk add --update make
- make gen-go
depends_on:
- verify-gen-cue
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: wire-install
- commands:
- dockerize -wait tcp://postgres:5432 -timeout 120s
@@ -3888,7 +3888,7 @@ steps:
GRAFANA_TEST_DB: postgres
PGPASSWORD: grafanatest
POSTGRES_HOST: postgres
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: postgres-integration-tests
- commands:
- dockerize -wait tcp://mysql57:3306 -timeout 120s
@@ -3909,7 +3909,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql57
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-5.7-integration-tests
- commands:
- dockerize -wait tcp://mysql80:3306 -timeout 120s
@@ -3930,7 +3930,7 @@ steps:
environment:
GRAFANA_TEST_DB: mysql
MYSQL_HOST: mysql80
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: mysql-8.0-integration-tests
- commands:
- dockerize -wait tcp://redis:6379 -timeout 120s
@@ -3945,7 +3945,7 @@ steps:
- wait-for-redis
environment:
REDIS_URL: redis://redis:6379/0
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: redis-integration-tests
- commands:
- dockerize -wait tcp://memcached:11211 -timeout 120s
@@ -3960,7 +3960,7 @@ steps:
- wait-for-memcached
environment:
MEMCACHED_HOSTS: memcached:11211
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: memcached-integration-tests
- commands:
- dockerize -wait tcp://mimir:8080 -timeout 120s
@@ -3973,7 +3973,7 @@ steps:
depends_on:
- wire-install
- wait-for-remote-alertmanager
image: golang:1.20.8-alpine
image: golang:1.20.10-alpine
name: remote-alertmanager-integration-tests
trigger:
event:
@@ -4379,7 +4379,7 @@ steps:
path: /root/.docker/
- commands:
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM alpine/git:2.40.1
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.20.8-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM golang:1.20.10-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM node:18.12.0-alpine
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM google/cloud-sdk:431.0.0
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/grafana-ci-deploy:1.3.3
@@ -4413,7 +4413,7 @@ steps:
path: /root/.docker/
- commands:
- trivy --exit-code 1 --severity HIGH,CRITICAL alpine/git:2.40.1
- trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.20.8-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL golang:1.20.10-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL node:18.12.0-alpine
- trivy --exit-code 1 --severity HIGH,CRITICAL google/cloud-sdk:431.0.0
- trivy --exit-code 1 --severity HIGH,CRITICAL grafana/grafana-ci-deploy:1.3.3
@@ -4661,6 +4661,6 @@ kind: secret
name: gcr_credentials
---
kind: signature
hmac: 68c3d5cfb08c1e5384f39ea57badfd6dbb46943a87d54571665ec29a1257b707
hmac: efc8706531a40b72753d483d3d8a7ba4941b5f54e0b21a68bcf47556a97a9ede
...
+2 -1
View File
@@ -114,7 +114,7 @@
/pkg/services/navtree/ @grafana/backend-platform
/pkg/services/notifications/ @grafana/backend-platform
/pkg/services/org/ @grafana/backend-platform
/pkg/services/playlist/ @grafana/backend-platform
/pkg/services/playlist/ @grafana/grafana-app-platform-squad
/pkg/services/plugindashboards/ @grafana/backend-platform
/pkg/services/preference/ @grafana/backend-platform
/pkg/services/provisioning/ @grafana/backend-platform
@@ -323,6 +323,7 @@
/packages/grafana-ui/src/components/VizRepeater/ @grafana/dataviz-squad
/packages/grafana-ui/src/components/VizTooltip/ @grafana/dataviz-squad
/packages/grafana-ui/src/utils/storybook/ @grafana/plugins-platform-frontend
/packages/grafana-data/src/transformations/ @grafana/grafana-bi-squad
/packages/grafana-data/src/**/*logs* @grafana/observability-logs
/packages/grafana-schema/src/**/*tempo* @grafana/observability-traces-and-profiling
/packages/grafana-flamegraph/ @grafana/observability-traces-and-profiling
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
- name: Set go version
uses: actions/setup-go@v4
with:
go-version: '1.20.8'
go-version: '1.20.10'
- name: Build swagger
run: |
make -C pkg/services/ngalert/api/tooling post.json api.json
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
name: Set go version
uses: actions/setup-go@v4
with:
go-version: '1.20.8'
go-version: '1.20.10'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+1
View File
@@ -24,3 +24,4 @@ jobs:
token: ${{ steps.generate_token.outputs.token }}
version: ${{ inputs.version }}
metrics_api_key: ${{ secrets.GRAFANA_MISC_STATS_API_KEY }}
latest: ${{ inputs.latest }}
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- name: Set go version
uses: actions/setup-go@v4
with:
go-version: '1.20.8'
go-version: '1.20.10'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: "Setup Go"
uses: "actions/setup-go@v4"
with:
go-version: '1.20.8'
go-version: '1.20.10'
- name: "Verify kinds"
run: go run .github/workflows/scripts/kinds/verify-kinds.go
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
- name: "Setup Go"
uses: "actions/setup-go@v4"
with:
go-version: '1.20.8'
go-version: '1.20.10'
- name: "Verify kinds"
run: go run .github/workflows/scripts/kinds/verify-kinds.go
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
- name: "Setup Go"
uses: "actions/setup-go@v4"
with:
go-version: '1.20.8'
go-version: '1.20.10'
- name: "Verify kinds"
run: go run .github/workflows/scripts/kinds/verify-kinds.go
+72
View File
@@ -1,3 +1,32 @@
<!-- 10.1.5 START -->
# 10.1.5 (2023-10-11)
### Features and enhancements
- **Chore:** Upgrade Go to 1.20.10. [#76355](https://github.com/grafana/grafana/issues/76355), [@zerok](https://github.com/zerok)
- **Cloudwatch:** Backport 73524 Bring Back Legacy Log Group Picker. [#75031](https://github.com/grafana/grafana/issues/75031), [@sarahzinger](https://github.com/sarahzinger)
### Bug fixes
- **Cloudwatch:** Prevent log group requests with ARNs if feature flag is off. [#75691](https://github.com/grafana/grafana/issues/75691), [@sarahzinger](https://github.com/sarahzinger)
- **Alerting:** Add support for `keep_firing_for` field from external rulers. [#75257](https://github.com/grafana/grafana/issues/75257), [@rwwiv](https://github.com/rwwiv)
- **Canvas:** Avoid conflicting stylesheets when loading SVG icons. [#75032](https://github.com/grafana/grafana/issues/75032), [@adela-almasan](https://github.com/adela-almasan)
- **Alerting:** Prevent showing "Permissions denied" alert when not accurate. [#74925](https://github.com/grafana/grafana/issues/74925), [@VikaCep](https://github.com/VikaCep)
- **BrowseDashboards:** Only remember the most recent expanded folder. [#74809](https://github.com/grafana/grafana/issues/74809), [@joshhunt](https://github.com/joshhunt)
- **Tempo Service Map:** Fix context menu links in service map when namespace is present. [#74796](https://github.com/grafana/grafana/issues/74796), [@javiruiz01](https://github.com/javiruiz01)
- **Logs Panel:** Performance issue while scrolling within panel in safari. [#74747](https://github.com/grafana/grafana/issues/74747), [@gtk-grafana](https://github.com/gtk-grafana)
- **Bug:** Allow to uninstall a deprecated plugin. [#74704](https://github.com/grafana/grafana/issues/74704), [@andresmgot](https://github.com/andresmgot)
- **Licensing:** Pass func to update env variables when starting plugin. [#74678](https://github.com/grafana/grafana/issues/74678), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **Nested folders:** Fix folder hierarchy in folder responses. [#74580](https://github.com/grafana/grafana/issues/74580), [@papagian](https://github.com/papagian)
- **Share link:** Use panel relative time for direct link rendered image. [#74518](https://github.com/grafana/grafana/issues/74518), [@Clarity-89](https://github.com/Clarity-89)
- **Alerting:** Do not exit if Redis ping fails when using redis-based Alertmanager clustering. [#74399](https://github.com/grafana/grafana/issues/74399), [@alexweav](https://github.com/alexweav)
- **Alerting:** Refactor AlertRuleForm and fix annotations step description for cloud rules. [#74193](https://github.com/grafana/grafana/issues/74193), [@soniaAguilarPeiron](https://github.com/soniaAguilarPeiron)
- **RBAC:** Chore fix hasPermissionInOrg. (Enterprise)
- **Licensing:** Updated grpc plugin factory newPlugin signature. (Enterprise)
- **Reporting:** Add support for old dashboard schema. (Enterprise)
<!-- 10.1.5 END -->
<!-- 10.1.4 START -->
# 10.1.4 (2023-09-29)
@@ -446,6 +475,19 @@ Starting with 10.0, changing the folder UID is deprecated. It will be removed in
- **Grafana/ui:** Fix margin in RadioButtonGroup option when only icon is present. [#68899](https://github.com/grafana/grafana/issues/68899), [@aocenas](https://github.com/aocenas)
<!-- 10.1.0 END -->
<!-- 10.0.9 START -->
# 10.0.9 (2023-10-11)
### Bug fixes
- **BrowseDashboards:** Only remember the most recent expanded folder. [#74819](https://github.com/grafana/grafana/issues/74819), [@joshhunt](https://github.com/joshhunt)
- **Licensing:** Pass func to update env variables when starting plugin. [#74679](https://github.com/grafana/grafana/issues/74679), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **Nested folders:** Fix folder hierarchy in folder responses. [#74581](https://github.com/grafana/grafana/issues/74581), [@papagian](https://github.com/papagian)
- **RBAC:** Chore fix hasPermissionInOrg. (Enterprise)
- **Licensing:** Updated grpc plugin factory newPlugin signature. (Enterprise)
<!-- 10.0.9 END -->
<!-- 10.0.8 START -->
# 10.0.8 (2023-09-29)
@@ -947,6 +989,21 @@ The `database` field has been deprecated in the Elasticsearch datasource provisi
- **InteractiveTable:** Updated design and minor tweak to Correlactions page. [#66443](https://github.com/grafana/grafana/issues/66443), [@torkelo](https://github.com/torkelo)
<!-- 10.0.0-preview END -->
<!-- 9.5.13 START -->
# 9.5.13 (2023-10-11)
### Features and enhancements
- **Licensing:** Updated grpc plugin factory newPlugin signature. (Enterprise)
### Bug fixes
- **BrowseDashboards:** Only remember the most recent expanded folder. [#74817](https://github.com/grafana/grafana/issues/74817), [@joshhunt](https://github.com/joshhunt)
- **Licensing:** Pass func to update env variables when starting plugin. [#74681](https://github.com/grafana/grafana/issues/74681), [@leandro-deveikis](https://github.com/leandro-deveikis)
- **RBAC:** Chore fix hasPermissionInOrg. (Enterprise)
<!-- 9.5.13 END -->
<!-- 9.5.12 START -->
# 9.5.12 (2023-09-29)
@@ -1357,6 +1414,21 @@ Users who use LDAP role sync to only sync Viewer, Editor and Admin roles, but gr
- **ContextMenu:** Fix padding and show border based on items. [#63948](https://github.com/grafana/grafana/pull/63948), [@aocenas](https://github.com/aocenas)
<!-- 9.5.0 END -->
<!-- 9.4.17 START -->
# 9.4.17 (2023-10-11)
### Features and enhancements
- **SSE:** DSNode to update result with names to make each value identifiable by labels (only Graphite and TestData). [#74615](https://github.com/grafana/grafana/issues/74615), [@yuri-tceretian](https://github.com/yuri-tceretian)
### Bug fixes
- **BrowseDashboards:** Only remember the most recent expanded folder. [#74812](https://github.com/grafana/grafana/issues/74812), [@joshhunt](https://github.com/joshhunt)
- **SQL Datasources:** Fix variable throwing error if query returns no data. [#74609](https://github.com/grafana/grafana/issues/74609), [@mdvictor](https://github.com/mdvictor)
- **RBAC:** Chore fix hasPermissionInOrg. (Enterprise)
<!-- 9.4.17 END -->
<!-- 9.4.15 START -->
# 9.4.15 (2023-09-18)
+1 -1
View File
@@ -3,7 +3,7 @@
ARG BASE_IMAGE=alpine:3.18.3
ARG JS_IMAGE=node:18-alpine3.18
ARG JS_PLATFORM=linux/amd64
ARG GO_IMAGE=golang:1.20.8-alpine3.18
ARG GO_IMAGE=golang:1.20.10-alpine3.18
ARG GO_SRC=go-builder
ARG JS_SRC=js-builder
+1 -1
View File
@@ -238,7 +238,7 @@ build-docker-full-ubuntu: ## Build Docker image based on Ubuntu for development.
--build-arg COMMIT_SHA=$$(git rev-parse HEAD) \
--build-arg BUILD_BRANCH=$$(git rev-parse --abbrev-ref HEAD) \
--build-arg BASE_IMAGE=ubuntu:22.04 \
--build-arg GO_IMAGE=golang:1.20.8 \
--build-arg GO_IMAGE=golang:1.20.10 \
--tag grafana/grafana$(TAG_SUFFIX):dev-ubuntu \
$(DOCKER_BUILD_ARGS)
+10 -3
View File
@@ -644,7 +644,11 @@ token_url = https://oauth2.googleapis.com/token
api_url = https://openidconnect.googleapis.com/v1/userinfo
allowed_domains =
hosted_domain =
skip_org_role_sync = false
allowed_groups =
role_attribute_path =
role_attribute_strict = false
allow_assign_grafana_admin = false
skip_org_role_sync = true
tls_skip_verify_insecure = false
tls_client_cert =
tls_client_key =
@@ -828,7 +832,7 @@ assume_role_enabled = true
list_metrics_page_limit = 500
# Experimental, for use in Grafana Cloud only. Please do not set.
external_id =
external_id =
#################################### Azure ###############################
[azure]
@@ -1226,7 +1230,7 @@ url =
# Tenant ID to use in requests to the Alertmanager.
# It will also be used for the basic auth username.
tenant =
tenant =
# Optional password for basic authentication.
# If not present, the tenant ID will be set in the X-Scope-OrgID header.
@@ -1521,6 +1525,9 @@ ha_engine =
# This option is EXPERIMENTAL.
ha_engine_address = "127.0.0.1:6379"
# ha_engine_password allows setting an optional password to authenticate with the engine
ha_engine_password = ""
#################################### Grafana Image Renderer Plugin ##########################
[plugin.grafana-image-renderer]
# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert.
+8 -1
View File
@@ -626,6 +626,10 @@
;api_url = https://openidconnect.googleapis.com/v1/userinfo
;allowed_domains =
;hosted_domain =
;allowed_groups =
;role_attribute_path =
;role_attribute_strict = false
;allow_assign_grafana_admin = false
;skip_org_role_sync = false
;use_pkce = true
@@ -781,7 +785,7 @@
; list_metrics_page_limit = 500
# Experimental, for use in Grafana Cloud only. Please do not set.
; external_id =
; external_id =
#################################### Azure ###############################
[azure]
@@ -1414,6 +1418,9 @@
# This option is EXPERIMENTAL.
;ha_engine_address = "127.0.0.1:6379"
# ha_engine_password allows setting an optional password to authenticate with the engine
;ha_engine_password = ""
#################################### Grafana Image Renderer Plugin ##########################
[plugin.grafana-image-renderer]
# Instruct headless browser instance to use a default timezone when not provided by Grafana, e.g. when rendering panel image of alert.
+2 -2
View File
@@ -183,8 +183,8 @@ Once extracted with `yarn i18n:extract` you will need to manually edit the [Engl
```json
{
"inbox": {
"heading__one": "You got {{count}} message",
"heading__other": "You got {{count}} messages"
"heading_one": "You got {{count}} message",
"heading_other": "You got {{count}} messages"
}
}
```
@@ -1121,6 +1121,93 @@
"title": "Hue",
"type": "barchart"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"fillOpacity": 100,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0,
"scaleDistribution": {
"type": "linear"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 5,
"x": 10,
"y": 18
},
"id": 15,
"options": {
"barRadius": 0,
"barWidth": 0.97,
"groupWidth": 0.7,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"orientation": "auto",
"showValue": "auto",
"stacking": "none",
"tooltip": {
"mode": "single",
"sort": "none"
},
"xTickLabelRotation": 0,
"xTickLabelSpacing": 0
},
"targets": [
{
"csvContent": "label,value\nx,3\na,10\nb,20\nc,30",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Green-Yellow-Red",
"type": "barchart"
},
{
"datasource": {
"type": "testdata",
@@ -1239,7 +1326,7 @@
"type": "barchart"
}
],
"schemaVersion": 37,
"schemaVersion": 38,
"tags": [
"gdev",
"panel-tests",
@@ -1257,6 +1344,6 @@
"timezone": "",
"title": "BarChart - Thresholds & Mappings",
"uid": "2I2uMSB7z",
"version": 32,
"version": 7,
"weekStart": ""
}
}
@@ -53,6 +53,7 @@ Grafana includes the following basic roles:
- Organization administrator
- Editor
- Viewer
- None
Each basic role is comprised of a number of _permissions_. For example, the viewer basic role contains the following permissions among others:
@@ -64,7 +65,7 @@ Each basic role is comprised of a number of _permissions_. For example, the view
- `Action: annotations:delete, Scope: annotations:type:dashboard`: Enables the viewer to remove annotations from a dashboard.
{{% admonition type="note" %}}
You can't have a Grafana user without a basic role assigned.
You can't have a Grafana user without a basic role assigned. The `None` role contains no permissions.
{{% /admonition %}}
#### Basic role modification
@@ -32,7 +32,7 @@ To see the permissions associated with basic roles, refer to the following basic
| Basic role | UID |
| --------------- | --------------------- |
| `No Basic Role` | `basic_none` |
| `None` | `basic_none` |
| `Viewer` | `basic_viewer` |
| `Editor` | `basic_editor` |
| `Admin` | `basic_admin` |
@@ -117,6 +117,11 @@ If you have a use case that you'd like to share, feel free to contribute to this
1. Add dashboards to each folder.
1. Use folder permissions to add US-based users as Editors to the `US` folder and assign EU-based users as Editors to the `EU` folder.
### Assign a user specific set of roles
1. Create a user with the `No Basic Role` selected under organization roles.
1. Assign the user a set of fixed roles that meet your requirements.
### Create a custom role to access alerts in a specific folder
To see an alert rule in Grafana, the user must have read access to the folder that stores the alert rule, permission to read alerts in the folder, and permission to query all data sources that the rule uses.
@@ -74,7 +74,7 @@ If you have [server administrator]({{< relref "../../roles-and-permissions/#graf
## Invite a user to join an organization
When you invite users to join an organization, you assign the **Admin**, **Editor**, or **Viewer** role which controls user access to the dashboards and data sources owned by the organization. Users receive an email that prompts them to accept the invitation.
When you invite users to join an organization, you assign the **Admin**, **Editor**, or **Viewer** role, or select **No basic role**. Organization roles control user access to resources, such as dashboards and data sources, owned by the organization. Users receive an email that prompts them to accept the invitation.
- If you know that the user already has access Grafana and you know their user name, then you issue an invitation by entering their user name.
- If the user is new to Grafana, then use their email address to issue an invitation. The system automatically creates the user account on first sign in.
@@ -41,7 +41,7 @@ Learn how to write the content of your notification templates in Go’s templati
Create reusable notification templates for your contact points.
[Use notification templates][use-notication-templates]
[Use notification templates][use-notification-templates]
Use notification templates to send notifications to your contact points.
@@ -135,12 +135,12 @@ Interpolation result: 'test1.|test2'
### Raw
Turns off data source-specific formatting, such as single quotes in an SQL query.
The raw format for a data source variable returns the UID (unique identifier) of the data source, rather than its name.
```bash
servers = ['test.1', 'test2']
String to interpolate: '${var_name:raw}'
Interpolation result: 'test.1,test2'
datasourceVariable = 'd7bbe725-9e48-4af8-a0cb-6cb255d873a3'
String to interpolate: '${datasourceVariable:raw}'
Interpolation result: 'd7bbe725-9e48-4af8-a0cb-6cb255d873a3'
```
### Regex
+1 -1
View File
@@ -120,7 +120,7 @@ datasources:
password: ${GRAFANA_MYSQL_PASSWORD}
```
##### Using TLS Verificaiton
##### Using TLS verification
```yaml
apiVersion: 1
@@ -113,7 +113,7 @@ Status Codes:
## Remove External Group
`DELETE /api/teams/:teamId/groups/:groupId`
`DELETE /api/teams/:teamId/groups?groupId=external-group-id`
**Required permissions**
@@ -126,7 +126,7 @@ See note in the [introduction]({{< ref "#external-group-synchronization-api" >}}
**Example Request**:
```http
DELETE /api/teams/1/groups/cn=editors,ou=groups,dc=grafana,dc=org HTTP/1.1
DELETE /api/teams/1/groups?groupId=cn%3Deditors%2Cou%3Dgroups%2Cdc%3Dgrafana%2Cdc%3Dorg HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Basic YWRtaW46YWRtaW4=
@@ -71,13 +71,13 @@ extraFields is reserved for any fields that are pulled from the API server metad
| Property | Type | Required | Default | Description |
|------------------------|---------------------------------------------|----------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `editable` | boolean | **Yes** | `true` | Whether a dashboard is editable or not. |
| `graphTooltip` | integer | **Yes** | `0` | 0 for no shared crosshair or tooltip (default).<br/>1 for shared crosshair.<br/>2 for shared crosshair AND shared tooltip.<br/>Possible values are: `0`, `1`, `2`. |
| `schemaVersion` | uint16 | **Yes** | `36` | Version of the JSON schema, incremented each time a Grafana update brings<br/>changes to said schema. |
| `annotations` | [AnnotationContainer](#annotationcontainer) | No | | Contains the list of annotations that are associated with the dashboard.<br/>Annotations are used to overlay event markers and overlay event tags on graphs.<br/>Grafana comes with a native annotation store and the ability to add annotation events directly from the graph panel or via the HTTP API.<br/>See https://grafana.com/docs/grafana/latest/dashboards/build-dashboards/annotate-visualizations/ |
| `description` | string | No | | Description of dashboard. |
| `editable` | boolean | No | `true` | Whether a dashboard is editable or not. |
| `fiscalYearStartMonth` | integer | No | `0` | The month that the fiscal year starts on. 0 = January, 11 = December<br/>Constraint: `>=0 & <12`. |
| `gnetId` | string | No | | ID of a dashboard imported from the https://grafana.com/grafana/dashboards/ portal |
| `graphTooltip` | integer | No | `0` | 0 for no shared crosshair or tooltip (default).<br/>1 for shared crosshair.<br/>2 for shared crosshair AND shared tooltip.<br/>Possible values are: `0`, `1`, `2`. |
| `id` | integer or null | No | | Unique numeric identifier for the dashboard.<br/>`id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances. |
| `links` | [DashboardLink](#dashboardlink)[] | No | | Links with references to other dashboards or external websites. |
| `liveNow` | boolean | No | | When set to true, the dashboard will redraw panels at an interval matching the pixel width.<br/>This will keep data "moving left" regardless of the query refresh rate. This setting helps<br/>avoid dashboards presenting stale live data |
@@ -191,9 +191,9 @@ Sensitive information stripped: queries (metric, template,annotation) and panel
### Panels
| Property | Type | Required | Default | Description |
|----------|-----------------------------------------------------------------------------------------------------------------------|----------|---------|-------------|
| `object` | Possible types are: [Panel](#panel), [RowPanel](#rowpanel), [GraphPanel](#graphpanel), [HeatmapPanel](#heatmappanel). | | |
| Property | Type | Required | Default | Description |
|----------|-------------------------------------------------------------------------------------------------------------|----------|---------|-------------|
| `object` | Possible types are: [](#), [RowPanel](#rowpanel), [GraphPanel](#graphpanel), [HeatmapPanel](#heatmappanel). | | |
### DataTransformerConfig
@@ -443,19 +443,37 @@ Library panels streamline reuse of panels across multiple dashboards.
| `name` | string | **Yes** | | Library panel name |
| `uid` | string | **Yes** | | Library panel uid |
### RowPanel
Row panel
| Property | Type | Required | Default | Description |
|--------------|---------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|
| `collapsed` | boolean | **Yes** | `false` | Whether this row should be collapsed or not. |
| `id` | uint32 | **Yes** | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
| `panels` | [panels](#panels)[] | **Yes** | | List of panels in the row |
| `type` | string | **Yes** | | The panel type<br/>Possible values are: `row`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `repeat` | string | No | | Name of template variable to repeat for. |
| `title` | string | No | | Row title |
### Panels
| Property | Type | Required | Default | Description |
|----------|------------------------------------------------------------------------------------------------|----------|---------|-------------|
| `object` | Possible types are: [Panel](#panel), [GraphPanel](#graphpanel), [HeatmapPanel](#heatmappanel). | | |
### Panel
Dashboard panels are the basic visualization building blocks.
| Property | Type | Required | Default | Description |
|--------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `options` | [object](#options) | **Yes** | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | **Yes** | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | **Yes** | `false` | Whether to display the panel without a background. |
| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.<br/>Constraint: `length >=1`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `description` | string | No | | Panel description. |
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | No | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `hideTimeOverride` | boolean | No | | Controls if the timeFrom or timeShift overrides are shown in the panel header |
| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
@@ -464,6 +482,7 @@ Dashboard panels are the basic visualization building blocks.
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
| `maxPerRow` | number | No | | Option for repeated panels that controls max items per row<br/>Only relevant for horizontally repeated panels |
| `options` | [object](#options) | No | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
| `repeat` | string | No | | Name of template variable to repeat for. |
@@ -472,6 +491,8 @@ Dashboard panels are the basic visualization building blocks.
| `timeFrom` | string | No | | Overrides the relative time range for individual panels,<br/>which causes them to be different than what is selected in<br/>the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different<br/>time periods or days on the same dashboard.<br/>The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),<br/>`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.<br/>For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `title` | string | No | | Panel title. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | No | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | No | `false` | Whether to display the panel without a background. |
### FieldConfigSource
@@ -560,27 +581,6 @@ It depends on the panel plugin. They are specified by the Options field in panel
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
### RowPanel
Row panel
| Property | Type | Required | Default | Description |
|--------------|---------------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|
| `collapsed` | boolean | **Yes** | `false` | Whether this row should be collapsed or not. |
| `id` | uint32 | **Yes** | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
| `panels` | [panels](#panels)[] | **Yes** | | List of panels in the row |
| `type` | string | **Yes** | | The panel type<br/>Possible values are: `row`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `repeat` | string | No | | Name of template variable to repeat for. |
| `title` | string | No | | Row title |
### Panels
| Property | Type | Required | Default | Description |
|----------|------------------------------------------------------------------------------------------------|----------|---------|-------------|
| `object` | Possible types are: [Panel](#panel), [GraphPanel](#graphpanel), [HeatmapPanel](#heatmappanel). | | |
### GraphPanel
Support for legacy graph panel.
@@ -597,13 +597,10 @@ Dashboard panels are the basic visualization building blocks.
| Property | Type | Required | Default | Description |
|--------------------|---------------------------------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | **Yes** | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `options` | [options](#options) | **Yes** | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | **Yes** | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | **Yes** | `false` | Whether to display the panel without a background. |
| `type` | string | **Yes** | | The panel plugin type id. This is used to find the plugin to display the panel.<br/>Constraint: `length >=1`. |
| `datasource` | [DataSourceRef](#datasourceref) | No | | Ref to a DataSource instance |
| `description` | string | No | | Panel description. |
| `fieldConfig` | [FieldConfigSource](#fieldconfigsource) | No | | The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.<br/>Each column within this structure is called a field. A field can represent a single time series or table column.<br/>Field options allow you to change how the data is displayed in your visualizations. |
| `gridPos` | [GridPos](#gridpos) | No | | Position and dimensions of a panel in the grid |
| `hideTimeOverride` | boolean | No | | Controls if the timeFrom or timeShift overrides are shown in the panel header |
| `id` | uint32 | No | | Unique identifier of the panel. Generated by Grafana when creating a new panel. It must be unique within a dashboard, but not globally. |
@@ -612,6 +609,7 @@ Dashboard panels are the basic visualization building blocks.
| `links` | [DashboardLink](#dashboardlink)[] | No | | Panel links. |
| `maxDataPoints` | number | No | | The maximum number of data points that the panel queries are retrieving. |
| `maxPerRow` | number | No | | Option for repeated panels that controls max items per row<br/>Only relevant for horizontally repeated panels |
| `options` | [options](#options) | No | | It depends on the panel plugin. They are specified by the Options field in panel plugin schemas. |
| `pluginVersion` | string | No | | The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs. |
| `repeatDirection` | string | No | `h` | Direction to repeat in if 'repeat' is set.<br/>`h` for horizontal, `v` for vertical.<br/>Possible values are: `h`, `v`. |
| `repeat` | string | No | | Name of template variable to repeat for. |
@@ -620,6 +618,8 @@ Dashboard panels are the basic visualization building blocks.
| `timeFrom` | string | No | | Overrides the relative time range for individual panels,<br/>which causes them to be different than what is selected in<br/>the dashboard time picker in the top-right corner of the dashboard. You can use this to show metrics from different<br/>time periods or days on the same dashboard.<br/>The value is formatted as time operation like: `now-5m` (Last 5 minutes), `now/d` (the day so far),<br/>`now-5d/d`(Last 5 days), `now/w` (This week so far), `now-2y/y` (Last 2 years).<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `timeShift` | string | No | | Overrides the time range for individual panels by shifting its start and end relative to the time picker.<br/>For example, you can shift the time range for the panel to be two hours earlier than the dashboard time picker setting `2h`.<br/>Note: Panel time overrides have no effect when the dashboard’s time range is absolute.<br/>See: https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/#query-options |
| `title` | string | No | | Panel title. |
| `transformations` | [DataTransformerConfig](#datatransformerconfig)[] | No | | List of transformations that are applied to the panel data before rendering.<br/>When there are multiple transformations, Grafana applies them in the order they are listed.<br/>Each transformation creates a result set that then passes on to the next transformation in the processing pipeline. |
| `transparent` | boolean | No | `false` | Whether to display the panel without a background. |
### Templating
@@ -32,7 +32,7 @@ To sign in to Grafana for the first time:
The default HTTP port that Grafana listens to is `3000` unless you have configured a different port.
1. On the sign-in page, enter `admin` for the username and password.
1. On the sign-in page, enter `admin` for both the username and password.
1. Click **Sign in**.
If successful, you will see a prompt to change the password.
@@ -82,11 +82,15 @@ Grafana can sometimes be too aggressive in parsing strings and displaying them a
### Min
Lets you set the minimum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
Lets you set the minimum value used in percentage threshold calculations. Leave blank to automatically calculate the minimum.
### Max
Lets you set the maximum value used in percentage threshold calculations. Leave blank for auto calculation based on all series and fields.
Lets you set the maximum value used in percentage threshold calculations. Leave blank to automatically calculate the maximum.
### Field min/max
By default the calculated min and max will be based on the minimum and maximum, in all series and fields. Turning field min/max on, will calculate the min or max on each field individually, based on the minimum or maximum value of the field.
### Decimals
@@ -314,7 +314,7 @@ List of comma- or space-separated organizations. Each user must be a member of a
### org_mapping
List of comma- or space-separated Organization:OrgId:Role mappings. Organization can be `*` meaning "All users". Role is optional and can have the following values: `Viewer`, `Editor` or `Admin`.
List of comma- or space-separated Organization:OrgId:Role mappings. Organization can be `*` meaning "All users". Role is optional and can have the following values: `Admin`, `Editor` ,`Viewer` or `None`.
### role_values_editor
@@ -35,7 +35,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view | Yes |
| `gcomOnlyExternalOrgRoleSync` | Prohibits a user from changing organization roles synced with Grafana Cloud auth provider | |
| `prometheusMetricEncyclopedia` | Adds the metrics explorer component to the Prometheus query builder as an option in metric select | Yes |
| `prometheusResourceBrowserCache` | Displays browser caching options in Prometheus data source configuration | Yes |
| `prometheusDataplane` | Changes responses to from Prometheus to be compliant with the dataplane specification. In particular it sets the numeric Field.Name from 'Value' to the value of the `__name__` label when present. | Yes |
| `lokiMetricDataplane` | Changes metric responses from Loki to be compliant with the dataplane specification. | Yes |
| `dataplaneFrontendFallback` | Support dataplane contract field name change for transformations and field name matchers where the name is different | Yes |
@@ -50,6 +49,7 @@ Some features are enabled by default. You can disable these feature by setting t
| `azureMonitorDataplane` | Adds dataplane compliant frame metadata in the Azure Monitor datasource | Yes |
| `prometheusConfigOverhaulAuth` | Update the Prometheus configuration page with the new auth component | Yes |
| `newBrowseDashboards` | New browse/manage dashboards UI | Yes |
| `alertingInsights` | Show the new alerting insights landing page | Yes |
| `cloudWatchWildCardDimensionValues` | Fetches dimension values from CloudWatch to correctly label wildcard dimensions | Yes |
## Preview feature toggles
@@ -64,7 +64,6 @@ Some features are enabled by default. You can disable these feature by setting t
| `newDBLibrary` | Use jmoiron/sqlx rather than xorm for a few backend services |
| `autoMigrateOldPanels` | Migrate old angular panels to supported versions (graph, table-old, worldmap, etc) |
| `disableAngular` | Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime. |
| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu |
| `grpcServer` | Run the GRPC server |
| `accessControlOnCall` | Access control primitives for OnCall |
| `nestedFolders` | Enable folder nesting |
@@ -97,6 +96,7 @@ Experimental features might be changed or removed without prior notice.
| `scenes` | Experimental framework to build interactive dashboards |
| `disableSecretsCompatibility` | Disable duplicated secret storage in legacy tables |
| `logRequestsInstrumentedAsUnknown` | Logs the path for requests that are instrumented as unknown |
| `dockedMegaMenu` | Enable support for a persistent (docked) navigation menu |
| `showDashboardValidationWarnings` | Show warnings when dashboards do not validate against the schema |
| `mysqlAnsiQuotes` | Use double quotes to escape keyword in a MySQL query |
| `alertingBacktesting` | Rule backtesting API for alerting |
@@ -132,13 +132,12 @@ Experimental features might be changed or removed without prior notice.
| `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end |
| `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder |
| `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI |
| `noBasicRole` | Enables a new role that has no permissions by default |
| `angularDeprecationUI` | Display new Angular deprecation-related UI features |
| `dashgpt` | Enable AI powered features in dashboards |
| `sseGroupByDatasource` | Send query to the same datasource in a single request when using server side expressions |
| `requestInstrumentationStatusSource` | Include a status source label for request metrics and logs |
| `libraryPanelRBAC` | Enables RBAC support for library panels |
| `wargamesTesting` | Placeholder feature flag for internal testing |
| `alertingInsights` | Show the new alerting insights landing page |
| `externalCorePlugins` | Allow core plugins to be loaded as external |
| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins |
| `httpSLOLevels` | Adds SLO level to http request metrics |
@@ -148,6 +147,8 @@ Experimental features might be changed or removed without prior notice.
| `transformationsVariableSupport` | Allows using variables in transformations |
| `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists |
| `navAdminSubsections` | Splits the administration section of the nav tree into subsections |
| `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression |
| `awsDatasourcesNewFormStyling` | Applies new form styling for configuration and query editors in AWS plugins |
## Development feature toggles
@@ -205,7 +205,7 @@ This section includes examples of JMESPath expressions used for role mapping.
#### Map user organization role
In this example, the user has been granted the role of an `Editor`. The role assigned is based on the value of the property `role`, which must be a valid Grafana role such as `Viewer`, `Editor` or `Admin`.
In this example, the user has been granted the role of an `Editor`. The role assigned is based on the value of the property `role`, which must be a valid Grafana role such as `Admin`, `Editor`, `Viewer` or `None`.
Payload:
@@ -79,15 +79,15 @@ The table below describes all GitLab OAuth configuration options. Like any other
| `api_url` | No | Grafana uses `<api_url>/user` endpoint to obtain GitLab user information compatible with [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo). | `https://gitlab.com/api/v4` |
| `name` | No | Name used to refer to the GitLab authentication in the Grafana user interface. | `GitLab` |
| `icon` | No | Icon used for GitLab authentication in the Grafana user interface. | `gitlab` |
| `scopes` | No | List of comma- or space-separated GitLab OAuth scopes. | `openid email profile` |
| `scopes` | No | List of comma or space-separated GitLab OAuth scopes. | `openid email profile` |
| `allow_sign_up` | No | Whether to allow new Grafana user creation through GitLab login. If set to `false`, then only existing Grafana users can log in with GitLab OAuth. | `true` |
| `auto_login` | No | Set to `true` to enable users to bypass the login screen and automatically log in. This setting is ignored if you configure multiple auth providers to use auto-login. | `false` |
| `role_attribute_path` | No | [JMESPath](http://jmespath.org/examples.html) expression to use for Grafana role lookup. Grafana will first evaluate the expression using the GitLab OAuth token. If no role is found, Grafana creates a JSON data with `groups` key that maps to groups obtained from GitLab's `/oauth/userinfo` endpoint, and evaluates the expression using this data. Finally, if a valid role is still not found, the expression is evaluated against the user information retrieved from `api_url/users` endpoint and groups retrieved from `api_url/groups` endpoint. The result of the evaluation should be a valid Grafana role (`Viewer`, `Editor`, `Admin` or `GrafanaAdmin`). For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | |
| `role_attribute_strict` | No | Set to `true` to deny user login if the Grafana role cannot be extracted using `role_attribute_path`. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `allow_assign_grafana_admin` | No | Set to `true` to enable automatic sync of the Grafana server administrator role. If this option is set to `true` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user the server administrator privileges and organization administrator role. If this option is set to `false` and the result of evaluating `role_attribute_path` for a user is `GrafanaAdmin`, Grafana grants the user only organization administrator role. For more information on user role mapping, refer to [Configure role mapping]({{< relref "#configure-role-mapping" >}}). | `false` |
| `skip_org_role_sync` | No | Set to `true` to stop automatically syncing user roles. | `false` |
| `allowed_domains` | No | List of comma- or space-separated domains. User must belong to at least one domain to log in. | |
| `allowed_groups` | No | List of comma- or space-separated groups. The user should be a member of at least one group to log in. | |
| `allowed_domains` | No | List of comma or space-separated domains. User must belong to at least one domain to log in. | |
| `allowed_groups` | No | List of comma or space-separated groups. The user should be a member of at least one group to log in. | |
| `tls_skip_verify_insecure` | No | If set to `true`, the client accepts any certificate presented by the server and any host name in that certificate. _You should only use this for testing_, because this mode leaves SSL/TLS susceptible to man-in-the-middle attacks. | `false` |
| `tls_client_cert` | No | The path to the certificate. | |
| `tls_client_key` | No | The path to the key. | |
@@ -115,7 +115,7 @@ Refresh token fetching and access token expiration check is enabled by default f
To limit access to authenticated users that are members of one or more [GitLab
groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups`
to a comma- or space-separated list of groups.
to a comma or space-separated list of groups.
GitLab's groups are referenced by the group name. For example, `developers`. To reference a subgroup `frontend`, use `developers/frontend`.
Note that in GitLab, the group or subgroup name does not always match its display name, especially if the display name contains spaces or special characters.
@@ -100,16 +100,6 @@ This setting is ignored if multiple auth providers are configured to use auto lo
auto_login = true
```
## Skip organization role sync
We do not currently sync roles from Google and instead set the AutoAssigned role to the user at first login. The default setting for `skip_org_role_sync` is `true`, which means that role modifications can still be made through the user interface.
```ini
[auth.google]
# ..
skip_org_role_sync = true
```
### Configure team sync for Google OAuth
> Available in Grafana v10.1.0 and later versions.
@@ -132,3 +122,68 @@ With team sync, you can easily add users to teams by utilizing their Google grou
The external group ID for a Google group is the group's email address, such as `dev@grafana.com`.
To learn more about Team Sync, refer to [Configure Team Sync]({{< relref "../../configure-team-sync" >}}).
### Configure allowed groups
> Available in Grafana v10.2.0 and later versions.
To limit access to authenticated users that are members of one or more groups, set `allowed_groups`
to a comma or space separated list of groups.
Google groups are referenced by the group email key. For example, `developers@google.com`.
> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups
## Configure role mapping
> Available in Grafana v10.2.0 and later versions.
Unless `skip_org_role_sync` option is enabled, the user's role will be set to the role mapped from Google upon user login. If no mapping is set the default instance role is used.
The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.html) expression from the `role_attribute_path` configuration option.
To map the server administrator role, use the `allow_assign_grafana_admin` configuration option.
If no valid role is found, the user is assigned the role specified by [the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}).
You can disable this default role assignment by setting `role_attribute_strict = true`.
This setting denies user access if no role or an invalid role is returned.
To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads.
> By default skip_org_role_sync is enabled. skip_org_role_sync will default to false in Grafana v10.3.0 and later versions.
### Role mapping examples
This section includes examples of JMESPath expressions used for role mapping.
#### Map roles using user information from OAuth token
In this example, the user with email `admin@company.com` has been granted the `Admin` role.
All other users are granted the `Viewer` role.
```ini
role_attribute_path = email=='admin@company.com' && 'Admin' || 'Viewer'
skip_org_role_sync = false
```
#### Map roles using groups
In this example, the user from Google group 'example-group@google.com' have been granted the `Editor` role.
All other users are granted the `Viewer` role.
```ini
role_attribute_path = contains(groups[*], 'example-group@google.com') && 'Editor' || 'Viewer'
skip_org_role_sync = false
```
> Note: Add the `https://www.googleapis.com/auth/cloud-identity.groups.readonly` scope to your Grafana `[auth.google]` scopes configuration to retrieve groups
#### Map server administrator role
In this example, the user with email `admin@company.com` has been granted the `Admin` organization role as well as the Grafana server admin role.
All other users are granted the `Viewer` role.
```ini
allow_assign_grafana_admin = true
skip_org_role_sync = false
role_attribute_path = email=='admin@company.com' && 'GrafanaAdmin' || 'Viewer'
```
@@ -83,6 +83,17 @@ To learn more, refer to the following video demo, as well as the [Grafana Variab
{{< video-embed src="/media/docs/tempo/screen-recording-grafana-10.2-tempo-query-type-template-variables.mp4" >}}
## Improved TraceQL query editor
<!-- Fabrizio Casati -->
<!-- OSS, Enterprise -->
_Generally available in Grafana Cloud_
The [TraceQL query editor](https://grafana.com/docs/tempo/latest/traceql/#traceql-query-editor) has been improved to facilitate the creation of TraceQL queries. In particular, it now features improved autocompletion, syntax highlighting, and error reporting.
{{< video-embed src="/media/docs/tempo/screen-recording-grafana-10.2-traceql-query-editor-improvements.mp4" >}}
## Grafana OnCall integration for Alerting
<!-- Brenda Muir -->
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>

After

Width:  |  Height:  |  Size: 100 B

@@ -0,0 +1,41 @@
/*
* This is a dummy plugin to test the frontend sandbox
* It is not meant to be used in any other way
* This file doesn't require any compilation
*/
define(['react', '@grafana/data', 'react-router-dom'], function (React, grafanaData, ReactRouterDom) {
const { AppPlugin } = grafanaData;
const { Switch, Route } = ReactRouterDom;
function PageOne() {
return React.createElement(
'div',
{
'data-testid': 'sandbox-app-test-page-one',
},
'This is a page one'
);
}
function App() {
return React.createElement(Switch, null, React.createElement(Route, { component: PageOne }));
}
function AppConfig() {
return React.createElement(
'div',
{
'data-testid': 'sandbox-app-test-config-page',
},
'This is a config page'
);
}
const plugin = new AppPlugin().setRootPage(App).addConfigPage({
title: 'Configuration',
icon: 'cog',
body: AppConfig,
id: 'configuration',
});
return { plugin };
});
@@ -0,0 +1,35 @@
{
"$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
"type": "app",
"name": "Sandbox app test plugin",
"id": "sandbox-app-test",
"info": {
"keywords": ["app", "sandbox"],
"description": "",
"author": {
"name": "Grafana"
},
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"links": [],
"screenshots": [],
"version": "1.0.0",
"updated": "2023-06-27"
},
"includes": [
{
"type": "page",
"icon": "cog",
"name": "Sandbox App Page",
"path": "/plugins/sandbox-app-test",
"role": "Admin",
"addToNav": true
}
],
"dependencies": {
"grafanaDependency": ">=10.0",
"plugins": []
}
}
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>

After

Width:  |  Height:  |  Size: 100 B

@@ -0,0 +1,115 @@
/*
* This is a dummy plugin to test the frontend sandbox
* It is not meant to be used in any other way
* This file doesn't require any compilation
*/
define(['react', '@grafana/data'], function (React, grafanaData) {
const { DataSourcePlugin, DataSourceApi, MutableDataFrame, FieldType } = grafanaData;
const { useState } = React;
const QueryEditor = (props) => {
const [value, setValue] = useState('');
const handleChange = (event) => {
setValue(event.target.value);
props.onChange({
...props.query,
testValue: event.target.value,
});
props.onRunQuery();
};
return React.createElement(
'div',
null,
React.createElement('label', { htmlFor: 'inputField' }, 'Dummy input field'),
React.createElement('input', {
type: 'text',
id: 'inputField',
'data-testid': 'sandbox-query-editor-query-input',
value: value,
onChange: handleChange,
})
);
};
const ConfigEditor = (props) => {
const { onOptionsChange, options } = props;
const value = options.jsonData.input || '';
const handleChange = (event) => {
onOptionsChange({
...options,
jsonData: {
input: event.target.value,
},
});
};
return React.createElement(
'div',
null,
React.createElement('label', { htmlFor: 'inputField' }, 'Test Config field'),
React.createElement('input', {
type: 'text',
id: 'inputField',
'data-testid': 'sandbox-config-editor-query-input',
value: value,
onChange: handleChange,
})
);
};
class BasicDataSource extends DataSourceApi {
constructor(instanceSettings) {
super(instanceSettings);
}
// this is a test query it'll generate a logarithmic series starting now-1h
async query(options) {
const promises = options.targets.map(async (target) => {
const query = target;
const timestamps = [];
const values = [];
const endTime = Date.now();
const startTime = endTime - 3600000; // 1 hour in milliseconds
// Define the logarithmic base and increment factor
const base = 2;
const increment = 0.1;
// Generate the data points
for (let i = 0; i < 50; i++) {
// Calculate the timestamp
const timestamp = startTime + ((endTime - startTime) / 50) * i;
timestamps.push(timestamp);
// Calculate the value using logarithmic increase
const value = Math.pow(base, increment * i);
values.push(value);
}
return new MutableDataFrame({
refId: query.refId,
fields: [
{ name: 'Time', type: FieldType.time, values: timestamps },
{ name: 'Value', type: FieldType.number, values: values },
],
});
});
return Promise.all(promises).then((data) => ({ data }));
}
async testDatasource() {
return {
status: 'success',
message: 'Sandbox Success',
};
}
}
const plugin = new DataSourcePlugin(BasicDataSource).setConfigEditor(ConfigEditor).setQueryEditor(QueryEditor);
return { plugin };
});
@@ -0,0 +1,26 @@
{
"$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
"type": "datasource",
"name": "Sandbox datasource test plugin",
"id": "sandbox-test-datasource",
"metrics": true,
"info": {
"keywords": ["explore", "datasource"],
"description": "",
"author": {
"name": "Grafana"
},
"logos": {
"small": "img/logo.svg",
"large": "img/logo.svg"
},
"links": [],
"screenshots": [],
"version": "1.0.0",
"updated": "2023-06-27"
},
"dependencies": {
"grafanaDependency": ">=10.0",
"plugins": []
}
}
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>

After

Width:  |  Height:  |  Size: 100 B

@@ -57,7 +57,7 @@ describe('Panel sandbox', () => {
});
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled');
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x');
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true });
cy.get('[data-sandbox-test="panel-editor"]').should('exist');
});
});
@@ -114,7 +114,7 @@ describe('Panel sandbox', () => {
});
cy.get('[data-testid="panel-editor-custom-editor-input"]').should('not.be.disabled');
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x');
cy.get('[data-testid="panel-editor-custom-editor-input"]').type('x', { force: true });
cy.wait(100); // small delay to prevent false positives from too fast tests
cy.get('[data-sandbox-test="panel-editor"]').should('not.exist');
});
@@ -0,0 +1,85 @@
import { e2e } from '../utils';
const APP_ID = 'sandbox-app-test';
describe('Datasource sandbox', () => {
before(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
cy.request({
url: `${Cypress.env('BASE_URL')}/api/plugins/${APP_ID}/settings`,
method: 'POST',
body: {
enabled: true,
},
});
});
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
});
describe('App Page', () => {
describe('Sandbox disabled', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
});
});
it('Loads the app page without the sandbox div wrapper', () => {
e2e.pages.Home.visit();
e2e.components.NavBar.Toggle.button().click();
e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click();
e2e.components.NavMenu.item().contains('Sandbox app test plugin').click();
cy.wait(200); // wait to prevent false positives because cypress checks too fast
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('not.exist');
cy.get('div[data-testid="sandbox-app-test-page-one"]').should('exist');
});
it('Loads the app configuration without the sandbox div wrapper', () => {
e2e.pages.Home.visit();
e2e.components.NavBar.Toggle.button().click();
e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click();
e2e.components.NavMenu.item().contains('Apps').click();
cy.get('a[aria-label="Tab Sandbox App Page"]').click();
cy.wait(200); // wait to prevent false positives because cypress checks too fast
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('not.exist');
cy.get('div[data-testid="sandbox-app-test-config-page"]').should('exist');
});
});
describe('Sandbox enabled', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
});
});
it('Loads the app page with the sandbox div wrapper', () => {
e2e.pages.Home.visit();
e2e.components.NavBar.Toggle.button().click();
e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click();
e2e.components.NavMenu.item().contains('Sandbox app test plugin').click();
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('exist');
cy.get('div[data-testid="sandbox-app-test-page-one"]').should('exist');
});
it('Loads the app configuration with the sandbox div wrapper', () => {
e2e.pages.Home.visit();
e2e.components.NavBar.Toggle.button().click();
e2e.components.NavToolbar.container().get('[aria-label="Expand section Apps"]').click();
e2e.components.NavMenu.item().contains('Apps').click();
cy.get('a[aria-label="Tab Sandbox App Page"]').click();
cy.get('div[data-plugin-sandbox="sandbox-app-test"]').should('exist');
cy.get('div[data-testid="sandbox-app-test-config-page"]').should('exist');
});
});
});
afterEach(() => {
e2e.flows.revertAllChanges();
});
after(() => {
cy.clearCookies();
});
});
@@ -0,0 +1,145 @@
import { random } from 'lodash';
import { e2e } from '../utils';
const DATASOURCE_ID = 'sandbox-test-datasource';
let DATASOURCE_CONNECTION_ID = '';
const DATASOURCE_TYPED_NAME = 'SandboxDatasourceInstance';
describe('Datasource sandbox', () => {
before(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
e2e.pages.AddDataSource.visit();
e2e.pages.AddDataSource.dataSourcePluginsV2('Sandbox datasource test plugin')
.scrollIntoView()
.should('be.visible') // prevents flakiness
.click();
e2e.pages.DataSource.name().clear();
e2e.pages.DataSource.name().type(DATASOURCE_TYPED_NAME);
e2e.pages.DataSource.saveAndTest().click();
cy.url().then((url) => {
const split = url.split('/');
DATASOURCE_CONNECTION_ID = split[split.length - 1];
});
});
beforeEach(() => {
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'), true);
});
describe('Config Editor', () => {
describe('Sandbox disabled', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
});
});
it('Should not render a sandbox wrapper around the datasource config editor', () => {
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
cy.wait(300); // wait to prevent false positives because cypress checks too fast
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('not.exist');
});
});
describe('Sandbox enabled', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
});
});
it('Should render a sandbox wrapper around the datasource config editor', () => {
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('exist');
});
it('Should store values in jsonData and secureJsonData correctly', () => {
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
const valueToStore = 'test' + random(100);
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('not.be.disabled');
cy.get('[data-testid="sandbox-config-editor-query-input"]').type(valueToStore);
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('have.value', valueToStore);
e2e.pages.DataSource.saveAndTest().click();
e2e.pages.DataSource.alert().should('exist').contains('Sandbox Success', {});
// validate the value was stored
e2e.pages.EditDataSource.visit(DATASOURCE_CONNECTION_ID);
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('not.be.disabled');
cy.get('[data-testid="sandbox-config-editor-query-input"]').should('have.value', valueToStore);
});
});
});
describe('Explore Page', () => {
describe('Sandbox disabled', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=0');
});
});
it('Should not wrap the query editor in a sandbox wrapper', () => {
e2e.pages.Explore.visit();
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
cy.wait(300); // wait to prevent false positives because cypress checks too fast
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('not.exist');
});
it('Should accept values when typed', () => {
e2e.pages.Explore.visit();
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
const valueToType = 'test' + random(100);
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('not.be.disabled');
cy.get('[data-testid="sandbox-query-editor-query-input"]').type(valueToType);
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('have.value', valueToType);
});
});
describe('Sandbox enabled', () => {
beforeEach(() => {
cy.window().then((win) => {
win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
});
});
it('Should wrap the query editor in a sandbox wrapper', () => {
e2e.pages.Explore.visit();
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
cy.get(`div[data-plugin-sandbox="${DATASOURCE_ID}"]`).should('exist');
});
it('Should accept values when typed', () => {
e2e.pages.Explore.visit();
e2e.components.DataSourcePicker.container().should('be.visible').click();
cy.contains(DATASOURCE_TYPED_NAME).scrollIntoView().should('be.visible').click();
const valueToType = 'test' + random(100);
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('not.be.disabled');
cy.get('[data-testid="sandbox-query-editor-query-input"]').type(valueToType);
cy.get('[data-testid="sandbox-query-editor-query-input"]').should('have.value', valueToType);
// typing the query editor should reflect in the url
cy.url().should('include', valueToType);
});
});
});
afterEach(() => {
e2e.flows.revertAllChanges();
});
after(() => {
cy.clearCookies();
});
});
+6 -5
View File
@@ -67,7 +67,7 @@ require (
github.com/grafana/cuetsy v0.1.10 // @grafana/grafana-as-code
github.com/grafana/grafana-aws-sdk v0.19.1 // @grafana/aws-datasources
github.com/grafana/grafana-azure-sdk-go v1.9.0 // @grafana/backend-platform
github.com/grafana/grafana-plugin-sdk-go v0.179.0 // @grafana/plugins-platform-backend
github.com/grafana/grafana-plugin-sdk-go v0.180.0 // @grafana/plugins-platform-backend
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform
github.com/hashicorp/go-hclog v1.5.0 // @grafana/plugins-platform-backend
github.com/hashicorp/go-plugin v1.4.9 // @grafana/plugins-platform-backend
@@ -201,7 +201,7 @@ require (
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common/sigv4 v0.1.0 // indirect
github.com/prometheus/exporter-toolkit v0.10.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/prometheus/procfs v0.11.0 // indirect
github.com/protocolbuffers/txtpbfmt v0.0.0-20220428173112-74888fd59c2b // indirect
github.com/rs/cors v1.9.0 // indirect
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
@@ -234,7 +234,7 @@ require (
github.com/armon/go-radix v1.0.0 // @grafana/grafana-app-platform-squad
github.com/blugelabs/bluge v0.1.9 // @grafana/backend-platform
github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/backend-platform
github.com/bufbuild/connect-go v1.4.1 // @grafana/observability-traces-and-profiling
github.com/bufbuild/connect-go v1.10.0 // @grafana/observability-traces-and-profiling
github.com/dlmiddlecote/sqlstats v1.0.2 // @grafana/backend-platform
github.com/drone/drone-cli v1.6.1 // @grafana/grafana-delivery
github.com/getkin/kin-openapi v0.120.0 // @grafana/grafana-operator-experience-squad
@@ -242,7 +242,6 @@ require (
github.com/google/go-github/v45 v45.2.0 // @grafana/grafana-delivery
github.com/grafana/codejen v0.0.3 // @grafana/dataviz-squad
github.com/grafana/dskit v0.0.0-20230706162620-5081d8ed53e6 // @grafana/backend-platform
github.com/grafana/phlare/api v0.1.4-0.20230426005640-f90edba05413 // @grafana/observability-traces-and-profiling
github.com/huandu/xstrings v1.3.1 // @grafana/partner-datasources
github.com/jmoiron/sqlx v1.3.5 // @grafana/backend-platform
github.com/matryer/is v1.4.0 // @grafana/grafana-as-code
@@ -289,6 +288,8 @@ require (
require github.com/grafana/gofpdf v0.0.0-20231002120153-857cc45be447 // @grafana/sharing-squad
require github.com/grafana/pyroscope/api v0.2.0 // @grafana/observability-traces-and-profiling
require (
cloud.google.com/go v0.110.6 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
@@ -489,7 +490,7 @@ require (
)
// Use fork of crewjam/saml with fixes for some issues until changes get merged into upstream
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.13-0.20230823151800-99004118fff5
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.13-0.20231011134114-94154cb1bda6
// Thema's thema CLI requires cobra, which eventually works its way down to go-hclog@v1.0.0.
// Upgrading affects backend plugins: https://github.com/grafana/grafana/pull/47653#discussion_r850508593
+10 -11
View File
@@ -841,8 +841,8 @@ github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pO
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bsm/ginkgo/v2 v2.5.0 h1:aOAnND1T40wEdAtkGSkvSICWeQ8L3UASX7YVCqQx+eQ=
github.com/bsm/gomega v1.20.0 h1:JhAwLmtRzXFTx2AkALSLa8ijZafntmhSoU63Ok18Uq8=
github.com/bufbuild/connect-go v1.4.1 h1:6usL3JGjKhxQpvDlizP7u8VfjAr1JkckcAUbrdcbgNY=
github.com/bufbuild/connect-go v1.4.1/go.mod h1:9iNvh/NOsfhNBUH5CtvXeVUskQO1xsrEviH7ZArwZ3I=
github.com/bufbuild/connect-go v1.10.0 h1:QAJ3G9A1OYQW2Jbk3DeoJbkCxuKArrvZgDt47mjdTbg=
github.com/bufbuild/connect-go v1.10.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/buildkite/yaml v2.1.0+incompatible h1:xirI+ql5GzfikVNDmt+yeiXpf/v1Gt03qXTtT5WXdr8=
github.com/buildkite/yaml v2.1.0+incompatible/go.mod h1:UoU8vbcwu1+vjZq01+KrpSeLBgQQIjL/H7Y6KwikUrI=
@@ -1782,8 +1782,6 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotestyourself/gotestyourself v1.3.0/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/gotestyourself/gotestyourself v2.2.0+incompatible/go.mod h1:zZKM6oeNM8k+FRljX1mnzVYeS8wiGgQyvST1/GafPbY=
github.com/grafana/alerting v0.0.0-20230918125844-e60d564786c9 h1:XHgUyX+q7zYJnBmtbfPyBeOg0W/ds/uw218XOP2k5v0=
github.com/grafana/alerting v0.0.0-20230918125844-e60d564786c9/go.mod h1:6BES5CyEqz7fDAG3MYvJLe0hqGwvIoGDN8A1aNrLGus=
github.com/grafana/alerting v0.0.0-20231005194306-939f55779e5f h1:zz5YocNyxQ9X8Bvm2ezPOCGKxngrXX1XGLfywqk5+vU=
github.com/grafana/alerting v0.0.0-20231005194306-939f55779e5f/go.mod h1:6BES5CyEqz7fDAG3MYvJLe0hqGwvIoGDN8A1aNrLGus=
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
@@ -1810,19 +1808,19 @@ github.com/grafana/grafana-google-sdk-go v0.1.0 h1:LKGY8z2DSxKjYfr2flZsWgTRTZ6HG
github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkrZvFtBws0qSZdXhQxRdJrE=
github.com/grafana/grafana-plugin-sdk-go v0.94.0/go.mod h1:3VXz4nCv6wH5SfgB3mlW39s+c+LetqSCjFj7xxPC5+M=
github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk=
github.com/grafana/grafana-plugin-sdk-go v0.179.0 h1:gaow50UDWLhvVlrPye4rESajpRItEecKi2+i+TE1r7Q=
github.com/grafana/grafana-plugin-sdk-go v0.179.0/go.mod h1:fPX9spPWEzyUg0BLTQbdXCCq7PVSJZGNVKfNpiTQUts=
github.com/grafana/grafana-plugin-sdk-go v0.180.0 h1:+5aFolBj2H7ze7oejIUjrkmZ9lSydnsbo3e4Kn1CN1Y=
github.com/grafana/grafana-plugin-sdk-go v0.180.0/go.mod h1:fPX9spPWEzyUg0BLTQbdXCCq7PVSJZGNVKfNpiTQUts=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo=
github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y=
github.com/grafana/phlare/api v0.1.4-0.20230426005640-f90edba05413 h1:bBzCezZNRyYlJpXTkyZdY4fpPxHZUdyeyRWzhtw/P6I=
github.com/grafana/phlare/api v0.1.4-0.20230426005640-f90edba05413/go.mod h1:IvwuGG9xa/h96UH/exgvsfy3zE+ZpctkNT9o5aaGdrU=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7 h1:7gsywzIb39SYZEp9qOnNaxD4d9OOkAfJGvnRUQUtlTM=
github.com/grafana/prometheus-alertmanager v0.25.1-0.20230918083811-3513be6760b7/go.mod h1:0SOsbwZb277OdNdUR+376IxcAYV3Pp+igiAOJdr/98M=
github.com/grafana/pyroscope/api v0.2.0 h1:TzOxL0s6SiaLEy944ZAKgHcx/JDRJXu4O8ObwkqR6p4=
github.com/grafana/pyroscope/api v0.2.0/go.mod h1:nhH+xai9cYFgs6lMy/+L0pKj0d5yCMwji/QAiQFCP+U=
github.com/grafana/regexp v0.0.0-20221122212121-6b5c0a4cb7fd/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db h1:7aN5cccjIqCLTzedH7MZzRZt5/lsAHch6Z3L2ZGn5FA=
github.com/grafana/regexp v0.0.0-20221123153739-15dc172cd2db/go.mod h1:M5qHK+eWfAv8VR/265dIuEpL3fNfeC21tXXp9itM24A=
github.com/grafana/saml v0.4.13-0.20230823151800-99004118fff5 h1:F0fY72CD+fFUFQVbqVXqszdm/T3b/LUqv2EssTAwm14=
github.com/grafana/saml v0.4.13-0.20230823151800-99004118fff5/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
github.com/grafana/saml v0.4.13-0.20231011134114-94154cb1bda6 h1:CuCfH9nIpkrOkv1UasyLexbskquvgxsmrUy6MaJEwDM=
github.com/grafana/saml v0.4.13-0.20231011134114-94154cb1bda6/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
github.com/grafana/sqlds/v2 v2.3.10 h1:HWKhE0vR6LoEiE+Is8CSZOgaB//D1yqb2ntkass9Fd4=
github.com/grafana/sqlds/v2 v2.3.10/go.mod h1:c6ibxnxRVGxV/0YkEgvy7QpQH/lyifFyV7K/14xvdIs=
github.com/grafana/tempo v1.5.1-0.20230524121406-1dc1bfe7085b h1:mDlkqgTEJuK7vjPG44f3ZMtId5AAYLWHvBVbiGqIOOQ=
@@ -2594,8 +2592,9 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/prometheus v0.43.0 h1:18iCSfrbAHbXvYFvR38U1Pt4uZmU9SmDcCpCrBKUiGg=
github.com/prometheus/prometheus v0.43.0/go.mod h1:2BA14LgBeqlPuzObSEbh+Y+JwLH2GcqDlJKbF2sA6FM=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+6 -6
View File
@@ -42,11 +42,11 @@ lineage: schemas: [{
timezone?: string | *"browser"
// Whether a dashboard is editable or not.
editable: bool | *true
editable?: bool | *true
// Configuration of dashboard cursor sync behavior.
// Accepted values are 0 (sync turned off), 1 (shared crosshair), 2 (shared crosshair and tooltip).
graphTooltip: #DashboardCursorSync
graphTooltip?: #DashboardCursorSync
// Time range for dashboard.
// Accepted values are relative time strings like {from: 'now-6h', to: 'now'} or absolute time strings like {from: '2020-07-10T08:00:00.000Z', to: '2020-07-10T14:00:00.000Z'}.
@@ -515,7 +515,7 @@ lineage: schemas: [{
description?: string
// Whether to display the panel without a background.
transparent: bool | *false
transparent?: bool | *false
// The datasource used in all targets.
datasource?: #DataSourceRef
@@ -543,7 +543,7 @@ lineage: schemas: [{
// List of transformations that are applied to the panel data before rendering.
// When there are multiple transformations, Grafana applies them in the order they are listed.
// Each transformation creates a result set that then passes on to the next transformation in the processing pipeline.
transformations: [...#DataTransformerConfig]
transformations?: [...#DataTransformerConfig]
// The min time interval setting defines a lower limit for the $__interval and $__interval_ms variables.
// This value must be formatted as a number followed by a valid time
@@ -574,10 +574,10 @@ lineage: schemas: [{
libraryPanel?: #LibraryPanelRef
// It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
options: {...} @grafanamaturity(NeedsExpertReview)
options?: {...} @grafanamaturity(NeedsExpertReview)
// Field options allow you to change how the data is displayed in your visualizations.
fieldConfig: #FieldConfigSource
fieldConfig?: #FieldConfigSource
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
+11 -10
View File
@@ -49,6 +49,7 @@
"i18n:extract": "yarn run i18next -c public/locales/i18next-parser.config.js 'public/**/*.{tsx,ts}' 'packages/grafana-ui/**/*.{tsx,ts}' && yarn i18n:pseudo",
"i18n:compile": "echo 'no i18n compile yet, all good'",
"i18n:pseudo": "node ./public/locales/pseudo.js",
"i18n:stats": "node ./scripts/cli/reportI18nStats.mjs",
"betterer": "betterer",
"betterer:merge": "betterer merge",
"betterer:stats": "ts-node --transpile-only --project ./scripts/cli/tsconfig.json ./scripts/cli/reportBettererStats.ts",
@@ -85,14 +86,14 @@
"@react-types/button": "3.9.0",
"@react-types/menu": "3.9.2",
"@react-types/overlays": "3.8.0",
"@react-types/shared": "3.18.1",
"@react-types/shared": "3.21.0",
"@rtsao/plugin-proposal-class-properties": "7.0.1-patch.1",
"@swc/core": "1.3.38",
"@swc/helpers": "0.4.14",
"@testing-library/dom": "9.3.1",
"@testing-library/dom": "9.3.3",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@testing-library/user-event": "14.5.1",
"@types/angular": "1.8.5",
"@types/angular-route": "1.7.3",
"@types/chance": "^1.1.3",
@@ -117,7 +118,7 @@
"@types/lucene": "^2",
"@types/marked": "5.0.1",
"@types/mousetrap": "1.6.11",
"@types/node": "18.16.16",
"@types/node": "18.18.4",
"@types/node-forge": "^1",
"@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.0",
"@types/papaparse": "5.3.7",
@@ -157,7 +158,7 @@
"blob-polyfill": "7.0.20220408",
"browserslist": "^4.21.4",
"chance": "^1.0.10",
"chrome-remote-interface": "0.32.2",
"chrome-remote-interface": "0.33.0",
"codeowners": "^5.1.1",
"copy-webpack-plugin": "11.0.0",
"css-loader": "6.8.1",
@@ -247,10 +248,10 @@
"@grafana/flamegraph": "workspace:*",
"@grafana/google-sdk": "0.1.1",
"@grafana/lezer-logql": "0.2.0",
"@grafana/lezer-traceql": "0.0.6",
"@grafana/lezer-traceql": "0.0.7",
"@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*",
"@grafana/scenes": "^1.13.0",
"@grafana/scenes": "^1.15.0",
"@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*",
"@kusto/monaco-kusto": "^7.4.0",
@@ -305,7 +306,7 @@
"combokeys": "^3.0.0",
"comlink": "4.4.1",
"common-tags": "1.8.2",
"core-js": "3.31.1",
"core-js": "3.33.0",
"d3": "7.8.5",
"d3-force": "3.0.0",
"d3-scale-chromatic": "3.0.0",
@@ -417,8 +418,8 @@
"resolutions": {
"underscore": "1.13.6",
"@types/slate": "0.47.11",
"@rushstack/rig-package": "0.4.0",
"@rushstack/ts-command-line": "4.15.1",
"@rushstack/rig-package": "0.5.1",
"@rushstack/ts-command-line": "4.16.1",
"ngtemplate-loader/loader-utils": "^2.0.0",
"trim": "1.0.1",
"semver@~7.0.0": "7.5.4",
+4 -4
View File
@@ -65,18 +65,18 @@
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.1.0",
"@testing-library/dom": "9.3.1",
"@rollup/plugin-node-resolve": "15.2.3",
"@testing-library/dom": "9.3.3",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@testing-library/user-event": "14.5.1",
"@types/dompurify": "^2",
"@types/history": "4.7.11",
"@types/jest": "29.5.4",
"@types/jquery": "3.5.16",
"@types/lodash": "4.14.195",
"@types/marked": "5.0.1",
"@types/node": "18.16.19",
"@types/node": "18.18.4",
"@types/papaparse": "5.3.7",
"@types/react": "18.2.15",
"@types/react-dom": "18.2.7",
+1 -1
View File
@@ -8,4 +8,4 @@ export * from './ArrayDataFrame';
export * from './DataFrameJSON';
export * from './frameComparisons';
export { anySeriesWithTimeField, isTimeSeriesFrame, isTimeSeriesFrames } from './utils';
export { StreamingDataFrame, StreamingFrameAction, type StreamingFrameOptions } from './StreamingDataFrame';
export { StreamingDataFrame, StreamingFrameAction, type StreamingFrameOptions, closestIdx } from './StreamingDataFrame';
@@ -330,6 +330,106 @@ describe('applyFieldOverrides', () => {
expect(range.min).toEqual(-20);
});
it('should calculate min/max per field when fieldMinMax is set', () => {
const df = toDataFrame([
{ title: 'AAA', value: 100, value2: 1234 },
{ title: 'BBB', value: -20, value2: null },
{ title: 'CCC', value: 200, value2: 1000 },
]);
const fieldCfgSource: FieldConfigSource = {
defaults: {
fieldMinMax: true,
},
overrides: [],
};
const data = applyFieldOverrides({
data: [df], // the frame
fieldConfig: fieldCfgSource,
replaceVariables: undefined as unknown as InterpolateFunction,
theme: createTheme(),
fieldConfigRegistry: customFieldRegistry,
})[0];
const valueColumn1 = data.fields[1];
const range1 = valueColumn1.state!.range!;
expect(range1.max).toEqual(200);
expect(range1.min).toEqual(-20);
const valueColumn2 = data.fields[2];
const range2 = valueColumn2.state!.range!;
expect(range2.max).toEqual(1234);
expect(range2.min).toEqual(1000);
});
it('should calculate min/max locally for fields with fieldMinMax and globally for other fields', () => {
const df = toDataFrame({
fields: [
{ name: 'first', type: FieldType.number, values: [-1, -2] },
{ name: 'second', type: FieldType.number, values: [1, 2] },
{ name: 'third', type: FieldType.number, values: [1000, 2000] },
],
});
const fieldCfgSource: FieldConfigSource = {
defaults: {},
overrides: [
{
matcher: { id: FieldMatcherID.byName, options: 'second' },
properties: [{ id: 'fieldMinMax', value: true }],
},
],
};
const data = applyFieldOverrides({
data: [df], // the frame
fieldConfig: fieldCfgSource,
replaceVariables: undefined as unknown as InterpolateFunction,
theme: createTheme(),
fieldConfigRegistry: customFieldRegistry,
})[0];
const valueColumn0 = data.fields[0];
const range0 = valueColumn0.state!.range!;
expect(range0.max).toEqual(2000);
expect(range0.min).toEqual(-2);
const valueColumn1 = data.fields[1];
const range1 = valueColumn1.state!.range!;
expect(range1.max).toEqual(2);
expect(range1.min).toEqual(1);
const valueColumn2 = data.fields[2];
const range2 = valueColumn2.state!.range!;
expect(range2.max).toEqual(2000);
expect(range2.min).toEqual(-2);
});
it('should not calculate min if min is set', () => {
const df = toDataFrame({
fields: [{ name: 'first', type: FieldType.number, values: [-1, -2] }],
});
const fieldCfgSource: FieldConfigSource = {
defaults: {
min: 1,
fieldMinMax: true,
},
overrides: [],
};
const data = applyFieldOverrides({
data: [df], // the frame
fieldConfig: fieldCfgSource,
replaceVariables: undefined as unknown as InterpolateFunction,
theme: createTheme(),
fieldConfigRegistry: customFieldRegistry,
})[0];
const valueColumn0 = data.fields[0];
const range0 = valueColumn0.state!.range!;
expect(range0.max).toEqual(-1);
expect(range0.min).toEqual(1);
});
it('getLinks should use applied field config', () => {
const replaceVariablesCalls: ScopedVars[] = [];
@@ -40,6 +40,7 @@ import { mapInternalLinkToExplore } from '../utils/dataLinks';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor';
import { getMinMaxAndDelta } from './scale';
import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry';
interface OverrideProps {
@@ -166,15 +167,8 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
}
// Set the Min/Max value automatically
let range: NumericRange | undefined = undefined;
if (field.type === FieldType.number) {
if (!globalRange && (!isNumber(config.min) || !isNumber(config.max))) {
globalRange = findNumericFieldMinMax(options.data!);
}
const min = config.min ?? globalRange!.min;
const max = config.max ?? globalRange!.max;
range = { min, max, delta: max! - min! };
}
const { range, newGlobalRange } = calculateRange(config, field, globalRange, options.data!);
globalRange = newGlobalRange;
field.state!.seriesIndex = seriesIndex;
field.state!.range = range;
@@ -243,6 +237,32 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
});
}
function calculateRange(
config: FieldConfig<any>,
field: Field,
globalRange: NumericRange | undefined,
data: DataFrame[]
): { range?: { min?: number | null; max?: number | null; delta: number }; newGlobalRange: NumericRange | undefined } {
// Only calculate ranges when the field is a number and one of min/max is set to auto.
if (field.type !== FieldType.number || (isNumber(config.min) && isNumber(config.max))) {
return { newGlobalRange: globalRange };
}
// Calculate the min/max from the field.
if (config.fieldMinMax) {
const localRange = getMinMaxAndDelta(field);
const min = config.min ?? localRange.min;
const max = config.max ?? localRange.max;
return { range: { min, max, delta: max! - min! }, newGlobalRange: globalRange };
}
// We use the global range if supplied, otherwise we calculate it.
const newGlobalRange = globalRange ?? findNumericFieldMinMax(data);
const min = config.min ?? newGlobalRange!.min;
const max = config.max ?? newGlobalRange!.max;
return { range: { min, max, delta: max! - min! }, newGlobalRange };
}
// this is a significant optimization for streaming, where we currently re-process all values in the buffer on ech update
// via field.display(value). this can potentially be removed once we...
// 1. process data packets incrementally and/if cache the results in the streaming datafame (maybe by buffer index)
@@ -315,7 +315,11 @@ export function getNameFromOptions(options: CalculateFieldTransformerOptions) {
}
case CalculateFieldMode.BinaryOperation: {
const { binary } = options;
return `${binary?.left ?? ''} ${binary?.operator ?? ''} ${binary?.right ?? ''}`;
const alias = `${binary?.left ?? ''} ${binary?.operator ?? ''} ${binary?.right ?? ''}`;
//Remove $ signs as they will be interpolated and cause issues. Variables can still be used
//in alias but shouldn't in the autogenerated name
return alias.replace(/\$/g, '');
}
case CalculateFieldMode.ReduceRow:
{
@@ -101,6 +101,9 @@ export interface FieldConfig<TOptions = any> {
// Panel Specific Values
custom?: TOptions;
// Calculate min max per field
fieldMinMax?: boolean;
}
export interface FieldTypeConfig {
@@ -66,7 +66,6 @@ export interface FeatureToggles {
gcomOnlyExternalOrgRoleSync?: boolean;
prometheusMetricEncyclopedia?: boolean;
timeSeriesTable?: boolean;
prometheusResourceBrowserCache?: boolean;
influxdbBackendMigration?: boolean;
clientTokenRotation?: boolean;
prometheusDataplane?: boolean;
@@ -118,7 +117,6 @@ export interface FeatureToggles {
prometheusConfigOverhaulAuth?: boolean;
configurableSchedulerTick?: boolean;
influxdbSqlSupport?: boolean;
noBasicRole?: boolean;
alertingNoDataErrorExecution?: boolean;
angularDeprecationUI?: boolean;
dashgpt?: boolean;
@@ -126,6 +124,7 @@ export interface FeatureToggles {
newBrowseDashboards?: boolean;
sseGroupByDatasource?: boolean;
requestInstrumentationStatusSource?: boolean;
libraryPanelRBAC?: boolean;
lokiRunQueriesInParallel?: boolean;
wargamesTesting?: boolean;
alertingInsights?: boolean;
@@ -141,4 +140,6 @@ export interface FeatureToggles {
transformationsVariableSupport?: boolean;
kubernetesPlaylists?: boolean;
navAdminSubsections?: boolean;
recoveryThreshold?: boolean;
awsDatasourcesNewFormStyling?: boolean;
}
+1
View File
@@ -225,6 +225,7 @@ export const availableIconsIndex = {
'vertical-align-bottom': true,
'vertical-align-center': true,
'vertical-align-top': true,
'web-section-alt': true,
'wrap-text': true,
rss: true,
x: true,
+1
View File
@@ -63,4 +63,5 @@ export {
type PluginExtensionEventHelpers,
type PluginExtensionPanelContext,
type PluginExtensionDataSourceConfigContext,
type PluginExtensionOpenModalOptions,
} from './pluginExtensions';
@@ -95,15 +95,21 @@ export type PluginExtensionComponentConfig<Context extends object = object> = {
export type PluginExtensionConfig = PluginExtensionLinkConfig | PluginExtensionComponentConfig;
export type PluginExtensionOpenModalOptions = {
// The title of the modal
title: string;
// A React element that will be rendered inside the modal
body: React.ElementType<{ onDismiss?: () => void }>;
// Width of the modal in pixels or percentage
width?: string | number;
// Height of the modal in pixels or percentage
height?: string | number;
};
export type PluginExtensionEventHelpers<Context extends object = object> = {
context?: Readonly<Context>;
// Opens a modal dialog and renders the provided React component inside it
openModal: (options: {
// The title of the modal
title: string;
// A React element that will be rendered inside the modal
body: React.ElementType<{ onDismiss?: () => void }>;
}) => void;
openModal: (options: PluginExtensionOpenModalOptions) => void;
};
// Extension Points & Contexts
@@ -67,6 +67,23 @@ export const mockStandardProperties = () => {
shouldApply: () => true,
};
const fieldMinMax = {
id: 'fieldMinMax',
path: 'fieldMinMax',
name: 'localminmax',
description: 'Calculate min/max per field ',
editor: () => null,
override: () => null,
process: identityOverrideProcessor,
settings: {
placeholder: 'auto',
},
shouldApply: () => true,
};
const decimals = {
id: 'decimals',
path: 'decimals',
@@ -166,5 +183,5 @@ export const mockStandardProperties = () => {
shouldApply: () => true,
};
return [unit, min, max, decimals, title, noValue, thresholds, mappings, links, color];
return [unit, min, max, fieldMinMax, decimals, title, noValue, thresholds, mappings, links, color];
};
@@ -61,7 +61,7 @@ export function getPanelPlugin(
},
screenshots: [],
updated: '',
version: '',
version: '1',
},
hideFromList: options.hideFromList === true,
module: options.module ?? '',
+2 -2
View File
@@ -40,8 +40,8 @@
},
"devDependencies": {
"@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-node-resolve": "15.1.0",
"@types/node": "18.16.19",
"@rollup/plugin-node-resolve": "15.2.3",
"@types/node": "18.18.4",
"esbuild": "0.18.12",
"rimraf": "5.0.1",
"rollup": "2.79.1",
+3 -3
View File
@@ -47,10 +47,10 @@
"postpack": "mv package.json.bak package.json"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@types/chrome-remote-interface": "0.31.10",
"@types/lodash": "4.14.195",
"@types/node": "18.16.19",
"@types/node": "18.18.4",
"@types/uuid": "9.0.2",
"esbuild": "0.18.12",
"rollup": "2.79.1",
@@ -69,7 +69,7 @@
"@mochajs/json-file-reporter": "^1.2.0",
"babel-loader": "9.1.3",
"blink-diff": "1.0.13",
"chrome-remote-interface": "0.32.2",
"chrome-remote-interface": "0.33.0",
"commander": "8.3.0",
"cypress": "9.5.1",
"cypress-file-upload": "5.0.8",
+2 -2
View File
@@ -60,10 +60,10 @@
"@babel/preset-env": "7.22.9",
"@babel/preset-react": "7.22.5",
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@testing-library/jest-dom": "^6.1.2",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@testing-library/user-event": "14.5.1",
"@types/d3": "^7",
"@types/jest": "^29.5.4",
"@types/lodash": "4.14.195",
@@ -112,9 +112,9 @@ export function getBarColorByDiff(
// the language from the backend and use the right regex but right now we just try all of them from most to least
// specific.
const matchers = [
['phpspy', /^(?<packageName>(.*\/)*)(?<filename>.*\.php+)(?<line_info>.*)$/],
['pyspy', /^(?<packageName>(.*\/)*)(?<filename>.*\.py+)(?<line_info>.*)$/],
['rbspy', /^(?<packageName>(.*\/)*)(?<filename>.*\.rb+)(?<line_info>.*)$/],
['phpspy', /^(?<packageName>([^\/]*\/)*)(?<filename>.*\.php+)(?<line_info>.*)$/],
['pyspy', /^(?<packageName>([^\/]*\/)*)(?<filename>.*\.py+)(?<line_info>.*)$/],
['rbspy', /^(?<packageName>([^\/]*\/)*)(?<filename>.*\.rb+)(?<line_info>.*)$/],
[
'nodespy',
/^(\.\/node_modules\/)?(?<packageName>[^/]*)(?<filename>.*\.?(jsx?|tsx?)?):(?<functionName>.*):(?<line_info>.*)$/,
+3 -3
View File
@@ -51,10 +51,10 @@
"devDependencies": {
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-node-resolve": "15.1.0",
"@testing-library/dom": "9.3.1",
"@rollup/plugin-node-resolve": "15.2.3",
"@testing-library/dom": "9.3.3",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@testing-library/user-event": "14.5.1",
"@types/angular": "1.8.5",
"@types/history": "4.7.11",
"@types/jest": "29.5.4",
+1 -1
View File
@@ -39,7 +39,7 @@
"@grafana/tsconfig": "^1.2.0-rc1",
"@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"esbuild": "0.18.12",
"glob": "^10.2.7",
"rimraf": "5.0.1",
@@ -644,7 +644,7 @@ export interface Panel {
/**
* Field options allow you to change how the data is displayed in your visualizations.
*/
fieldConfig: FieldConfigSource;
fieldConfig?: FieldConfigSource;
/**
* Grid position.
*/
@@ -684,7 +684,7 @@ export interface Panel {
/**
* It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
*/
options: Record<string, unknown>;
options?: Record<string, unknown>;
/**
* The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs.
*/
@@ -733,11 +733,11 @@ export interface Panel {
* When there are multiple transformations, Grafana applies them in the order they are listed.
* Each transformation creates a result set that then passes on to the next transformation in the processing pipeline.
*/
transformations: Array<DataTransformerConfig>;
transformations?: Array<DataTransformerConfig>;
/**
* Whether to display the panel without a background.
*/
transparent: boolean;
transparent?: boolean;
/**
* The panel plugin type id. This is used to find the plugin to display the panel.
*/
@@ -991,7 +991,7 @@ export interface Dashboard {
/**
* Whether a dashboard is editable or not.
*/
editable: boolean;
editable?: boolean;
/**
* The month that the fiscal year starts on. 0 = January, 11 = December
*/
@@ -1004,7 +1004,7 @@ export interface Dashboard {
* Configuration of dashboard cursor sync behavior.
* Accepted values are 0 (sync turned off), 1 (shared crosshair), 2 (shared crosshair and tooltip).
*/
graphTooltip: DashboardCursorSync;
graphTooltip?: DashboardCursorSync;
/**
* Unique numeric identifier for the dashboard.
* `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances.
@@ -7,7 +7,7 @@ export type { CommonDataSourceRef as DataSourceRef };
export interface Panel<TOptions = Record<string, unknown>, TCustomFieldConfig = Record<string, unknown>>
extends Omit<raw.Panel, 'fieldConfig'> {
fieldConfig: FieldConfigSource<TCustomFieldConfig>;
fieldConfig?: FieldConfigSource<TCustomFieldConfig>;
}
export interface RowPanel extends Omit<raw.RowPanel, 'panels'> {
@@ -36,6 +36,9 @@ export interface AnnotationQuery<TQuery extends DataQuery = DataQuery>
extends Omit<raw.AnnotationQuery, 'target' | 'datasource'> {
datasource?: DataSourceRef | null;
target?: TQuery;
// TODO: When migrating to snapshot queries, remove this property.
// With snapshot queries annotations become a part of the panel snapshot data.
snapshotData?: unknown;
}
export interface AnnotationContainer extends Omit<raw.AnnotationContainer, 'list'> {
-4
View File
@@ -19,7 +19,3 @@ See [package source](https://github.com/grafana/grafana/tree/main/packages/grafa
## Development
For development purposes we suggest using `yarn link` that will create symlink to @grafana/ui lib. To do so navigate to `packages/grafana-ui` and run `yarn link`. Then, navigate to your project and run `yarn link @grafana/ui` to use the linked version of the lib. To unlink follow the same procedure, but use `yarn unlink` instead.
### Storybook 6.x migration
We've upgraded Storybook to version 6 and with that we will convert to using [controls](https://storybook.js.org/docs/react/essentials/controls) instead of knobs for manipulating components. Controls will not require as much coding as knobs do. Please refer to the [storybook style-guide](https://github.com/grafana/grafana/blob/main/contribute/style-guides/storybook.md#contrls) for further information.
+5 -5
View File
@@ -66,7 +66,7 @@
"ansicolor": "1.1.100",
"calculate-size": "1.1.1",
"classnames": "2.3.2",
"core-js": "3.31.1",
"core-js": "3.33.0",
"d3": "7.8.5",
"date-fns": "2.30.0",
"hoist-non-react-statics": "3.3.2",
@@ -118,7 +118,7 @@
"@babel/core": "7.23.0",
"@grafana/tsconfig": "^1.2.0-rc1",
"@mdx-js/react": "1.6.22",
"@rollup/plugin-node-resolve": "15.1.0",
"@rollup/plugin-node-resolve": "15.2.3",
"@storybook/addon-a11y": "7.4.5",
"@storybook/addon-actions": "7.4.5",
"@storybook/addon-docs": "7.4.5",
@@ -134,10 +134,10 @@
"@storybook/react": "7.4.5",
"@storybook/react-webpack5": "7.4.5",
"@storybook/theming": "7.4.5",
"@testing-library/dom": "9.3.1",
"@testing-library/dom": "9.3.3",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
"@testing-library/user-event": "14.5.1",
"@types/common-tags": "^1.8.0",
"@types/d3": "7.4.0",
"@types/hoist-non-react-statics": "3.3.1",
@@ -146,7 +146,7 @@
"@types/jquery": "3.5.16",
"@types/lodash": "4.14.195",
"@types/mock-raf": "1.0.3",
"@types/node": "18.16.19",
"@types/node": "18.18.4",
"@types/prismjs": "1.26.0",
"@types/react": "18.2.15",
"@types/react-beautiful-dnd": "13.1.4",
@@ -302,7 +302,7 @@ const getStyles = (theme: GrafanaTheme2) => {
tabsWrapper: css({
label: 'drawer-tabs',
paddingLeft: theme.spacing(2),
margin: theme.spacing(0, -1, -3, -3),
margin: theme.spacing(1, -1, -3, -3),
}),
};
};
@@ -2,6 +2,8 @@ import { css, cx } from '@emotion/css';
import React, { InputHTMLAttributes } from 'react';
import { InlineFormLabel } from '../FormLabel/FormLabel';
import { Field } from '../Forms/Field';
import { InlineField } from '../Forms/InlineField';
import { PopoverContent } from '../Tooltip';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
@@ -23,6 +25,8 @@ const defaultProps = {
/**
* Default form field including label used in Grafana UI. Default input element is simple <input />. You can also pass
* custom inputEl if required in which case inputWidth and inputProps are ignored.
* @deprecated Please use the {@link Field} component, {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-field--simple See Storybook}.
* For inline fields, use {@link InlineField}, {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-inlinefield--basic See Storybook}.
*/
export const FormField = ({
label,
@@ -4,6 +4,7 @@ import React, { PureComponent, ChangeEvent } from 'react';
import { ValidationEvents, ValidationRule } from '../../../../types';
import { validate, EventsWithValidation, hasValidationEvent } from '../../../../utils';
/** @deprecated Please use the `Input` component, which does not require this enum. */
export enum LegacyInputStatus {
Invalid = 'invalid',
Valid = 'valid',
@@ -24,6 +25,7 @@ interface State {
error: string | null;
}
/** @deprecated Please use the `Input` component. {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-input--simple See Storybook for example.} */
export class Input extends PureComponent<Props, State> {
static defaultProps = {
className: '',
@@ -2,7 +2,9 @@ import React from 'react';
import { components, IndicatorsContainerProps } from 'react-select';
import { Icon } from '../../../Icon/Icon';
import { Select } from '../../../Select/Select';
/** @deprecated Please use the {@link Select} component*/
export const IndicatorsContainer = (props: IndicatorsContainerProps) => {
const isOpen = props.selectProps.menuIsOpen;
return (
@@ -3,8 +3,11 @@ import { components, NoticeProps, GroupBase } from 'react-select';
import { SelectableValue } from '@grafana/data';
import { Select } from '../../../Select/Select';
export type Props<T> = NoticeProps<SelectableValue<T>, boolean, GroupBase<SelectableValue<T>>>;
/** @deprecated Please use the {@link Select} component*/
export const NoOptionsMessage = <T extends unknown>(props: Props<T>) => {
const { children } = props;
return (
@@ -51,6 +51,8 @@ export const MenuList = (props: MenuListProps) => {
</components.MenuList>
);
};
/** @deprecated Please use the `Select` component, as seen {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-select--basic in Storybook}. */
export class Select<T> extends PureComponent<LegacySelectProps<T>> {
declare context: React.ContextType<typeof ThemeContext>;
static contextType = ThemeContext;
@@ -168,6 +170,7 @@ export class Select<T> extends PureComponent<LegacySelectProps<T>> {
}
}
/** @deprecated Please use the `Select` component with async functionality, as seen {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-select--basic-select-async in Storybook}. */
export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
static contextType = ThemeContext;
@@ -22,6 +22,7 @@ export interface State {
id: string;
}
/** @deprecated Please use the `Switch` component, {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-switch--controlled as seen in Storybook} */
export class Switch extends PureComponent<Props, State> {
state = {
id: uniqueId(),
@@ -4,6 +4,8 @@ import React, { InputHTMLAttributes } from 'react';
import { Button } from '../Button/Button';
import { FormField } from '../FormField/FormField';
import { Field } from '../Forms/Field';
import { SecretInput } from '../SecretInput';
import { PopoverContent } from '../Tooltip';
export interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'onReset'> {
@@ -33,11 +35,14 @@ const getSecretFormFieldStyles = () => {
}),
};
};
/**
* Form field that has 2 states configured and not configured. If configured it will not show its contents and adds
* a reset button that will clear the input and makes it accessible. In non configured state it behaves like normal
* form field. This is used for passwords or anything that is encrypted on the server and is later returned encrypted
* to the user (like datasource passwords).
*
* @deprecated Please use the {@link SecretInput} component with a {@link Field} instead, {@link https://developers.grafana.com/ui/latest/index.html?path=/story/forms-secretinput--basic as seen in Storybook}
*/
export const SecretFormField = ({
label = 'Password',
@@ -520,7 +520,7 @@ describe('Table', () => {
});
describe('when mounted with nested data', () => {
it('then correct rows should be rendered and new table is rendered when expander is clicked', async () => {
beforeEach(() => {
const nestedFrame = (idx: number) =>
applyOverrides(
toDataFrame({
@@ -539,6 +539,7 @@ describe('Table', () => {
],
})
);
const defaultFrame = getDefaultDataFrame();
getTestContext({
@@ -555,6 +556,9 @@ describe('Table', () => {
],
}),
});
});
it('then correct rows should be rendered and new table is rendered when expander is clicked', async () => {
expect(getTable()).toBeInTheDocument();
expect(screen.getAllByRole('columnheader')).toHaveLength(4);
expect(getColumnHeader(/time/)).toBeInTheDocument();
@@ -593,5 +597,64 @@ describe('Table', () => {
expect(within(subTableRows1[2]).getByText(/17%_1/)).toBeInTheDocument();
expect(within(subTableRows1[2]).getByText(/humid_1/)).toBeInTheDocument();
});
it('then properly handle row expansion and sorting', async () => {
expect(getTable()).toBeInTheDocument();
expect(screen.getAllByRole('columnheader')).toHaveLength(4);
expect(getColumnHeader(/time/)).toBeInTheDocument();
expect(getColumnHeader(/temperature/)).toBeInTheDocument();
expect(getColumnHeader(/img/)).toBeInTheDocument();
let rows = within(getTable()).getAllByRole('row');
expect(rows).toHaveLength(5);
expect(getRowsData(rows)).toEqual([
{ time: '2021-01-01 00:00:00', temperature: '10', link: '${__value.text} interpolation' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: '${__value.text} interpolation' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '${__value.text} interpolation' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '${__value.text} interpolation' },
]);
// Sort rows, and check the new order
const table = getTable();
await userEvent.click(within(table).getAllByTitle('Toggle SortBy')[0]);
rows = within(table).getAllByRole('row');
expect(rows).toHaveLength(5);
expect(getRowsData(rows)).toEqual([
{ time: '2021-01-01 00:00:00', temperature: '10', link: '${__value.text} interpolation' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '${__value.text} interpolation' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '${__value.text} interpolation' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: '${__value.text} interpolation' },
]);
// No sub table exists before expending a row
let tables = screen.getAllByRole('table');
expect(tables).toHaveLength(1);
// Expand a row, and check its height
rows = within(getTable()).getAllByRole('row');
await userEvent.click(within(rows[1]).getByLabelText('Expand row'));
tables = screen.getAllByRole('table');
expect(tables).toHaveLength(3);
let subTable = screen.getAllByRole('table')[2];
expect(subTable.style.height).toBe('108px');
// Sort again rows
tables = screen.getAllByRole('table');
await userEvent.click(within(tables[0]).getAllByTitle('Toggle SortBy')[0]);
rows = within(table).getAllByRole('row');
expect(rows).toHaveLength(5);
expect(getRowsData(rows)).toEqual([
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: '${__value.text} interpolation' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '${__value.text} interpolation' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '${__value.text} interpolation' },
{ time: '2021-01-01 00:00:00', temperature: '10', link: '${__value.text} interpolation' },
]);
// Expand another row
rows = within(getTable()).getAllByRole('row');
await userEvent.click(within(rows[1]).getByLabelText('Expand row'));
subTable = screen.getAllByRole('table')[2];
expect(subTable.style.height).toBe('108px');
});
});
});
@@ -110,8 +110,26 @@ export const Table = memo((props: Props) => {
[data, width, columnMinWidth, footerItems, hasNestedData, isCountRowsSet]
);
// we need a ref to later store the `toggleAllRowsExpanded` function, returned by `useTable`.
// We cannot simply use a variable because we need to use such function in the initialization of
// `useTableStateReducer`, which is needed to construct options for `useTable` (the hook that returns
// `toggleAllRowsExpanded`), and if we used a variable, that variable would be undefined at the time
// we initialize `useTableStateReducer`.
const toggleAllRowsExpandedRef = useRef<(value?: boolean) => void>();
// Internal react table state reducer
const stateReducer = useTableStateReducer(props);
const stateReducer = useTableStateReducer({
...props,
onSortByChange: (state) => {
// Collapse all rows. This prevents a known bug that causes the size of the rows to be incorrect due to
// using `VariableSizeList` and `useExpanded` together.
toggleAllRowsExpandedRef.current!(false);
if (props.onSortByChange) {
props.onSortByChange(state);
}
},
});
const options: any = useMemo(
() => ({
@@ -142,9 +160,17 @@ export const Table = memo((props: Props) => {
gotoPage,
setPageSize,
pageOptions,
toggleAllRowsExpanded,
} = useTable(options, useFilters, useSortBy, useAbsoluteLayout, useResizeColumns, useExpanded, usePagination);
const extendedState = state as GrafanaTableState;
toggleAllRowsExpandedRef.current = toggleAllRowsExpanded;
const expandedRowsRepr = JSON.stringify(Object.keys(state.expanded));
useEffect(() => {
// Reset the list size cache when the expanded rows change
listRef.current?.resetAfterIndex(0);
}, [expandedRowsRepr]);
/*
Footer value calculation is being moved in the Table component and the footerValues prop will be deprecated.
@@ -213,11 +239,9 @@ export const Table = memo((props: Props) => {
);
const RenderRow = useCallback(
({ index: rowIndex, style }: { index: number; style: CSSProperties }) => {
let row = rows[rowIndex];
if (enablePagination) {
row = page[rowIndex];
}
({ index, style }: { index: number; style: CSSProperties }) => {
const indexForPagination = rowIndexForPagination(index);
const row = rows[indexForPagination];
prepareRow(row);
@@ -230,7 +254,7 @@ export const Table = memo((props: Props) => {
<ExpandedRow
nestedData={nestedDataField}
tableStyles={tableStyles}
rowIndex={row.index}
rowIndex={index}
width={width}
cellHeight={cellHeight}
/>
@@ -251,18 +275,17 @@ export const Table = memo((props: Props) => {
);
},
[
rowIndexForPagination,
rows,
enablePagination,
prepareRow,
state.expanded,
tableStyles,
nestedDataField,
page,
width,
cellHeight,
onCellFilterAdded,
timeRange,
data,
width,
cellHeight,
state.expanded,
]
);
@@ -301,8 +324,9 @@ export const Table = memo((props: Props) => {
const getItemSize = (index: number): number => {
const indexForPagination = rowIndexForPagination(index);
if (state.expanded[indexForPagination] && nestedDataField) {
return getExpandedRowHeight(nestedDataField, indexForPagination, tableStyles);
const row = rows[indexForPagination];
if (state.expanded[row.index] && nestedDataField) {
return getExpandedRowHeight(nestedDataField, index, tableStyles);
}
return tableStyles.rowHeight;
@@ -265,6 +265,7 @@ export { type UserView } from './UsersIndicator/types';
export { InlineFormLabel } from './FormLabel/FormLabel';
export { Divider } from './Divider/Divider';
/** @deprecated Please use non-legacy versions of these components */
const LegacyForms = {
SecretFormField,
FormField,
@@ -5,14 +5,13 @@ import uPlot from 'uplot';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../themes/ThemeContext';
import { useStyles2 } from '../../../themes';
import { UPlotConfigBuilder } from '../config/UPlotConfigBuilder';
import { CloseButton } from './CloseButton';
interface TooltipPlugin2Props {
config: UPlotConfigBuilder;
// or via .children() render prop callback?
render: (
u: uPlot,
dataIdxs: Array<number | null>,
@@ -67,7 +66,7 @@ export const TooltipPlugin2 = ({ config, render }: TooltipPlugin2Props) => {
const sizeRef = useRef<TooltipContainerSize>();
const className = useStyles2(getStyles).tooltipWrapper;
const styles = useStyles2(getStyles);
useLayoutEffect(() => {
sizeRef.current = {
@@ -108,8 +107,9 @@ export const TooltipPlugin2 = ({ config, render }: TooltipPlugin2Props) => {
let closestSeriesIdx: number | null = null;
let pendingRender = false;
let pendingPinned = false;
const scheduleRender = () => {
const scheduleRender = (setPinned = false) => {
if (!pendingRender) {
// defer unrender for 100ms to reduce flickering in small gaps
if (!_isHovering) {
@@ -120,11 +120,43 @@ export const TooltipPlugin2 = ({ config, render }: TooltipPlugin2Props) => {
pendingRender = true;
}
if (setPinned) {
pendingPinned = true;
}
};
// in some ways this is similar to ClickOutsideWrapper.tsx
const downEventOutside = (e: Event) => {
let isOutside = (e.target as HTMLDivElement).closest(`.${styles.tooltipWrapper}`) !== domRef.current;
if (isOutside) {
dismiss();
}
};
const _render = () => {
pendingRender = false;
if (pendingPinned) {
_style = { pointerEvents: _isPinned ? 'all' : 'none' };
domRef.current!.closest<HTMLDivElement>('.react-grid-item')?.classList.toggle('context-menu-open', _isPinned);
// @ts-ignore
_plot!.cursor._lock = _isPinned;
if (_isPinned) {
document.addEventListener('mousedown', downEventOutside, true);
document.addEventListener('keydown', downEventOutside, true);
} else {
document.removeEventListener('mousedown', downEventOutside, true);
document.removeEventListener('keydown', downEventOutside, true);
}
pendingPinned = false;
}
let state: TooltipContainerState = {
style: _style,
isPinned: _isPinned,
@@ -139,35 +171,18 @@ export const TooltipPlugin2 = ({ config, render }: TooltipPlugin2Props) => {
const dismiss = () => {
_isPinned = false;
_isHovering = false;
_style = { pointerEvents: 'none' };
// @ts-ignore
_plot!.cursor._lock = _isPinned;
scheduleRender();
_plot!.setCursor({ left: -10, top: -10 });
scheduleRender(true);
};
config.addHook('init', (u) => {
setState({ plot: (_plot = u) });
// TODO: use cursor.lock & and mousedown/mouseup here (to prevent unlocking)
// this handles pinning
u.over.addEventListener('click', (e) => {
if (_isHovering) {
if (e.target === u.over) {
_isPinned = !_isPinned;
_style = { pointerEvents: _isPinned ? 'all' : 'none' };
scheduleRender();
}
// @ts-ignore
u.cursor._lock = _isPinned;
// hack to trigger cursor to new position after unlock
// (should not be necessary after using the cursor.lock API)
if (!_isPinned) {
u.setCursor({ left: e.clientX - u.rect.left, top: e.clientY - u.rect.top });
_isHovering = false;
}
if (_isHovering && !_isPinned && e.target === u.over) {
_isPinned = true;
scheduleRender(true);
}
});
});
@@ -268,7 +283,7 @@ export const TooltipPlugin2 = ({ config, render }: TooltipPlugin2Props) => {
if (plot && isHovering) {
return createPortal(
<div className={className} style={style} ref={domRef}>
<div className={styles.tooltipWrapper} style={style} ref={domRef}>
{isPinned && <CloseButton onClick={dismiss} style={{ top: '16px' }} />}
{contents}
</div>,
@@ -290,5 +305,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
position: 'absolute',
background: theme.colors.background.secondary,
boxShadow: `0 4px 8px ${theme.colors.background.primary}`,
userSelect: 'text',
}),
});
@@ -11,6 +11,8 @@ interface ZoomPluginProps {
// min px width that triggers zoom
const MIN_ZOOM_DIST = 5;
const maybeZoomAction = (e?: MouseEvent | null) => e != null && !e.ctrlKey && !e.metaKey;
/**
* @alpha
*/
@@ -21,9 +23,13 @@ export const ZoomPlugin = ({ onZoom, config, withZoomY = false }: ZoomPluginProp
if (withZoomY) {
config.addHook('init', (u) => {
u.root!.addEventListener(
u.over!.addEventListener(
'mousedown',
(e) => {
if (!maybeZoomAction(e)) {
return;
}
if (e.button === 0 && e.shiftKey) {
yDrag = true;
@@ -45,6 +51,10 @@ export const ZoomPlugin = ({ onZoom, config, withZoomY = false }: ZoomPluginProp
}
config.addHook('setSelect', (u) => {
if (!maybeZoomAction(u.cursor!.event)) {
return;
}
if (withZoomY && yDrag) {
if (u.select.height >= MIN_ZOOM_DIST) {
for (let key in u.scales!) {
@@ -76,6 +86,10 @@ export const ZoomPlugin = ({ onZoom, config, withZoomY = false }: ZoomPluginProp
config.setCursor({
bind: {
dblclick: (u) => () => {
if (!maybeZoomAction(u.cursor!.event)) {
return null;
}
if (withZoomY && yZoomed) {
for (let key in u.scales!) {
if (key !== 'x') {
+11 -6
View File
@@ -19,17 +19,22 @@ export interface Logger {
/** @internal */
export const createLogger = (name: string): Logger => {
let LOGGIN_ENABLED = false;
let loggingEnabled = false;
if (typeof window !== 'undefined') {
loggingEnabled = window.localStorage.getItem('grafana.debug') === 'true';
}
return {
logger: (id: string, throttle = false, ...t: any[]) => {
if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test' || !LOGGIN_ENABLED) {
if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'test' || !loggingEnabled) {
return;
}
const fn = throttle ? throttledLog : console.log;
fn(`[${name}: ${id}]: `, ...t);
fn(`[${name}: ${id}]:`, ...t);
},
enable: () => (LOGGIN_ENABLED = true),
disable: () => (LOGGIN_ENABLED = false),
isEnabled: () => LOGGIN_ENABLED,
enable: () => (loggingEnabled = true),
disable: () => (loggingEnabled = false),
isEnabled: () => loggingEnabled,
};
};
+79 -4
View File
@@ -7,6 +7,8 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/tsdb/grafanads"
@@ -408,6 +410,76 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{"Admin"},
}
libraryPanelsCreatorRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:library.panels:creator",
DisplayName: "Library panel creator",
Description: "Create library panel in general folder.",
Group: "Library panels",
Permissions: []ac.Permission{
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
{Action: libraryelements.ActionLibraryPanelsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
},
},
Grants: []string{"Editor"},
}
libraryPanelsReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:library.panels:reader",
DisplayName: "Library panel reader",
Description: "Read all library panels.",
Group: "Library panels",
Permissions: []ac.Permission{
{Action: libraryelements.ActionLibraryPanelsRead, Scope: libraryelements.ScopeLibraryPanelsAll},
},
},
Grants: []string{"Admin"},
}
libraryPanelsGeneralReaderRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:library.panels:general.reader",
DisplayName: "Library panel general reader",
Description: "Read all library panels in general folder.",
Group: "Library panels",
Permissions: []ac.Permission{
{Action: libraryelements.ActionLibraryPanelsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
},
},
Grants: []string{"Viewer"},
}
libraryPanelsWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:library.panels:writer",
DisplayName: "Library panel writer",
Group: "Library panels",
Description: "Create, read, write or delete all library panels and their permissions.",
Permissions: ac.ConcatPermissions(libraryPanelsReaderRole.Role.Permissions, []ac.Permission{
{Action: libraryelements.ActionLibraryPanelsWrite, Scope: libraryelements.ScopeLibraryPanelsAll},
{Action: libraryelements.ActionLibraryPanelsDelete, Scope: libraryelements.ScopeLibraryPanelsAll},
{Action: libraryelements.ActionLibraryPanelsCreate, Scope: libraryelements.ScopeLibraryPanelsAll},
}),
},
Grants: []string{"Admin"},
}
libraryPanelsGeneralWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:library.panels:general.writer",
DisplayName: "Library panel general writer",
Group: "Library panels",
Description: "Create, read, write or delete all library panels and their permissions in the general folder.",
Permissions: ac.ConcatPermissions(libraryPanelsGeneralReaderRole.Role.Permissions, []ac.Permission{
{Action: libraryelements.ActionLibraryPanelsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
{Action: libraryelements.ActionLibraryPanelsDelete, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
{Action: libraryelements.ActionLibraryPanelsCreate, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(ac.GeneralFolderUID)},
}),
},
Grants: []string{"Editor"},
}
publicDashboardsWriterRole := ac.RoleRegistration{
Role: ac.RoleDTO{
Name: "fixed:dashboards.public:writer",
@@ -447,15 +519,18 @@ func (hs *HTTPServer) declareFixedRoles() error {
Grants: []string{"Admin"},
}
return hs.accesscontrolService.DeclareFixedRoles(
provisioningWriterRole, datasourcesReaderRole, builtInDatasourceReader, datasourcesWriterRole,
roles := []ac.RoleRegistration{provisioningWriterRole, datasourcesReaderRole, builtInDatasourceReader, datasourcesWriterRole,
datasourcesIdReaderRole, orgReaderRole, orgWriterRole,
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole,
annotationsReaderRole, dashboardAnnotationsWriterRole, annotationsWriterRole,
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyReaderRole, apikeyWriterRole,
publicDashboardsWriterRole, featuremgmtReaderRole, featuremgmtWriterRole,
)
publicDashboardsWriterRole, featuremgmtReaderRole, featuremgmtWriterRole}
if hs.Features.IsEnabled(featuremgmt.FlagLibraryPanelRBAC) {
roles = append(roles, libraryPanelsCreatorRole, libraryPanelsReaderRole, libraryPanelsWriterRole, libraryPanelsGeneralReaderRole, libraryPanelsGeneralWriterRole)
}
return hs.accesscontrolService.DeclareFixedRoles(roles...)
}
// Metadata helpers
+1 -8
View File
@@ -57,13 +57,6 @@ func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Respo
OrgID: form.OrgId,
}
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
return response.Error(400, "Validation error, need specify either username or email", nil)
}
}
if len(cmd.Password) < 4 {
return response.Error(400, "Password is missing or too short", nil)
}
@@ -78,7 +71,7 @@ func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Respo
return response.Error(http.StatusPreconditionFailed, fmt.Sprintf("User with email '%s' or username '%s' already exists", form.Email, form.Login), err)
}
return response.Error(http.StatusInternalServerError, "failed to create user", err)
return response.ErrOrFallback(http.StatusInternalServerError, "failed to create user", err)
}
metrics.MApiAdminUserCreate.Inc()
+7 -25
View File
@@ -124,18 +124,19 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/live/pipeline", reqGrafanaAdmin, hs.Index)
r.Get("/live/cloud", reqGrafanaAdmin, hs.Index)
r.Get("/plugins", middleware.CanAdminPlugins(hs.Cfg), hs.Index)
r.Get("/plugins/:id/", middleware.CanAdminPlugins(hs.Cfg), hs.Index)
r.Get("/plugins/:id/edit", middleware.CanAdminPlugins(hs.Cfg), hs.Index) // deprecated
r.Get("/plugins/:id/page/:page", middleware.CanAdminPlugins(hs.Cfg), hs.Index)
r.Get("/plugins", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
r.Get("/plugins/:id/", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
r.Get("/plugins/:id/edit", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index) // deprecated
r.Get("/plugins/:id/page/:page", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
r.Get("/connections/datasources", authorize(datasources.ConfigurationPageAccess), hs.Index)
r.Get("/connections/datasources/new", authorize(datasources.NewPageAccess), hs.Index)
r.Get("/connections/datasources/edit/*", authorize(datasources.EditPageAccess), hs.Index)
r.Get("/connections", authorize(datasources.ConfigurationPageAccess), hs.Index)
r.Get("/connections/add-new-connection", authorize(datasources.ConfigurationPageAccess), hs.Index)
r.Get("/connections/datasources/:id", middleware.CanAdminPlugins(hs.Cfg), hs.Index)
r.Get("/connections/datasources/:id/page/:page", middleware.CanAdminPlugins(hs.Cfg), hs.Index)
// Plugin details pages
r.Get("/connections/datasources/:id", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
r.Get("/connections/datasources/:id/page/:page", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
// App Root Page
appPluginIDScope := pluginaccesscontrol.ScopeProvider.GetResourceScope(ac.Parameter(":id"))
@@ -270,25 +271,6 @@ func (hs *HTTPServer) registerRoutes() {
usersRoute.Post("/:id/using/:orgId", authorize(ac.EvalPermission(ac.ActionUsersWrite, userIDScope)), routing.Wrap(hs.UpdateUserActiveOrg))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// team (admin permission required)
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Post("/", authorize(ac.EvalPermission(ac.ActionTeamsCreate)), routing.Wrap(hs.CreateTeam))
teamsRoute.Put("/:teamId", authorize(ac.EvalPermission(ac.ActionTeamsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.UpdateTeam))
teamsRoute.Delete("/:teamId", authorize(ac.EvalPermission(ac.ActionTeamsDelete, ac.ScopeTeamsID)), routing.Wrap(hs.DeleteTeamByID))
teamsRoute.Get("/:teamId/members", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsRead, ac.ScopeTeamsID)), routing.Wrap(hs.GetTeamMembers))
teamsRoute.Post("/:teamId/members", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.AddTeamMember))
teamsRoute.Put("/:teamId/members/:userId", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.UpdateTeamMember))
teamsRoute.Delete("/:teamId/members/:userId", authorize(ac.EvalPermission(ac.ActionTeamsPermissionsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.RemoveTeamMember))
teamsRoute.Get("/:teamId/preferences", authorize(ac.EvalPermission(ac.ActionTeamsRead, ac.ScopeTeamsID)), routing.Wrap(hs.GetTeamPreferences))
teamsRoute.Put("/:teamId/preferences", authorize(ac.EvalPermission(ac.ActionTeamsWrite, ac.ScopeTeamsID)), routing.Wrap(hs.UpdateTeamPreferences))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// team without requirement of user to be org admin
apiRoute.Group("/teams", func(teamsRoute routing.RouteRegister) {
teamsRoute.Get("/:teamId", authorize(ac.EvalPermission(ac.ActionTeamsRead, ac.ScopeTeamsID)), routing.Wrap(hs.GetTeamByID))
teamsRoute.Get("/search", authorize(ac.EvalPermission(ac.ActionTeamsRead)), routing.Wrap(hs.SearchTeams))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// org information available to all users.
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/", authorize(ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.GetCurrentOrg))
-1
View File
@@ -214,7 +214,6 @@ func setupScenarioContext(t *testing.T, url string) *scenarioContext {
return sc
}
// FIXME: This user should not be anonymous
func authedUserWithPermissions(userID, orgID int64, permissions []accesscontrol.Permission) *user.SignedInUser {
return &user.SignedInUser{UserID: userID, OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(permissions)}}
}
+1 -3
View File
@@ -170,7 +170,6 @@ type HTTPServer struct {
queryDataService query.Service
serviceAccountsService serviceaccounts.Service
authInfoService login.AuthInfoService
teamPermissionsService accesscontrol.TeamPermissionsService
NotificationService *notifications.NotificationService
DashboardService dashboards.DashboardService
dashboardProvisioningService dashboards.DashboardProvisioningService
@@ -236,7 +235,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dsGuardian guardian.DatasourceGuardianProvider, alertNotificationService *alerting.AlertNotificationService,
dashboardsnapshotsService dashboardsnapshots.Service, pluginSettings pluginSettings.Service,
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, basekinds *corekind.Base,
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore,
@@ -319,7 +318,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardProvisioningService: dashboardProvisioningService,
folderService: folderService,
dsGuardian: dsGuardian,
teamPermissionsService: teamsPermissionsService,
AlertNotificationService: alertNotificationService,
dashboardsnapshotsService: dashboardsnapshotsService,
PluginSettings: pluginSettings,
+1 -1
View File
@@ -166,7 +166,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
hs.HooksService.RunIndexDataHooks(&data, c)
data.NavTree.ApplyAdminIA()
data.NavTree.ApplyAdminIA(hs.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagNavAdminSubsections))
data.NavTree.Sort()
return &data, nil
+6 -94
View File
@@ -11,6 +11,7 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/dashboards"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/preference/prefapi"
"github.com/grafana/grafana/pkg/web"
)
@@ -68,56 +69,7 @@ func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Re
return response.Error(http.StatusInternalServerError, "Failed to get user preferences", errID)
}
return hs.getPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), userID, 0)
}
func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, teamID int64) response.Response {
prefsQuery := pref.GetPreferenceQuery{UserID: userID, OrgID: orgID, TeamID: teamID}
preference, err := hs.preferenceService.Get(ctx, &prefsQuery)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to get preferences", err)
}
var dashboardUID string
// when homedashboardID is 0, that means it is the default home dashboard, no UID would be returned in the response
if preference.HomeDashboardID != 0 {
query := dashboards.GetDashboardQuery{ID: preference.HomeDashboardID, OrgID: orgID}
queryResult, err := hs.DashboardService.GetDashboard(ctx, &query)
if err == nil {
dashboardUID = queryResult.UID
}
}
dto := preferences.Spec{}
if preference.WeekStart != nil && *preference.WeekStart != "" {
dto.WeekStart = preference.WeekStart
}
if preference.Theme != "" {
dto.Theme = &preference.Theme
}
if dashboardUID != "" {
dto.HomeDashboardUID = &dashboardUID
}
if preference.Timezone != "" {
dto.Timezone = &preference.Timezone
}
if preference.JSONData != nil {
if preference.JSONData.Language != "" {
dto.Language = &preference.JSONData.Language
}
if preference.JSONData.QueryHistory.HomeTab != "" {
dto.QueryHistory = &preferences.QueryHistoryPreference{
HomeTab: &preference.JSONData.QueryHistory.HomeTab,
}
}
}
return response.JSON(http.StatusOK, &dto)
return prefapi.GetPreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, c.SignedInUser.GetOrgID(), userID, 0)
}
// swagger:route PUT /user/preferences user_preferences updateUserPreferences
@@ -142,48 +94,8 @@ func (hs *HTTPServer) UpdateUserPreferences(c *contextmodel.ReqContext) response
return response.Error(http.StatusInternalServerError, "Failed to update user preferences", errID)
}
return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), userID, 0, &dtoCmd)
}
func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) response.Response {
if dtoCmd.Theme != "" && !pref.IsValidThemeID(dtoCmd.Theme) {
return response.Error(http.StatusBadRequest, "Invalid theme", nil)
}
dashboardID := dtoCmd.HomeDashboardID
if dtoCmd.HomeDashboardUID != nil {
query := dashboards.GetDashboardQuery{UID: *dtoCmd.HomeDashboardUID, OrgID: orgID}
if query.UID == "" {
// clear the value
dashboardID = 0
} else {
queryResult, err := hs.DashboardService.GetDashboard(ctx, &query)
if err != nil {
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
dashboardID = queryResult.ID
}
}
dtoCmd.HomeDashboardID = dashboardID
saveCmd := pref.SavePreferenceCommand{
UserID: userID,
OrgID: orgID,
TeamID: teamId,
Theme: dtoCmd.Theme,
Language: dtoCmd.Language,
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
QueryHistory: dtoCmd.QueryHistory,
CookiePreferences: dtoCmd.Cookies,
}
if err := hs.preferenceService.Save(ctx, &saveCmd); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save preferences", err)
}
return response.Success("Preferences updated")
return prefapi.UpdatePreferencesFor(c.Req.Context(), hs.DashboardService,
hs.preferenceService, c.SignedInUser.GetOrgID(), userID, 0, &dtoCmd)
}
// swagger:route PATCH /user/preferences user_preferences patchUserPreferences
@@ -262,7 +174,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) GetOrgPreferences(c *contextmodel.ReqContext) response.Response {
return hs.getPreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, 0)
return prefapi.GetPreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, c.SignedInUser.GetOrgID(), 0, 0)
}
// swagger:route PUT /org/preferences org_preferences updateOrgPreferences
@@ -281,7 +193,7 @@ func (hs *HTTPServer) UpdateOrgPreferences(c *contextmodel.ReqContext) response.
return response.Error(http.StatusBadRequest, "bad request data", err)
}
return hs.updatePreferencesFor(c.Req.Context(), c.SignedInUser.GetOrgID(), 0, 0, &dtoCmd)
return prefapi.UpdatePreferencesFor(c.Req.Context(), hs.DashboardService, hs.preferenceService, c.SignedInUser.GetOrgID(), 0, 0, &dtoCmd)
}
// swagger:route PATCH /org/preferences org_preferences patchOrgPreferences
+1 -8
View File
@@ -221,18 +221,11 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC
return response.Error(http.StatusForbidden, "User info cannot be updated for external Users", nil)
}
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email
if len(cmd.Login) == 0 {
return response.Error(http.StatusBadRequest, "Validation error, need to specify either username or email", nil)
}
}
if err := hs.userService.Update(ctx, &cmd); err != nil {
if errors.Is(err, user.ErrCaseInsensitive) {
return response.Error(http.StatusConflict, "Update would result in user login conflict", err)
}
return response.Error(http.StatusInternalServerError, "Failed to update user", err)
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update user", err)
}
return response.Success("User updated")
+1 -1
View File
@@ -225,7 +225,7 @@ func (s *Service) buildGraph(req *Request) (*simple.DirectedGraph, error) {
case TypeDatasourceNode:
node, err = s.buildDSNode(dp, rn, req)
case TypeCMDNode:
node, err = buildCMDNode(dp, rn)
node, err = buildCMDNode(rn, s.features)
case TypeMLNode:
if s.features.IsEnabled(featuremgmt.FlagMlExpressions) {
node, err = s.buildMLNode(dp, rn, req)
+122
View File
@@ -0,0 +1,122 @@
package expr
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/infra/tracing"
)
type Fingerprints map[data.Fingerprint]struct{}
// HysteresisCommand is a special case of ThresholdCommand that encapsulates two thresholds that are applied depending on the results of the previous evaluations:
// - first threshold - "loading", is used when the metric is determined as not loaded, i.e. it does not exist in the data provided by the reader.
// - second threshold - "unloading", is used when the metric is determined as loaded.
// To determine whether a metric is loaded, the command uses LoadedDimensions that is supposed to contain data.Fingerprint of
// the metrics that were loaded during the previous evaluation.
// The result of the execution of the command is the same as ThresholdCommand: 0 or 1 for each metric.
type HysteresisCommand struct {
RefID string
ReferenceVar string
LoadingThresholdFunc ThresholdCommand
UnloadingThresholdFunc ThresholdCommand
LoadedDimensions Fingerprints
}
func (h *HysteresisCommand) NeedsVars() []string {
return []string{h.ReferenceVar}
}
func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) {
results := vars[h.ReferenceVar]
// shortcut for NoData
if results.IsNoData() {
return mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil
}
if h.LoadedDimensions == nil || len(h.LoadedDimensions) == 0 {
return h.LoadingThresholdFunc.Execute(ctx, now, vars, tracer)
}
var loadedVals, unloadedVals mathexp.Values
for _, value := range results.Values {
_, ok := h.LoadedDimensions[value.GetLabels().Fingerprint()]
if ok {
loadedVals = append(loadedVals, value)
} else {
unloadedVals = append(unloadedVals, value)
}
}
if len(loadedVals) == 0 { // if all values are unloaded
return h.LoadingThresholdFunc.Execute(ctx, now, vars, tracer)
}
if len(unloadedVals) == 0 { // if all values are loaded
return h.UnloadingThresholdFunc.Execute(ctx, now, vars, tracer)
}
defer func() {
// return back the old values
vars[h.ReferenceVar] = results
}()
vars[h.ReferenceVar] = mathexp.Results{Values: unloadedVals}
loadingResults, err := h.LoadingThresholdFunc.Execute(ctx, now, vars, tracer)
if err != nil {
return mathexp.Results{}, fmt.Errorf("failed to execute loading threshold: %w", err)
}
vars[h.ReferenceVar] = mathexp.Results{Values: loadedVals}
unloadingResults, err := h.UnloadingThresholdFunc.Execute(ctx, now, vars, tracer)
if err != nil {
return mathexp.Results{}, fmt.Errorf("failed to execute unloading threshold: %w", err)
}
return mathexp.Results{Values: append(loadingResults.Values, unloadingResults.Values...)}, nil
}
func NewHysteresisCommand(refID string, referenceVar string, loadCondition ThresholdCommand, unloadCondition ThresholdCommand, l Fingerprints) (*HysteresisCommand, error) {
return &HysteresisCommand{
RefID: refID,
LoadingThresholdFunc: loadCondition,
UnloadingThresholdFunc: unloadCondition,
ReferenceVar: referenceVar,
LoadedDimensions: l,
}, nil
}
// FingerprintsFromFrame converts data.Frame to Fingerprints.
// The input data frame must have a single field of uint64 type.
// Returns error if the input data frame has invalid format
func FingerprintsFromFrame(frame *data.Frame) (Fingerprints, error) {
frameType, frameVersion := frame.TypeInfo("")
if frameType != "fingerprints" {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: expected frame type 'fingerprints'")
}
if frameVersion.Greater(data.FrameTypeVersion{1, 0}) {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: expected frame type 'fingerprints' of version 1.0 or lower")
}
if len(frame.Fields) != 1 {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: expected a single field but got %d", len(frame.Fields))
}
fld := frame.Fields[0]
if fld.Type() != data.FieldTypeUint64 {
return nil, fmt.Errorf("invalid format of loaded dimensions frame: the field type must be uint64 but got %s", fld.Type().String())
}
result := make(Fingerprints, fld.Len())
for i := 0; i < fld.Len(); i++ {
val, ok := fld.ConcreteAt(i)
if !ok {
continue
}
switch v := val.(type) {
case uint64:
result[data.Fingerprint(v)] = struct{}{}
default:
return nil, fmt.Errorf("cannot read the value at index [%d], expected uint64 but got '%T'", i, val)
}
}
return result, nil
}
+188
View File
@@ -0,0 +1,188 @@
package expr
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/infra/tracing"
)
func TestHysteresisExecute(t *testing.T) {
number := func(label string, value float64) mathexp.Number {
n := mathexp.NewNumber("A", data.Labels{"label": label})
n.SetValue(&value)
return n
}
fingerprint := func(label string) data.Fingerprint {
return data.Labels{"label": label}.Fingerprint()
}
tracer := tracing.InitializeTracerForTest()
var loadThreshold = 100.0
var unloadThreshold = 30.0
testCases := []struct {
name string
loadedDimensions Fingerprints
input mathexp.Values
expected mathexp.Values
expectedError error
}{
{
name: "return NoData when no data",
loadedDimensions: Fingerprints{0: struct{}{}},
input: mathexp.Values{mathexp.NewNoData()},
expected: mathexp.Values{mathexp.NewNoData()},
},
{
name: "use only loaded condition if no loaded metrics",
loadedDimensions: Fingerprints{},
input: mathexp.Values{
number("value1", loadThreshold+1),
number("value2", loadThreshold),
number("value3", loadThreshold-1),
number("value4", unloadThreshold+1),
number("value5", unloadThreshold),
number("value6", unloadThreshold-1),
},
expected: mathexp.Values{
number("value1", 1),
number("value2", 0),
number("value3", 0),
number("value4", 0),
number("value5", 0),
number("value6", 0),
},
},
{
name: "evaluate loaded metrics against unloaded threshold",
loadedDimensions: Fingerprints{
fingerprint("value4"): {},
fingerprint("value5"): {},
fingerprint("value6"): {},
},
input: mathexp.Values{
number("value1", loadThreshold+1),
number("value2", loadThreshold),
number("value3", loadThreshold-1),
number("value4", unloadThreshold+1),
number("value5", unloadThreshold),
number("value6", unloadThreshold-1),
},
expected: mathexp.Values{
number("value1", 1),
number("value2", 0),
number("value3", 0),
number("value4", 1),
number("value5", 0),
number("value6", 0),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cmd := &HysteresisCommand{
RefID: "B",
ReferenceVar: "A",
LoadingThresholdFunc: ThresholdCommand{
ReferenceVar: "A",
RefID: "B",
ThresholdFunc: ThresholdIsAbove,
Conditions: []float64{loadThreshold},
},
UnloadingThresholdFunc: ThresholdCommand{
ReferenceVar: "A",
RefID: "B",
ThresholdFunc: ThresholdIsAbove,
Conditions: []float64{unloadThreshold},
},
LoadedDimensions: tc.loadedDimensions,
}
result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{
"A": mathexp.Results{Values: tc.input},
}, tracer)
if tc.expectedError != nil {
require.ErrorIs(t, err, tc.expectedError)
return
}
require.NoError(t, err)
require.EqualValues(t, result.Values, tc.expected)
})
}
}
func TestLoadedDimensionsFromFrame(t *testing.T) {
correctType := &data.FrameMeta{Type: "fingerprints", TypeVersion: data.FrameTypeVersion{1, 0}}
testCases := []struct {
name string
frame *data.Frame
expected Fingerprints
expectedError bool
}{
{
name: "should fail if frame has wrong type",
frame: data.NewFrame("test").SetMeta(&data.FrameMeta{Type: "test"}),
expectedError: true,
},
{
name: "should fail if frame has unsupported version",
frame: data.NewFrame("test").SetMeta(&data.FrameMeta{Type: "fingerprints", TypeVersion: data.FrameTypeVersion{1, 1}}),
expectedError: true,
},
{
name: "should fail if frame has no fields",
frame: data.NewFrame("test").SetMeta(correctType),
expectedError: true,
},
{
name: "should fail if frame has many fields",
frame: data.NewFrame("test",
data.NewField("fingerprints", nil, []uint64{}),
data.NewField("test", nil, []string{}),
).SetMeta(correctType),
expectedError: true,
},
{
name: "should fail if frame has field of a wrong type",
frame: data.NewFrame("test",
data.NewField("fingerprints", nil, []int64{}),
).SetMeta(correctType),
expectedError: true,
},
{
name: "should fail if frame has nullable uint64 field",
frame: data.NewFrame("test",
data.NewField("fingerprints", nil, []*uint64{}),
).SetMeta(correctType),
expectedError: true,
},
{
name: "should create LoadedMetrics",
frame: data.NewFrame("test",
data.NewField("fingerprints", nil, []uint64{1, 2, 3, 4, 5}),
).SetMeta(correctType),
expected: Fingerprints{1: {}, 2: {}, 3: {}, 4: {}, 5: {}},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result, err := FingerprintsFromFrame(testCase.frame)
if testCase.expectedError {
require.Error(t, err)
} else {
require.EqualValues(t, testCase.expected, result)
b, _ := json.Marshal(testCase.frame)
t.Log(string(b))
}
})
}
}
+5
View File
@@ -13,6 +13,11 @@ type Results struct {
Error error
}
// IsNoData checks whether the result contains NoData value
func (r Results) IsNoData() bool {
return len(r.Values) == 0 || len(r.Values) == 1 && r.Values[0].Type() == parse.TypeNoData
}
// Values is a slice of Value interfaces
type Values []Value
+2 -2
View File
@@ -96,7 +96,7 @@ func (gn *CMDNode) Execute(ctx context.Context, now time.Time, vars mathexp.Vars
return gn.Command.Execute(ctx, now, vars, s.tracer)
}
func buildCMDNode(dp *simple.DirectedGraph, rn *rawNode) (*CMDNode, error) {
func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles) (*CMDNode, error) {
commandType, err := rn.GetCommandType()
if err != nil {
return nil, fmt.Errorf("invalid command type in expression '%v': %w", rn.RefID, err)
@@ -120,7 +120,7 @@ func buildCMDNode(dp *simple.DirectedGraph, rn *rawNode) (*CMDNode, error) {
case TypeClassicConditions:
node.Command, err = classic.UnmarshalConditionsCmd(rn.Query, rn.RefID)
case TypeThreshold:
node.Command, err = UnmarshalThresholdCommand(rn)
node.Command, err = UnmarshalThresholdCommand(rn, toggles)
default:
return nil, fmt.Errorf("expression command type '%v' in expression '%v' not implemented", commandType, rn.RefID)
}
+58 -37
View File
@@ -7,8 +7,11 @@ import (
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
type ThresholdCommand struct {
@@ -16,6 +19,7 @@ type ThresholdCommand struct {
RefID string
ThresholdFunc string
Conditions []float64
Invert bool
}
const (
@@ -39,6 +43,8 @@ func NewThresholdCommand(refID, referenceVar, thresholdFunc string, conditions [
if len(conditions) < 1 {
return nil, fmt.Errorf("incorrect number of arguments: got %d but need 1", len(conditions))
}
default:
return nil, fmt.Errorf("expected threshold function to be one of [%s], got %s", strings.Join(supportedThresholdFuncs, ", "), thresholdFunc)
}
return &ThresholdCommand{
@@ -49,50 +55,48 @@ func NewThresholdCommand(refID, referenceVar, thresholdFunc string, conditions [
}, nil
}
type ThresholdConditionJSON struct {
Evaluator ConditionEvalJSON `json:"evaluator"`
}
type ConditionEvalJSON struct {
Params []float64 `json:"params"`
Type string `json:"type"` // e.g. "gt"
}
// UnmarshalResampleCommand creates a ResampleCMD from Grafana's frontend query.
func UnmarshalThresholdCommand(rn *rawNode) (*ThresholdCommand, error) {
rawQuery := rn.Query
rawExpression, ok := rawQuery["expression"]
if !ok {
func UnmarshalThresholdCommand(rn *rawNode, features featuremgmt.FeatureToggles) (Command, error) {
cmdConfig := ThresholdCommandConfig{}
if err := json.Unmarshal(rn.QueryRaw, &cmdConfig); err != nil {
return nil, fmt.Errorf("failed to parse the threshold command: %w", err)
}
if cmdConfig.Expression == "" {
return nil, fmt.Errorf("no variable specified to reference for refId %v", rn.RefID)
}
referenceVar, ok := rawExpression.(string)
if !ok {
return nil, fmt.Errorf("expected threshold variable to be a string, got %T for refId %v", rawExpression, rn.RefID)
}
jsonFromM, err := json.Marshal(rawQuery["conditions"])
if err != nil {
return nil, fmt.Errorf("failed to remarshal threshold expression body: %w", err)
}
var conditions []ThresholdConditionJSON
if err = json.Unmarshal(jsonFromM, &conditions); err != nil {
return nil, fmt.Errorf("failed to unmarshal remarshaled threshold expression body: %w", err)
}
for _, condition := range conditions {
if !IsSupportedThresholdFunc(condition.Evaluator.Type) {
return nil, fmt.Errorf("expected threshold function to be one of %s, got %s", strings.Join(supportedThresholdFuncs, ", "), condition.Evaluator.Type)
}
}
referenceVar := cmdConfig.Expression
// we only support one condition for now, we might want to turn this in to "OR" expressions later
if len(conditions) != 1 {
if len(cmdConfig.Conditions) != 1 {
return nil, fmt.Errorf("threshold expression requires exactly one condition")
}
firstCondition := conditions[0]
firstCondition := cmdConfig.Conditions[0]
return NewThresholdCommand(rn.RefID, referenceVar, firstCondition.Evaluator.Type, firstCondition.Evaluator.Params)
threshold, err := NewThresholdCommand(rn.RefID, referenceVar, firstCondition.Evaluator.Type, firstCondition.Evaluator.Params)
if err != nil {
return nil, fmt.Errorf("invalid condition: %w", err)
}
if firstCondition.UnloadEvaluator != nil && features.IsEnabled(featuremgmt.FlagRecoveryThreshold) {
unloading, err := NewThresholdCommand(rn.RefID, referenceVar, firstCondition.UnloadEvaluator.Type, firstCondition.UnloadEvaluator.Params)
unloading.Invert = true
if err != nil {
return nil, fmt.Errorf("invalid unloadCondition: %w", err)
}
var d Fingerprints
if firstCondition.LoadedDimensions != nil {
d, err = FingerprintsFromFrame(firstCondition.LoadedDimensions)
if err != nil {
return nil, fmt.Errorf("failed to parse loaded dimensions: %w", err)
}
}
return NewHysteresisCommand(rn.RefID, referenceVar, *threshold, *unloading, d)
}
return threshold, nil
}
// NeedsVars returns the variable names (refIds) that are dependencies
@@ -102,7 +106,7 @@ func (tc *ThresholdCommand) NeedsVars() []string {
}
func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) {
mathExpression, err := createMathExpression(tc.ReferenceVar, tc.ThresholdFunc, tc.Conditions)
mathExpression, err := createMathExpression(tc.ReferenceVar, tc.ThresholdFunc, tc.Conditions, tc.Invert)
if err != nil {
return mathexp.Results{}, err
}
@@ -116,19 +120,25 @@ func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mat
}
// createMathExpression converts all the info we have about a "threshold" expression in to a Math expression
func createMathExpression(referenceVar string, thresholdFunc string, args []float64) (string, error) {
func createMathExpression(referenceVar string, thresholdFunc string, args []float64, invert bool) (string, error) {
var exp string
switch thresholdFunc {
case ThresholdIsAbove:
return fmt.Sprintf("${%s} > %f", referenceVar, args[0]), nil
exp = fmt.Sprintf("${%s} > %f", referenceVar, args[0])
case ThresholdIsBelow:
return fmt.Sprintf("${%s} < %f", referenceVar, args[0]), nil
exp = fmt.Sprintf("${%s} < %f", referenceVar, args[0])
case ThresholdIsWithinRange:
return fmt.Sprintf("${%s} > %f && ${%s} < %f", referenceVar, args[0], referenceVar, args[1]), nil
exp = fmt.Sprintf("${%s} > %f && ${%s} < %f", referenceVar, args[0], referenceVar, args[1])
case ThresholdIsOutsideRange:
return fmt.Sprintf("${%s} < %f || ${%s} > %f", referenceVar, args[0], referenceVar, args[1]), nil
exp = fmt.Sprintf("${%s} < %f || ${%s} > %f", referenceVar, args[0], referenceVar, args[1])
default:
return "", fmt.Errorf("failed to evaluate threshold expression: no such threshold function %s", thresholdFunc)
}
if invert {
return fmt.Sprintf("!(%s)", exp), nil
}
return exp, nil
}
func IsSupportedThresholdFunc(name string) bool {
@@ -142,3 +152,14 @@ func IsSupportedThresholdFunc(name string) bool {
return isSupported
}
type ThresholdCommandConfig struct {
Expression string `json:"expression"`
Conditions []ThresholdConditionJSON `json:"conditions"`
}
type ThresholdConditionJSON struct {
Evaluator ConditionEvalJSON `json:"evaluator"`
UnloadEvaluator *ConditionEvalJSON `json:"unloadEvaluator"`
LoadedDimensions *data.Frame `json:"loadedDimensions"`
}
+93 -35
View File
@@ -2,9 +2,13 @@ package expr
import (
"encoding/json"
"fmt"
"sort"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
)
func TestNewThresholdCommand(t *testing.T) {
@@ -82,6 +86,7 @@ func TestUnmarshalThresholdCommand(t *testing.T) {
query string
shouldError bool
expectedError string
assert func(*testing.T, Command)
}
cases := []testCase{
@@ -97,7 +102,13 @@ func TestUnmarshalThresholdCommand(t *testing.T) {
}
}]
}`,
shouldError: false,
assert: func(t *testing.T, command Command) {
require.IsType(t, &ThresholdCommand{}, command)
cmd := command.(*ThresholdCommand)
require.Equal(t, []string{"A"}, cmd.NeedsVars())
require.Equal(t, "gt", cmd.ThresholdFunc)
require.Equal(t, []float64{20.0, 80.0}, cmd.Conditions)
},
},
{
description: "unmarshal with missing conditions should error",
@@ -107,17 +118,7 @@ func TestUnmarshalThresholdCommand(t *testing.T) {
"conditions": []
}`,
shouldError: true,
expectedError: "requires exactly one condition",
},
{
description: "unmarshal with missing conditions should error",
query: `{
"expression" : "A",
"type": "threshold",
"conditions": []
}`,
shouldError: true,
expectedError: "requires exactly one condition",
expectedError: "threshold expression requires exactly one condition",
},
{
description: "unmarshal with unsupported threshold function",
@@ -141,37 +142,86 @@ func TestUnmarshalThresholdCommand(t *testing.T) {
"type": "threshold",
"conditions": []
}`,
shouldError: true,
expectedError: "expected threshold variable to be a string",
shouldError: true,
},
{
description: "unmarshal as hysteresis command if two evaluators",
query: `{
"expression": "B",
"conditions": [
{
"evaluator": {
"params": [
100
],
"type": "gt"
},
"unloadEvaluator": {
"params": [
31
],
"type": "lt"
},
"loadedDimensions": {"schema":{"name":"test","meta":{"type":"fingerprints","typeVersion":[1,0]},"fields":[{"name":"fingerprints","type":"number","typeInfo":{"frame":"uint64"}}]},"data":{"values":[[1,2,3,4,5]]}}
}
]
}`,
assert: func(t *testing.T, c Command) {
require.IsType(t, &HysteresisCommand{}, c)
cmd := c.(*HysteresisCommand)
require.Equal(t, []string{"B"}, cmd.NeedsVars())
require.Equal(t, []string{"B"}, cmd.LoadingThresholdFunc.NeedsVars())
require.Equal(t, "gt", cmd.LoadingThresholdFunc.ThresholdFunc)
require.Equal(t, []float64{100.0}, cmd.LoadingThresholdFunc.Conditions)
require.Equal(t, []string{"B"}, cmd.UnloadingThresholdFunc.NeedsVars())
require.Equal(t, "lt", cmd.UnloadingThresholdFunc.ThresholdFunc)
require.Equal(t, []float64{31.0}, cmd.UnloadingThresholdFunc.Conditions)
require.True(t, cmd.UnloadingThresholdFunc.Invert)
require.NotNil(t, cmd.LoadedDimensions)
actual := make([]uint64, 0, len(cmd.LoadedDimensions))
for fingerprint := range cmd.LoadedDimensions {
actual = append(actual, uint64(fingerprint))
}
sort.Slice(actual, func(i, j int) bool {
return actual[i] < actual[j]
})
require.EqualValues(t, []uint64{1, 2, 3, 4, 5}, actual)
},
},
}
for _, tc := range cases {
q := []byte(tc.query)
t.Run(tc.description, func(t *testing.T) {
q := []byte(tc.query)
var qmap = make(map[string]any)
require.NoError(t, json.Unmarshal(q, &qmap))
var qmap = make(map[string]any)
require.NoError(t, json.Unmarshal(q, &qmap))
cmd, err := UnmarshalThresholdCommand(&rawNode{
RefID: "",
Query: qmap,
QueryRaw: []byte(tc.query),
QueryType: "",
DataSource: nil,
}, featuremgmt.WithFeatures(featuremgmt.FlagRecoveryThreshold))
cmd, err := UnmarshalThresholdCommand(&rawNode{
RefID: "",
Query: qmap,
QueryType: "",
DataSource: nil,
if tc.shouldError {
require.Nil(t, cmd)
require.NotNil(t, err)
require.Contains(t, err.Error(), tc.expectedError)
} else {
require.Nil(t, err)
require.NotNil(t, cmd)
if tc.assert != nil {
tc.assert(t, cmd)
}
}
})
if tc.shouldError {
require.Nil(t, cmd)
require.NotNil(t, err)
require.Contains(t, err.Error(), tc.expectedError)
} else {
require.Nil(t, err)
require.NotNil(t, cmd)
}
}
}
func TestThresholdCommandVars(t *testing.T) {
cmd, err := NewThresholdCommand("B", "A", "is_above", []float64{})
cmd, err := NewThresholdCommand("B", "A", "lt", []float64{1.0})
require.Nil(t, err)
require.Equal(t, cmd.NeedsVars(), []string{"A"})
}
@@ -219,17 +269,25 @@ func TestCreateMathExpression(t *testing.T) {
for _, tc := range cases {
t.Run(tc.description, func(t *testing.T) {
expr, err := createMathExpression(tc.ref, tc.function, tc.params)
expr, err := createMathExpression(tc.ref, tc.function, tc.params, false)
require.Nil(t, err)
require.NotNil(t, expr)
require.Equal(t, expr, tc.expected)
require.Equal(t, tc.expected, expr)
t.Run("inverted", func(t *testing.T) {
expr, err := createMathExpression(tc.ref, tc.function, tc.params, true)
require.Nil(t, err)
require.NotNil(t, expr)
require.Equal(t, fmt.Sprintf("!(%s)", tc.expected), expr)
})
})
}
t.Run("should error if function is unsupported", func(t *testing.T) {
expr, err := createMathExpression("A", "foo", []float64{0})
expr, err := createMathExpression("A", "foo", []float64{0}, false)
require.Equal(t, expr, "")
require.NotNil(t, err)
require.Contains(t, err.Error(), "no such threshold function")
+6 -6
View File
@@ -529,7 +529,7 @@ type Panel struct {
// The data model used in Grafana, namely the data frame, is a columnar-oriented table structure that unifies both time series and table query results.
// Each column within this structure is called a field. A field can represent a single time series or table column.
// Field options allow you to change how the data is displayed in your visualizations.
FieldConfig FieldConfigSource `json:"fieldConfig"`
FieldConfig *FieldConfigSource `json:"fieldConfig,omitempty"`
// Position and dimensions of a panel in the grid
GridPos *GridPos `json:"gridPos,omitempty"`
@@ -562,7 +562,7 @@ type Panel struct {
MaxPerRow *float32 `json:"maxPerRow,omitempty"`
// It depends on the panel plugin. They are specified by the Options field in panel plugin schemas.
Options map[string]any `json:"options"`
Options map[string]any `json:"options,omitempty"`
// The version of the plugin that is used for this panel. This is used to find the plugin to display the panel and to migrate old panel configs.
PluginVersion *string `json:"pluginVersion,omitempty"`
@@ -602,10 +602,10 @@ type Panel struct {
// List of transformations that are applied to the panel data before rendering.
// When there are multiple transformations, Grafana applies them in the order they are listed.
// Each transformation creates a result set that then passes on to the next transformation in the processing pipeline.
Transformations []DataTransformerConfig `json:"transformations"`
Transformations []DataTransformerConfig `json:"transformations,omitempty"`
// Whether to display the panel without a background.
Transparent bool `json:"transparent"`
Transparent *bool `json:"transparent,omitempty"`
// The panel plugin type id. This is used to find the plugin to display the panel.
Type string `json:"type"`
@@ -734,7 +734,7 @@ type Spec struct {
Description *string `json:"description,omitempty"`
// Whether a dashboard is editable or not.
Editable bool `json:"editable"`
Editable *bool `json:"editable,omitempty"`
// The month that the fiscal year starts on. 0 = January, 11 = December
FiscalYearStartMonth *int `json:"fiscalYearStartMonth,omitempty"`
@@ -745,7 +745,7 @@ type Spec struct {
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.
GraphTooltip CursorSync `json:"graphTooltip"`
GraphTooltip *CursorSync `json:"graphTooltip,omitempty"`
// Unique numeric identifier for the dashboard.
// `id` is internal to a specific Grafana instance. `uid` should be used to identify a dashboard across Grafana instances.

Some files were not shown because too many files have changed in this diff Show More