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:
+33
-40
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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` |
|
||||
|
||||
+5
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
|
||||
+4
-4
@@ -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.
|
||||
|
||||
+65
-10
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ?? '',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>.*)$/,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'> {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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)}}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user