mirror of
https://github.com/grafana/grafana.git
synced 2025-12-22 20:54:34 +08:00
Compare commits
99 Commits
sriram/pos
...
v9.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2386d78193 | ||
|
|
fac483c393 | ||
|
|
fd67ab151d | ||
|
|
27a6d700f8 | ||
|
|
fcd658359c | ||
|
|
3d99babd0f | ||
|
|
1ffc7860e6 | ||
|
|
955b931756 | ||
|
|
afb449e8a1 | ||
|
|
3bf22fcb21 | ||
|
|
8ad6df8266 | ||
|
|
a6370342b0 | ||
|
|
6895e75f70 | ||
|
|
4f826bc76c | ||
|
|
efaf4f5e09 | ||
|
|
907381ed5c | ||
|
|
8c9cb8e839 | ||
|
|
028cc7e72b | ||
|
|
00555606d5 | ||
|
|
9cf5b4eb9e | ||
|
|
8637518540 | ||
|
|
0c27df8b8c | ||
|
|
8fd1547edb | ||
|
|
21f204d35a | ||
|
|
84da688400 | ||
|
|
b742567ade | ||
|
|
385b15bf69 | ||
|
|
94ec932d29 | ||
|
|
2525b30803 | ||
|
|
324964b86b | ||
|
|
d31f932800 | ||
|
|
363171b182 | ||
|
|
32e3cd7cbc | ||
|
|
2a223cbea2 | ||
|
|
c7a182e9d5 | ||
|
|
d8f757cb8c | ||
|
|
bfe6b520d7 | ||
|
|
190c3aad58 | ||
|
|
022abcb47d | ||
|
|
5b305cb696 | ||
|
|
a03069fb08 | ||
|
|
0413fea8d2 | ||
|
|
30081ca04b | ||
|
|
53a1e5b7e6 | ||
|
|
29cc5f9c62 | ||
|
|
b485d1cde9 | ||
|
|
045c2d4e59 | ||
|
|
654af9a48d | ||
|
|
5bb58c5172 | ||
|
|
42b9c898bf | ||
|
|
795c86b045 | ||
|
|
5d5e7f97e9 | ||
|
|
c8cf18d8f6 | ||
|
|
70e4499f83 | ||
|
|
2e251a2b20 | ||
|
|
cdec4eb6ab | ||
|
|
71a18da270 | ||
|
|
40354c6b40 | ||
|
|
68fb4da24a | ||
|
|
89f9081658 | ||
|
|
84d2814f7c | ||
|
|
193b671246 | ||
|
|
2bb672a7de | ||
|
|
c156621981 | ||
|
|
9a8983e8e9 | ||
|
|
4bdfc2d926 | ||
|
|
bd9707e8f3 | ||
|
|
3023a43d4f | ||
|
|
41b0393140 | ||
|
|
90a4c84bc2 | ||
|
|
6749e8667d | ||
|
|
9727346e63 | ||
|
|
3683b7a5ff | ||
|
|
f9ca726290 | ||
|
|
15ec06b593 | ||
|
|
953d9db30d | ||
|
|
a7b9dcdce8 | ||
|
|
76f3ed3c3f | ||
|
|
2ad87ce213 | ||
|
|
cc5b3c11c4 | ||
|
|
20731672ed | ||
|
|
f9ec04bbb7 | ||
|
|
0e6d038934 | ||
|
|
b8c6ff611d | ||
|
|
ecfd92ed30 | ||
|
|
8eb9971797 | ||
|
|
c567e690ad | ||
|
|
acf1b1285b | ||
|
|
3d65500a4f | ||
|
|
589284778a | ||
|
|
b9e989cbf2 | ||
|
|
f39c46a1b5 | ||
|
|
48bd8ebe92 | ||
|
|
34524d6dfa | ||
|
|
6e861b19fa | ||
|
|
26f7b8ee65 | ||
|
|
04dd4e7f7c | ||
|
|
992e5d72ff | ||
|
|
1b0f5f0a81 |
@@ -575,8 +575,7 @@ 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.", "5"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"packages/grafana-data/src/types/variables.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
@@ -719,17 +718,6 @@ 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-data/src/utils/location.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
||||
],
|
||||
"packages/grafana-data/src/utils/location.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
@@ -985,8 +973,9 @@ exports[`better eslint`] = {
|
||||
"packages/grafana-schema/src/veneer/dashboard.types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"]
|
||||
],
|
||||
"packages/grafana-toolkit/src/cli/tasks/component.create.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
||||
193
.drone.yml
193
.drone.yml
@@ -21,7 +21,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -77,7 +77,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -132,7 +132,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -278,7 +278,7 @@ steps:
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go test -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: grafana/build-container:v1.7.1
|
||||
@@ -396,7 +396,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -729,7 +729,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -844,7 +844,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -961,7 +961,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1038,7 +1038,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1178,7 +1178,7 @@ steps:
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go test -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: grafana/build-container:v1.7.1
|
||||
@@ -1288,7 +1288,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1719,7 +1719,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -1835,7 +1835,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: windows-init
|
||||
@@ -2014,7 +2014,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2320,7 +2320,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2406,7 +2406,7 @@ steps:
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go test -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: grafana/build-container:v1.7.1
|
||||
@@ -2467,7 +2467,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2573,7 +2573,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: windows-init
|
||||
@@ -2630,7 +2630,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -2956,7 +2956,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3021,7 +3021,7 @@ steps:
|
||||
name: clone-enterprise
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3079,7 +3079,7 @@ steps:
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go test -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: grafana/build-container:v1.7.1
|
||||
@@ -3146,7 +3146,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3238,7 +3238,9 @@ steps:
|
||||
name: mysql-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://redis:6379/0 -timeout 120s
|
||||
- ./bin/grabpl integration-tests
|
||||
- go clean -testcache
|
||||
- go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic
|
||||
-timeout=5m {}'
|
||||
depends_on:
|
||||
- wire-install
|
||||
environment:
|
||||
@@ -3299,7 +3301,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -3376,7 +3378,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3626,7 +3628,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3877,7 +3879,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -3924,8 +3926,8 @@ steps:
|
||||
- name: docker
|
||||
path: /var/run/docker.sock
|
||||
- commands:
|
||||
- ./bin/grabpl artifacts docker publish --dockerhub-repo grafana/grafana/grafana-oss
|
||||
--version-tag ${DRONE_TAG}
|
||||
- ./bin/grabpl artifacts docker publish --dockerhub-repo grafana/grafana-oss --version-tag
|
||||
${DRONE_TAG}
|
||||
depends_on:
|
||||
- fetch-images-oss
|
||||
environment:
|
||||
@@ -3936,7 +3938,7 @@ steps:
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
image: google/cloud-sdk
|
||||
name: publish-images-grafana/grafana-oss
|
||||
name: publish-images-grafana-oss
|
||||
volumes:
|
||||
- name: docker
|
||||
path: /var/run/docker.sock
|
||||
@@ -3973,7 +3975,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4052,7 +4054,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4322,20 +4324,27 @@ platform:
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl artifacts publish --security --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET}
|
||||
depends_on:
|
||||
- grabpl
|
||||
- go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.19.4
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- ./bin/build artifacts publish --security --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET}
|
||||
depends_on:
|
||||
- compile-build-cmd
|
||||
environment:
|
||||
ENTERPRISE2_SECURITY_PREFIX:
|
||||
from_secret: enterprise2_security_prefix
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
PRERELEASE_BUCKET:
|
||||
from_secret: prerelease_bucket
|
||||
SECURITY_DEST_BUCKET:
|
||||
from_secret: security_dest_bucket
|
||||
STATIC_ASSET_EDITIONS:
|
||||
from_secret: static_asset_editions
|
||||
image: grafana/grafana-ci-deploy:1.3.3
|
||||
name: publish-artifacts
|
||||
trigger:
|
||||
@@ -4366,20 +4375,27 @@ platform:
|
||||
services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl artifacts publish --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET}
|
||||
depends_on:
|
||||
- grabpl
|
||||
- go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd
|
||||
depends_on: []
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.19.4
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- ./bin/build artifacts publish --tag $${DRONE_TAG} --src-bucket $${PRERELEASE_BUCKET}
|
||||
depends_on:
|
||||
- compile-build-cmd
|
||||
environment:
|
||||
ENTERPRISE2_SECURITY_PREFIX:
|
||||
from_secret: enterprise2_security_prefix
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
PRERELEASE_BUCKET:
|
||||
from_secret: prerelease_bucket
|
||||
SECURITY_DEST_BUCKET:
|
||||
from_secret: security_dest_bucket
|
||||
STATIC_ASSET_EDITIONS:
|
||||
from_secret: static_asset_editions
|
||||
image: grafana/grafana-ci-deploy:1.3.3
|
||||
name: publish-artifacts
|
||||
trigger:
|
||||
@@ -4477,7 +4493,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4574,7 +4590,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4654,7 +4670,7 @@ clone:
|
||||
retries: 3
|
||||
depends_on: []
|
||||
environment:
|
||||
EDITION: all
|
||||
EDITION: enterprise
|
||||
image_pull_secrets:
|
||||
- dockerconfigjson
|
||||
kind: pipeline
|
||||
@@ -4668,14 +4684,47 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
- commands:
|
||||
- ./bin/grabpl artifacts-page
|
||||
- git clone "https://$${GITHUB_TOKEN}@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
- git checkout ${DRONE_TAG}
|
||||
environment:
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: clone-enterprise
|
||||
- commands:
|
||||
- mv bin/grabpl /tmp/
|
||||
- rmdir bin
|
||||
- mv grafana-enterprise /tmp/
|
||||
- /tmp/grabpl init-enterprise --github-token $${GITHUB_TOKEN} /tmp/grafana-enterprise
|
||||
${DRONE_TAG}
|
||||
- mv /tmp/grafana-enterprise/deployment_tools_config.json deployment_tools_config.json
|
||||
- mkdir bin
|
||||
- mv /tmp/grabpl bin/
|
||||
depends_on:
|
||||
- grabpl
|
||||
- clone-enterprise
|
||||
environment:
|
||||
GITHUB_TOKEN:
|
||||
from_secret: github_token
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: init-enterprise
|
||||
- commands:
|
||||
- go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd
|
||||
depends_on:
|
||||
- init-enterprise
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
image: golang:1.19.4
|
||||
name: compile-build-cmd
|
||||
- commands:
|
||||
- ./bin/build artifacts-page
|
||||
depends_on:
|
||||
- compile-build-cmd
|
||||
environment:
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
@@ -4713,7 +4762,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -4992,7 +5041,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -5075,7 +5124,7 @@ steps:
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go test -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: grafana/build-container:v1.7.1
|
||||
@@ -5133,7 +5182,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -5236,7 +5285,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
image: grafana/ci-wix:0.1.1
|
||||
name: windows-init
|
||||
@@ -5286,7 +5335,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -5614,7 +5663,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -5676,7 +5725,7 @@ steps:
|
||||
name: clone-enterprise
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -5733,7 +5782,7 @@ steps:
|
||||
image: grafana/build-container:v1.7.1
|
||||
name: wire-install
|
||||
- commands:
|
||||
- go test -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
- go test -tags requires_buildifer -short -covermode=atomic -timeout=5m ./pkg/...
|
||||
depends_on:
|
||||
- wire-install
|
||||
image: grafana/build-container:v1.7.1
|
||||
@@ -5797,7 +5846,7 @@ services:
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -5888,7 +5937,9 @@ steps:
|
||||
name: mysql-integration-tests
|
||||
- commands:
|
||||
- dockerize -wait tcp://redis:6379/0 -timeout 120s
|
||||
- ./bin/grabpl integration-tests
|
||||
- go clean -testcache
|
||||
- go list './pkg/...' | xargs -I {} sh -c 'go test -run Integration -covermode=atomic
|
||||
-timeout=5m {}'
|
||||
depends_on:
|
||||
- wire-install
|
||||
environment:
|
||||
@@ -5946,7 +5997,7 @@ steps:
|
||||
name: identify-runner
|
||||
- commands:
|
||||
- $$ProgressPreference = "SilentlyContinue"
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/windows/grabpl.exe
|
||||
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/windows/grabpl.exe
|
||||
-OutFile grabpl.exe
|
||||
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
|
||||
- cd grafana-enterprise
|
||||
@@ -6016,7 +6067,7 @@ services: []
|
||||
steps:
|
||||
- commands:
|
||||
- mkdir -p bin
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.20/grabpl
|
||||
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v3.0.21/grabpl
|
||||
- chmod +x bin/grabpl
|
||||
image: byrnedo/alpine-curl:0.1.8
|
||||
name: grabpl
|
||||
@@ -6502,6 +6553,6 @@ kind: secret
|
||||
name: aws_secret_access_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 6e76bf175f2c58fd4ffdc42e2120c558345a71a45011279b14092acb67252b28
|
||||
hmac: 934e2dbe22f80a17ebb301d68fdac9d91544dbb92486f798300bb39436405bff
|
||||
|
||||
...
|
||||
|
||||
@@ -11,7 +11,7 @@ apiVersion: 1
|
||||
# folder: my_first_folder
|
||||
# # <duration, required> interval of the rule group evaluation
|
||||
# interval: 60s
|
||||
# # <list, required> list of rules that are part of the rule group
|
||||
# # <list, required> list of rules that are part of the rule group
|
||||
# rules:
|
||||
# # <string, required> unique identifier for the rule
|
||||
# - uid: my_id_1
|
||||
@@ -23,7 +23,7 @@ apiVersion: 1
|
||||
# # evaluation - should be obtained via the API
|
||||
# data:
|
||||
# - refId: A
|
||||
# datasourceUid: "-100"
|
||||
# datasourceUid: "__expr__"
|
||||
# model:
|
||||
# conditions:
|
||||
# - evaluator:
|
||||
@@ -40,7 +40,7 @@ apiVersion: 1
|
||||
# type: query
|
||||
# datasource:
|
||||
# type: __expr__
|
||||
# uid: "-100"
|
||||
# uid: "__expr__"
|
||||
# expression: 1==0
|
||||
# intervalMs: 1000
|
||||
# maxDataPoints: 43200
|
||||
@@ -53,7 +53,7 @@ apiVersion: 1
|
||||
# # <string> state of the alert rule when no data is returned
|
||||
# # possible values: "NoData", "Alerting", "OK", default = NoData
|
||||
# noDataState: Alerting
|
||||
# # <string> state of the alert rule when the query execution
|
||||
# # <string> state of the alert rule when the query execution
|
||||
# # fails - possible values: "Error", "Alerting", "OK"
|
||||
# # default = Alerting
|
||||
# executionErrorState: Alerting
|
||||
@@ -62,10 +62,11 @@ apiVersion: 1
|
||||
# # <map<string, string>> map of strings to attach arbitrary custom data
|
||||
# annotations:
|
||||
# some_key: some_value
|
||||
# # <map<string, string> map of strings to filter and
|
||||
# # <map<string, string> map of strings to filter and
|
||||
# # route alerts
|
||||
# labels:
|
||||
# team: sre_team_1
|
||||
# isPaused: false
|
||||
|
||||
# # List of alert rule UIDs that should be deleted
|
||||
# deleteRules:
|
||||
@@ -103,7 +104,7 @@ apiVersion: 1
|
||||
# # <list<string>> The labels by which incoming alerts are grouped together. For example,
|
||||
# # multiple alerts coming in for cluster=A and alertname=LatencyHigh would
|
||||
# # be batched into a single group.
|
||||
# #
|
||||
# #
|
||||
# # To aggregate by all possible labels, use the special value '...' as
|
||||
# # the sole label name, for example:
|
||||
# # group_by: ['...']
|
||||
@@ -127,7 +128,7 @@ apiVersion: 1
|
||||
# mute_time_intervals:
|
||||
# - abc
|
||||
# # <duration> How long to initially wait to send a notification for a group
|
||||
# # of alerts. Allows to collect more initial alerts for the same group.
|
||||
# # of alerts. Allows to collect more initial alerts for the same group.
|
||||
# # (Usually ~0s to few minutes), default = 30s
|
||||
# group_wait: 30s
|
||||
# # <duration> How long to wait before sending a notification about new alerts that
|
||||
@@ -138,7 +139,7 @@ apiVersion: 1
|
||||
# # been sent successfully for an alert. (Usually ~3h or more), default = 4h
|
||||
# repeat_interval: 4h
|
||||
# # <list> Zero or more child routes
|
||||
# routes:
|
||||
# routes:
|
||||
# ...
|
||||
|
||||
# # List of orgIds that should be reset to the default policy
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
"label": "gdev-testdata",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "testdata",
|
||||
"pluginName": "TestData DB"
|
||||
"pluginId": "testdata"
|
||||
}
|
||||
],
|
||||
"__requires": [
|
||||
@@ -19,7 +18,7 @@
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "testdata",
|
||||
"name": "TestData DB",
|
||||
"name": "TestData",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
|
||||
326
devenv/dev-dashboards/panel-heatmap/heatmap-x.json
Normal file
326
devenv/dev-dashboards/panel-heatmap/heatmap-x.json
Normal file
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 116,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 10,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "9.4.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "x,y1,y2\n1,8,12\n2,6,13\n3,7,9\n5,9,7\n6,5,9",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Raw heatmap rows",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 14,
|
||||
"x": 10,
|
||||
"y": 0
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"calculate": false,
|
||||
"cellGap": 1,
|
||||
"color": {
|
||||
"exponent": 0.5,
|
||||
"fill": "dark-orange",
|
||||
"mode": "scheme",
|
||||
"reverse": false,
|
||||
"scale": "exponential",
|
||||
"scheme": "Oranges",
|
||||
"steps": 64
|
||||
},
|
||||
"exemplars": {
|
||||
"color": "rgba(255,0,255,0.7)"
|
||||
},
|
||||
"filterValues": {
|
||||
"le": 1e-9
|
||||
},
|
||||
"legend": {
|
||||
"show": true
|
||||
},
|
||||
"rowsFrame": {
|
||||
"layout": "auto"
|
||||
},
|
||||
"tooltip": {
|
||||
"show": true,
|
||||
"yHistogram": false
|
||||
},
|
||||
"yAxis": {
|
||||
"axisPlacement": "left",
|
||||
"reverse": false
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.4.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 2,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Row heatmap",
|
||||
"type": "heatmap"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 10,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true
|
||||
},
|
||||
"pluginVersion": "9.4.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"csvContent": "x,y,count\n1,4,10\n1,6,11\n2,5,30\n2,4,22\n3,6,17",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "csv_content"
|
||||
}
|
||||
],
|
||||
"title": "Raw heatmap cells",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 14,
|
||||
"x": 10,
|
||||
"y": 9
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"calculate": false,
|
||||
"cellGap": 1,
|
||||
"color": {
|
||||
"exponent": 0.5,
|
||||
"fill": "dark-orange",
|
||||
"mode": "scheme",
|
||||
"reverse": false,
|
||||
"scale": "exponential",
|
||||
"scheme": "Oranges",
|
||||
"steps": 64
|
||||
},
|
||||
"exemplars": {
|
||||
"color": "rgba(255,0,255,0.7)"
|
||||
},
|
||||
"filterValues": {
|
||||
"le": 1e-9
|
||||
},
|
||||
"legend": {
|
||||
"show": true
|
||||
},
|
||||
"rowsFrame": {
|
||||
"layout": "auto"
|
||||
},
|
||||
"tooltip": {
|
||||
"show": true,
|
||||
"yHistogram": false
|
||||
},
|
||||
"yAxis": {
|
||||
"axisPlacement": "left",
|
||||
"reverse": false
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.4.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "-- Dashboard --"
|
||||
},
|
||||
"panelId": 5,
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Cells heatmap",
|
||||
"type": "heatmap"
|
||||
}
|
||||
],
|
||||
"revision": 1,
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Heatmap X axis",
|
||||
"uid": "5Y0jv6pVz",
|
||||
"version": 3,
|
||||
"weekStart": ""
|
||||
}
|
||||
|
||||
@@ -299,7 +299,10 @@
|
||||
"revision": 1,
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"tags": [
|
||||
"gdev",
|
||||
"transform"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
@@ -309,7 +312,7 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Test extractFields JSON",
|
||||
"title": "Transforms - Test extractFields JSON",
|
||||
"uid": "pD4vPYhVz",
|
||||
"version": 3,
|
||||
"weekStart": ""
|
||||
|
||||
171
devenv/dev-dashboards/transforms/filter.json
Normal file
171
devenv/dev-dashboards/transforms/filter.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 1394,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"uid": "PD8C576611E62080A",
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"inspect": false
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green"
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"targets": [
|
||||
{
|
||||
"scenarioId": "csv_content",
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"uid": "PD8C576611E62080A",
|
||||
"type": "testdata"
|
||||
},
|
||||
"csvContent": "AAA\n1\n2\n3\n4"
|
||||
},
|
||||
{
|
||||
"scenarioId": "csv_content",
|
||||
"refId": "B",
|
||||
"datasource": {
|
||||
"uid": "PD8C576611E62080A",
|
||||
"type": "testdata"
|
||||
},
|
||||
"csvContent": "BBB\n1\n2\n3\n4\n",
|
||||
"hide": false
|
||||
}
|
||||
],
|
||||
"title": "Transformer query filters",
|
||||
"type": "table",
|
||||
"transformations": [
|
||||
{
|
||||
"id": "reduce",
|
||||
"options": {
|
||||
"reducers": [
|
||||
"min"
|
||||
],
|
||||
"mode": "reduceFields",
|
||||
"includeTimeField": false
|
||||
},
|
||||
"filter": {
|
||||
"id": "byRefId",
|
||||
"options": "A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "reduce",
|
||||
"options": {
|
||||
"reducers": [
|
||||
"max"
|
||||
],
|
||||
"mode": "reduceFields",
|
||||
"includeTimeField": false
|
||||
},
|
||||
"filter": {
|
||||
"id": "byRefId",
|
||||
"options": "B"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "concatenate",
|
||||
"options": {}
|
||||
},
|
||||
{
|
||||
"id": "organize",
|
||||
"options": {
|
||||
"excludeByName": {},
|
||||
"indexByName": {},
|
||||
"renameByName": {
|
||||
"AAA": "Min from Query A",
|
||||
"BBB": "Max from Query B"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"options": {
|
||||
"showHeader": true,
|
||||
"footer": {
|
||||
"show": false,
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"countRows": false,
|
||||
"fields": ""
|
||||
},
|
||||
"frameIndex": 0
|
||||
},
|
||||
"pluginVersion": "9.4.0-pre"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"gdev",
|
||||
"transform"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Transforms - Filters",
|
||||
"uid": "fGWBVW4k"
|
||||
}
|
||||
@@ -624,7 +624,7 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Join by field",
|
||||
"title": "Transforms - Join by field",
|
||||
"uid": "gw0K4rmVz",
|
||||
"version": 6,
|
||||
"weekStart": ""
|
||||
|
||||
@@ -347,7 +347,7 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Join by labels",
|
||||
"title": "Transforms - Join by labels",
|
||||
"uid": "FVl-9CR4z",
|
||||
"version": 10,
|
||||
"weekStart": ""
|
||||
|
||||
@@ -521,7 +521,10 @@
|
||||
],
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": ["devenv"],
|
||||
"tags": [
|
||||
"gdev",
|
||||
"transform"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
@@ -531,6 +534,6 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Reuse dashboard queries",
|
||||
"title": "Transforms - Reuse dashboard queries",
|
||||
"uid": "fYGWTVW4k"
|
||||
}
|
||||
|
||||
@@ -184,6 +184,13 @@ local dashboard = grafana.dashboard;
|
||||
id: 0,
|
||||
}
|
||||
},
|
||||
dashboard.new('filter', import '../dev-dashboards/transforms/filter.json') +
|
||||
resource.addMetadata('folder', 'dev-dashboards') +
|
||||
{
|
||||
spec+: {
|
||||
id: 0,
|
||||
}
|
||||
},
|
||||
dashboard.new('gauge-multi-series', import '../dev-dashboards/panel-gauge/gauge-multi-series.json') +
|
||||
resource.addMetadata('folder', 'dev-dashboards') +
|
||||
{
|
||||
@@ -296,6 +303,13 @@ local dashboard = grafana.dashboard;
|
||||
id: 0,
|
||||
}
|
||||
},
|
||||
dashboard.new('heatmap-x', import '../dev-dashboards/panel-heatmap/heatmap-x.json') +
|
||||
resource.addMetadata('folder', 'dev-dashboards') +
|
||||
{
|
||||
spec+: {
|
||||
id: 0,
|
||||
}
|
||||
},
|
||||
dashboard.new('histogram_tests', import '../dev-dashboards/panel-histogram/histogram_tests.json') +
|
||||
resource.addMetadata('folder', 'dev-dashboards') +
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}}
|
||||
## Add Grafana managed rule
|
||||
|
||||
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
|
||||
1. Click **New alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default.
|
||||
1. Click **Create alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default.
|
||||
1. In Step 1, add the rule name.
|
||||
- In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule.
|
||||
1. In Step 2, add queries and expressions to evaluate, and then select the alert condition.
|
||||
@@ -31,12 +31,18 @@ Watch this video to learn more about creating alerts: {{< vimeo 720001934 >}}
|
||||
- Click **Run queries** to verify that the query is successful.
|
||||
- Next, select the query or expression for your alert condition.
|
||||
1. In Step 3, specify the alert evaluation interval.
|
||||
|
||||
- From the **Condition** drop-down, select the query or expression to trigger the alert rule.
|
||||
- For **Evaluate every**, specify the frequency of evaluation. Must be a multiple of 10 seconds. For examples, `1m`, `30s`.
|
||||
- For **Evaluate for**, specify the duration for which the condition must be true before an alert fires.
|
||||
> **Note:** Once a condition is breached, the alert goes into the Pending state. If the condition remains breached for the duration specified, the alert transitions to the `Firing` state, otherwise it reverts back to the `Normal` state.
|
||||
- In **Configure no data and error handling**, configure alerting behavior in the absence of data. Use the guidelines in [No data and error handling](#no-data-and-error-handling).
|
||||
- Click **Preview alerts** to check the result of running the query at this moment. Preview excludes no data and error handling.
|
||||
|
||||
**Note:**
|
||||
|
||||
You can pause alert rule evaluation to prevent noisy alerting while tuning your alerts. Pausing stops alert rule evaluation and does not create any alert instances. This is different to mute timings, which stop notifications from being delivered, but still allow for alert rule evaluation and the creation of alert instances.
|
||||
|
||||
1. In Step 4, add the storage location, rule group, as well as additional metadata associated with the rule.
|
||||
- From the **Folder** drop-down, select the folder where you want to store the rule.
|
||||
- For **Group**, specify a pre-defined group. Newly created rules are appended to the end of the group. Rules within a group are run sequentially at a regular interval, with the same evaluation time.
|
||||
|
||||
@@ -35,7 +35,7 @@ You can create and manage recording rules for an external Grafana Mimir or Loki
|
||||
To create a Grafana Mimir or Loki managed recording rule
|
||||
|
||||
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
|
||||
1. Click **New alert rule**. The new alerting rule page opens where the **Grafana managed alert** option is selected by default.
|
||||
1. Click **Create alert rule**. The new alerting rule page opens where the **Grafana managed alert** option is selected by default.
|
||||
1. In Step 1, add the rule name. The recording name must be a Prometheus metric name and contain no whitespace.
|
||||
- In **Rule name**, add a descriptive name.
|
||||
1. In Step 2, select **Mimir or Loki recording rule** option.
|
||||
@@ -49,7 +49,7 @@ To create a Grafana Mimir or Loki managed recording rule
|
||||
1. Click **Save** to save the recording rule or **Save and exit** to save the recording rule and go back to the Alerting page.
|
||||
|
||||
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
|
||||
1. Click **New alert rule**.
|
||||
1. Click **Create alert rule**.
|
||||
1. In Step 1, add the rule name.
|
||||
- In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule.
|
||||
1. In Step 2, add the type, and storage location.
|
||||
|
||||
@@ -35,7 +35,7 @@ Watch this video to learn more about how to create a Mimir managed alert rule: {
|
||||
## Add a Grafana Mimir or Loki managed alerting rule
|
||||
|
||||
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
|
||||
1. Click **New alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default.
|
||||
1. Click **Create alert rule**. The new alerting rule page opens where the Grafana managed alerts option is selected by default.
|
||||
1. In Step 1, add the rule name.
|
||||
- In **Rule name**, add a descriptive name. This name is displayed in the alert rule list. It is also the `alertname` label for every alert instance that is created from this rule.
|
||||
1. In Step 2, select **Mimir or Loki alert** option.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
aliases:
|
||||
description: Declare an incident from a firing alert
|
||||
keywords:
|
||||
- grafana
|
||||
- alert rules
|
||||
- incident
|
||||
title: Declare an incident from a firing alert
|
||||
weight: 430
|
||||
---
|
||||
|
||||
# Declare incidents from firing alerts
|
||||
|
||||
Declare an incident from a firing alert to streamline your alert to incident workflow.
|
||||
|
||||
## Before you begin
|
||||
|
||||
- Ensure you have Grafana Incident installed
|
||||
- You must have a firing alert
|
||||
|
||||
## Procedure
|
||||
|
||||
To declare an incident from a firing alert, complete the following steps.
|
||||
|
||||
1. Navigate to Alerts & Incidents -> Alerting -> Alert rules.
|
||||
2. From the Alert rules list view, click the firing alert that you want to declare an incident for.
|
||||
|
||||
**Note:**
|
||||
|
||||
You can also access **Declare Incident** from the alert details page.
|
||||
|
||||
3. Click **Declare Incident**.
|
||||
The **Declare Incident** pop-up opens in the Grafana Incident application.
|
||||
4. In the **Declare Incident** pop-up, enter what's going on.
|
||||
|
||||
**Note**: This field is pre-filled with the name of the alert rule, but you can edit it as required.
|
||||
|
||||
The alert rule is also linked to the incident.
|
||||
|
||||
5. Select a severity.
|
||||
6. Add labels, as required.
|
||||
7. Click **More options** to include a channel prefix and status.
|
||||
8. Click **Declare Incident**.
|
||||
|
||||
## Next steps
|
||||
|
||||
View and track the incident in the Grafana Incident application.
|
||||
|
||||
For more information, refer to [Grafana Incident documentation.](https://grafana.com/docs/grafana-cloud/incident/configure-settings/)
|
||||
@@ -20,8 +20,12 @@ The Alerting page lists all existing alert rules. By default, rules are grouped
|
||||
|
||||
The Mimir/Cortex/Loki rules section lists all rules for Mimir, Cortex, or Loki data sources. Cloud alert rules are also listed in this section.
|
||||
|
||||
When managing large volumes of alerts, you can use extended alert rule search capabilities to filter on folders, evaluation groups, and rules. Additionally, you can filter alert rules by their properties like labels, state, type, and health.
|
||||
|
||||
- [View and filter alert rules](#view-and-filter-alert-rules)
|
||||
- [View alert rules](#view-alert-rules)
|
||||
- [Export alert rules](#export-alert-rules)
|
||||
- [View query definitions for provisioned alerts](#view-query-definitions-for-provisioned-alerts)
|
||||
- [Grouped view](#grouped-view)
|
||||
- [State view](#state-view)
|
||||
- [Filter alert rules](#filter-alert-rules)
|
||||
@@ -36,6 +40,18 @@ To view alerting details:
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/unified/rule-details-8-0.png" max-width="650px" caption="Alerting rule details" >}}
|
||||
|
||||
From the Alert list page, you can also make copies of alert rules to help you reuse existing alert rules.
|
||||
|
||||
## Export alert rules
|
||||
|
||||
Click **Export** to create and tune an alert rule in the UI, then export to YAML or JSON, and use in the provisioning API or files. You can also export an entire rule group to review or use.
|
||||
|
||||
**Note:** This is supported in both the UI and provisioning API.
|
||||
|
||||
## View query definitions for provisioned alerts
|
||||
|
||||
View read-only query definitions for provisioned alerts. Check quickly if your alert rule queries are correct, without diving into your "as-code" repository for rule definitions.
|
||||
|
||||
### Grouped view
|
||||
|
||||
Grouped view shows Grafana alert rules grouped by folder and Loki or Prometheus alert rules grouped by `namespace` + `group`. This is the default rule list view, intended for managing rules. You can expand each group to view a list of rules in this group. Expand a rule further to view its details. You can also expand action buttons and alerts resulting from the rule to view their details.
|
||||
|
||||
@@ -21,6 +21,10 @@ Use contact points to define how your contacts are notified when an alert rule f
|
||||
|
||||
You can also use notification templating to customize notification messages for contact point types.
|
||||
|
||||
**Note:**
|
||||
|
||||
If you've created an OnCall contact point in the Grafana OnCall application, you can view it in the Alerting application.
|
||||
|
||||
## Supported contact point types
|
||||
|
||||
The following table lists the contact point types supported by Grafana.
|
||||
|
||||
@@ -24,13 +24,13 @@ Complete the following steps to add a contact point.
|
||||
|
||||
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
|
||||
1. Click **Contact points** to open the page listing existing contact points.
|
||||
1. Click **New contact point**.
|
||||
1. Click **Add contact point**.
|
||||
1. From the **Alertmanager** dropdown, select an Alertmanager. By default, Grafana Alertmanager is selected.
|
||||
1. In **Name**, enter a descriptive name for the contact point.
|
||||
1. From **Contact point integration**, select a type and fill out mandatory fields. For example, if you choose email, enter the email addresses. Or if you choose Slack, enter the Slack channel(s) and users who should be contacted.
|
||||
1. Some contact point integrations, like email or webhook, have optional settings. In **Optional settings**, specify additional settings for the selected contact point integration.
|
||||
1. In Notification settings, optionally select **Disable resolved message** if you do not want to be notified when an alert resolves.
|
||||
1. To add another contact point integration, click **New contact point integration** and repeat steps 6 through 8.
|
||||
1. To add another contact point integration, click **Add contact point integration** and repeat steps 6 through 8.
|
||||
1. Click **Save contact point** to save your changes.
|
||||
|
||||
## Edit a contact point
|
||||
|
||||
@@ -28,7 +28,7 @@ To add a silence, complete the following steps.
|
||||
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
|
||||
2. On the Alerting page, click **Silences** to open the page listing existing silences.
|
||||
3. From Alertmanager drop-down, select an external Alertmanager to create and manage silences for the external data source. Otherwise, keep the default option of Grafana.
|
||||
4. Click **New Silence** to open the Create silence page.
|
||||
4. Click **Add Silence** to open the Create silence page.
|
||||
5. In **Silence start and end**, select the start and end date to indicate when the silence should go into effect and expire.
|
||||
6. Optionally, in **Duration**, specify how long the silence is enforced. This automatically updates the end time in the **Silence start and end** field.
|
||||
7. In the **Name** and **Value** fields, enter one or more _Matching Labels_. Matchers determine which rules the silence will apply to.
|
||||
|
||||
@@ -24,7 +24,7 @@ In the Contact points tab, you can see a list of your notification templates.
|
||||
|
||||
To create a template, complete the following steps.
|
||||
|
||||
1. Click New template.
|
||||
1. Click **Add template**.
|
||||
|
||||
2. Choose a name for the notification template.
|
||||
|
||||
@@ -40,7 +40,7 @@ To create a template, complete the following steps.
|
||||
|
||||
To create a notification template that contains more than one template:
|
||||
|
||||
1. Click New Template.
|
||||
1. Click **Add Template**.
|
||||
|
||||
2. Enter a name for the notification template.
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ groups:
|
||||
# evaluation - should be obtained trough the API
|
||||
data:
|
||||
- refId: A
|
||||
datasourceUid: '-100'
|
||||
datasourceUid: '__expr__'
|
||||
model:
|
||||
conditions:
|
||||
- evaluator:
|
||||
@@ -86,7 +86,7 @@ groups:
|
||||
type: query
|
||||
datasource:
|
||||
type: __expr__
|
||||
uid: '-100'
|
||||
uid: '__expr__'
|
||||
expression: 1==0
|
||||
intervalMs: 1000
|
||||
maxDataPoints: 43200
|
||||
@@ -313,7 +313,7 @@ settings:
|
||||
```yaml
|
||||
type: pagerduty
|
||||
settings:
|
||||
# <string, required>
|
||||
# <string, required> the 32-character Events API key https://support.pagerduty.com/docs/api-access-keys#events-api-keys
|
||||
integrationKey: XXX
|
||||
# <string> options: critical, error, warning, info
|
||||
severity: critical
|
||||
|
||||
@@ -282,7 +282,7 @@ resource "grafana_rule_group" "my_rule_group" {
|
||||
|
||||
// The query was configured to obtain data from the last 60 seconds. Let's alert on the average value of that series using a Reduce stage.
|
||||
data {
|
||||
datasource_uid = "-100"
|
||||
datasource_uid = "__expr__"
|
||||
// You can also create a rule in the UI, then GET that rule to obtain the JSON.
|
||||
// This can be helpful when using more complex reduce expressions.
|
||||
model = <<EOT
|
||||
@@ -298,7 +298,7 @@ EOT
|
||||
// Now, let's use a math expression as our threshold.
|
||||
// We want to alert when the value of stage "B" above exceeds 70.
|
||||
data {
|
||||
datasource_uid = "-100"
|
||||
datasource_uid = "__expr__"
|
||||
ref_id = "C"
|
||||
relative_time_range {
|
||||
from = 0
|
||||
|
||||
@@ -9,7 +9,6 @@ aliases:
|
||||
- dashboard-folders/
|
||||
- dashboard-manage/
|
||||
- export-import/
|
||||
- time-range-controls/
|
||||
keywords:
|
||||
- grafana
|
||||
- dashboard
|
||||
|
||||
@@ -7,6 +7,7 @@ aliases:
|
||||
- dashboard-ui/dashboard-row/
|
||||
- search/
|
||||
- shortcuts/
|
||||
- time-range-controls/
|
||||
keywords:
|
||||
- dashboard
|
||||
- search
|
||||
@@ -32,11 +33,11 @@ The following image and descriptions highlights all dashboards features.
|
||||
- **Dashboard title** (2): When you click the dashboard title you can search for dashboard contained in the current folder.
|
||||
- **Share dashboard** (3): Use this option to share the current dashboard by link or snapshot. You can also export the dashboard definition from the share modal.
|
||||
- **Add a new panel** (4): Use this option to add a panel, dashboard row, or library panel to the current dashboard.
|
||||
- **Dashboard settings** (5): Use this option to change dashboard name, folder, and tags and manage variables and annotation queries. For more information about dashboard settings, refer to [Modify dashboard settings]({{< relref "../build-dashboards/modify-dashboard-settings/" >}})
|
||||
- **Dashboard settings** (5): Use this option to change dashboard name, folder, and tags and manage variables and annotation queries. For more information about dashboard settings, refer to [Modify dashboard settings]({{< relref "../build-dashboards/modify-dashboard-settings/" >}}).
|
||||
- **Time picker dropdown** (6): Click to select relative time range options and set custom absolute time ranges.
|
||||
- You can change the **Timezone** and **fiscal year** settings from the time range controls by clicking the **Change time settings** button.
|
||||
- Time settings are saved on a per-dashboard basis.
|
||||
- **Zoom out time range** (7): Click to zoom out the time range. For more information about how to use time range controls, refer to [Common time range controls](../time-range-controls/#common-time-range-controls).
|
||||
- **Zoom out time range** (7): Click to zoom out the time range. For more information about how to use time range controls, refer to [Common time range controls]({{< relref "#common-time-range-controls" >}}).
|
||||
- **Refresh dashboard** (8): Click to immediately trigger queries and refresh dashboard data.
|
||||
- **Refresh dashboard time interval** (9): Click to select a dashboard auto refresh time interval.
|
||||
- **View mode** (10): Click to display the dashboard on a large screen such as a TV or a kiosk. View mode hides irrelevant information such as navigation menus. For more information about view mode, refer to [How to Create Kiosks to Display Dashboards on a TV](https://grafana.com/blog/2019/05/02/grafana-tutorial-how-to-create-kiosks-to-display-dashboards-on-a-tv/).
|
||||
|
||||
@@ -47,7 +47,7 @@ Variables can be used in titles, descriptions, text panels, and queries. Queries
|
||||
|
||||
The following dashboards in Grafana Play provide examples of template variables.
|
||||
|
||||
- [Elasticsearch Metrics](https://play.grafana.org/d/000000014/elasticsearch-metrics?orgId=1) - Uses ad hoc filters, global variables, and a custom variable.
|
||||
- [Elasticsearch Metrics](https://play.grafana.org/d/z8OZC66nk/elasticsearch-8-2-0-sample-flight-data?orgId=1) - Uses ad hoc filters, global variables, and a custom variable.
|
||||
- [Graphite Templated Nested](https://play.grafana.org/d/000000056/graphite-templated-nested?orgId=1) - Uses query variables, chained query variables, an interval variable, and a repeated panel.
|
||||
- [Influx DB Group By Variable](https://play.grafana.org/d/000000137/influxdb-group-by-variable?orgId=1) - Query variable, panel uses the variable results to group the metric data.
|
||||
- [InfluxDB Raw Query Template Var](https://play.grafana.org/d/000000083/influxdb-raw-query-template-var?orgId=1) - Uses query variables, chained query variables, and an interval variable.
|
||||
|
||||
@@ -44,7 +44,7 @@ In contrast, Azure Monitor Logs can store a variety of data types, each with the
|
||||
|
||||
1. In a Grafana panel, select the **Azure Monitor** data source.
|
||||
1. Select the **Metrics** service.
|
||||
1. Select a resource from which to metrics by using the subscription, resource group, resource type, and resource fields.
|
||||
1. Select a resource from which to query metrics by using the subscription, resource group, resource type, and resource fields. Multiple resources can also be selected as long as they belong to the same subscription, region and resource type. Note that only a limited amount of resource types support this feature.
|
||||
1. To select a different namespace than the default—for instance, to select resources like storage accounts that are organized under multiple namespaces—use the **Namespace** option.
|
||||
|
||||
> **Note:** Not all metrics returned by the Azure Monitor Metrics API have values.
|
||||
@@ -110,7 +110,7 @@ You can also perform complex analysis of Logs data by using KQL.
|
||||
|
||||
1. In a Grafana panel, select the **Azure Monitor** data source.
|
||||
1. Select the **Logs** service.
|
||||
1. Select a resource to query.
|
||||
1. Select a resource to query. Multiple resources can be selected as long as they are of the same type.
|
||||
|
||||
Alternatively, you can dynamically query all resources under a single resource group or subscription.
|
||||
|
||||
|
||||
@@ -32,16 +32,17 @@ For an introduction to templating and template variables, refer to the [Templati
|
||||
|
||||
You can specify these Azure Monitor data source queries in the Variable edit view's **Query Type** field.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| **Subscriptions** | Returns subscriptions. |
|
||||
| **Resource Groups** | Returns resource groups for a specified subscription. |
|
||||
| **Namespaces** | Returns metric namespaces for the specified subscription and resource group. |
|
||||
| **Resource Names** | Returns a list of resource names for a specified subscription, resource group and namespace. |
|
||||
| **Metric Names** | Returns a list of metric names for a resource. |
|
||||
| **Workspaces** | Returns a list of workspaces for the specified subscription. |
|
||||
| **Logs** | Use a KQL query to return values. |
|
||||
| **Resource Graph** | Use an ARG query to return values. |
|
||||
| Name | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **Subscriptions** | Returns subscriptions. |
|
||||
| **Resource Groups** | Returns resource groups for a specified. Supports multi-value. subscription. |
|
||||
| **Namespaces** | Returns metric namespaces for the specified subscription and resource group. |
|
||||
| **Regions** | Returns regions for the specified subscription |
|
||||
| **Resource Names** | Returns a list of resource names for a specified subscription, resource group and namespace. Supports multi-value. |
|
||||
| **Metric Names** | Returns a list of metric names for a resource. |
|
||||
| **Workspaces** | Returns a list of workspaces for the specified subscription. |
|
||||
| **Logs** | Use a KQL query to return values. |
|
||||
| **Resource Graph** | Use an ARG query to return values. |
|
||||
|
||||
Any Log Analytics Kusto Query Language (KQL) query that returns a single list of values can also be used in the Query field.
|
||||
For example:
|
||||
@@ -65,3 +66,13 @@ Perf
|
||||
| summarize avg(CounterValue) by bin(TimeGenerated, $__interval), Computer
|
||||
| order by TimeGenerated asc
|
||||
```
|
||||
|
||||
### Multi-value variables
|
||||
|
||||
It is possible to select multiple values for **Resource Groups** and **Resource Names** and use a single metrics query pointing to those values as long as they:
|
||||
|
||||
- Belong to the same subscription.
|
||||
- Are in the same region.
|
||||
- Are of the same type (namespace).
|
||||
|
||||
Also, note that if a template variable pointing to multiple resource groups or names is used in another template variable as a parameter (e.g. to retrieve metric names), only the first value will be used. This means that the combination of the first resource group and name selected should be valid.
|
||||
|
||||
@@ -118,6 +118,8 @@ The response from MySQL can be formatted as either a table or as a time series.
|
||||
|
||||
### Dataset and Table selection
|
||||
|
||||
> **Note:** If your table or database name contains a reserved word or a [not permitted character](https://dev.mysql.com/doc/refman/8.0/en/identifiers.html) the editor will put quotes around them. For example a table name like `table-name` will be quoted with backticks `` `table-name` ``.
|
||||
|
||||
In the dataset dropdown, choose the MySQL database to query. The dropdown is be populated with the databases that the user has access to.
|
||||
When the dataset is selected, the table dropdown is populated with the tables that are available.
|
||||
|
||||
|
||||
@@ -48,37 +48,58 @@ You can also configure settings specific to the Tempo data source:
|
||||
|
||||
### Configure trace to logs
|
||||
|
||||
{{< figure src="/static/img/docs/explore/traces-to-logs-settings-8-2.png" class="docs-image--no-shadow" caption="Screenshot of the trace to logs settings" >}}
|
||||

|
||||
|
||||
> **Note:** Available in Grafana v7.4 and higher.
|
||||
> If you use Grafana Cloud, open a [support ticket in the Cloud Portal](/profile/org#support) to access this feature.
|
||||
|
||||
The **Trace to logs** setting configures the [trace to logs feature]({{< relref "../../explore/trace-integration" >}}) that is available when you integrate Grafana with Tempo.
|
||||
|
||||
**To configure trace to logs:**
|
||||
There are two ways to configure the trace to logs feature. You can use simplified configuration with default query, or you can configure custom query where you can use a [template language]({{< relref "../../dashboards/variables/variable-syntax">}}) to interpolate variables from the trace or span.
|
||||
|
||||
**To use simple configuration:**
|
||||
|
||||
1. Select the target data source.
|
||||
1. Select which tags to use in the logs query. The tags you configure must be present in the spans attributes or resources for a trace to logs span link to appear.
|
||||
1. Set start and end time shift. As the logs timestamps may not exactly match the timestamps of the spans in trace it may be necessary to search in larger or shifted time range to find the desired logs.
|
||||
1. Select which tags to use in the logs query. The tags you configure must be present in the spans attributes or resources for a trace to logs span link to appear. You can optionally configure a new name for the tag. This is useful for example if the tag has dots in the name and the target data source does not allow using dots in labels. In that case you can for example remap `http.status` to `http_status`.
|
||||
1. Optionally switch on Filter by Trace ID and/or Filter by Span ID to further filter the logs if your logs consistently contain trace or span IDs.
|
||||
|
||||
- **Single tag**
|
||||
- Configuring `job` as a tag and clicking on a span link will take you to your configured logs datasource with the query `{job='value from clicked span'}`.
|
||||
- **Multiple tags**
|
||||
- If multiple tags are used they will be concatenated so the logs query would look like `{job='value from clicked span', service='value from clicked span'}`.
|
||||
- **Mapped tags**
|
||||
- For a mapped tag `service.name` with value `service`, clicking on a span link will take you to your configured logs datasource with the query `{service='value from clicked span'}` instead of `{service.name='value from clicked span'}`.
|
||||
- This is useful for instances where your tracing datasource tags and your logs datasource tags don't match one-to-one.
|
||||
**To use custom query configuration:**
|
||||
|
||||
1. Select the target data source.
|
||||
1. Set start and end time shift. As the logs timestamps may not exactly match the timestamps of the spans in the trace it may be necessary to widen or shift the time range to find the desired logs.
|
||||
1. Optionally select tags to map. These tags can be used in the custom query with `${__tags}` variable. This variable will interpolate the mapped tags as list in an appropriate syntax for the data source and will only include the tags that were present in the span omitting those that weren't present. You can optionally configure a new name for the tag. This is useful in cases where the tag has dots in the name and the target data source does not allow using dots in labels. For example, you can remap `http.status` to `http_status` in such a case. If you don't map any tags here, you can still use any tag in the query like this `method="${__span.tags.method}"`.
|
||||
1. Skip Filter by Trace ID or Filter by Span ID as these cannot be used with custom query.
|
||||
1. Switch on Use custom query.
|
||||
1. Specify custom query to be used to query the logs. You can use various variables to make that query relevant for current span. The link will only be shown only if all the variables are interpolated with non-empty values to prevent creating invalid query.
|
||||
|
||||
**Variables that can be used in custom query**
|
||||
To use a variable you need to wrap it in `${}`. For example `${__span.name}`.
|
||||
|
||||
| Variable name | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| \_\_tags | This variable is special because it uses the tag mapping in from the UI to create a label matcher string in the specific data source syntax. It uses only tags that are present in the span so the link will still be created even if only one of those tags is present in the span. You can use this if not all the tags are required for the query to be useful. |
|
||||
| \_\_span.spanId | The ID of the span. |
|
||||
| \_\_span.traceId | The ID of the trace. |
|
||||
| \_\_span.duration | The duration of the span. |
|
||||
| \_\_span.name | Name of the span. |
|
||||
| \_\_span.tags | Namespace for the tags in the span. To access a specific tag named "version" you would use `${__span.tags.version}`. In case the tag contains dot you have to access it as `${__span.tags["http.status"]}`. |
|
||||
| \_\_trace.traceId | The ID of the trace. |
|
||||
| \_\_trace.duration | The duration of the trace. |
|
||||
| \_\_trace.name | The name of the trace. |
|
||||
|
||||
The following table describes the ways in which you can configure your trace to logs settings:
|
||||
|
||||
| Setting name | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. |
|
||||
| **Tags** | Defines the the tags to use in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. |
|
||||
| **Map tag names** | Enables you to configure how Tempo tag names map to logs label names. For example, you can map `service.name` to `service`. |
|
||||
| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default is 0. |
|
||||
| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default is 0. |
|
||||
| **Filter by Trace ID** | Toggles whether to append the trace ID to the logs query. |
|
||||
| **Filter by Span ID** | Toggles whether to append the span ID to the logs query. |
|
||||
| Setting name | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Data source** | Defines the target data source. You can select only Loki or Splunk \[logs\] data sources. |
|
||||
| **Span start time shift** | Shifts the start time for the logs query, based on the span's start time. You can use time units, such as `5s`, `1m`, `3h`. To extend the time to the past, use a negative value. Default is 0. |
|
||||
| **Span end time shift** | Shifts the end time for the logs query, based on the span's end time. You can use time units. Default is 0. |
|
||||
| **Tags** | Defines the the tags to use in the logs query. Default is `'cluster', 'hostname', 'namespace', 'pod'`. You can change the tag name for example to remove dots from the name if they are not allowed in the target data source. For example map `http.status` to `http_status` |
|
||||
| **Filter by Trace ID** | Toggles whether to append the trace ID to the logs query. |
|
||||
| **Filter by Span ID** | Toggles whether to append the span ID to the logs query. |
|
||||
| **Use custom query** | Toggles use of custom query with interpolation. |
|
||||
| **Query** | Input to write custom query. Use variable interpolation to customize it with variables from span. |
|
||||
|
||||
### Configure trace to metrics
|
||||
|
||||
@@ -293,7 +314,6 @@ For details, refer to the [APM dashboard documentation](/docs/tempo/latest/metri
|
||||
|
||||
**To display the APM table:**
|
||||
|
||||
1. Activate the `tempoApmTable` [feature toggle]({{< relref "../../setup-grafana/configure-grafana#feature_toggles" >}}) in your `grafana.ini` file.
|
||||
1. Link a Prometheus data source in the Tempo data source settings.
|
||||
1. Navigate to [Explore]({{< relref "../../explore/" >}}).
|
||||
1. Select the Tempo data source.
|
||||
|
||||
16
docs/sources/datasources/testdata/_index.md
vendored
16
docs/sources/datasources/testdata/_index.md
vendored
@@ -9,14 +9,14 @@ keywords:
|
||||
- troubleshooting
|
||||
- panels
|
||||
- testdata
|
||||
menuTitle: TestData DB
|
||||
title: TestData DB data source
|
||||
menuTitle: TestData
|
||||
title: TestData data source
|
||||
weight: 1500
|
||||
---
|
||||
|
||||
# TestData DB data source
|
||||
# TestData data source
|
||||
|
||||
Grafana ships with a TestData DB data source, which creates simulated time series data for any [panel]({{< relref "../../panels-visualizations/" >}}).
|
||||
Grafana ships with a TestData data source, which creates simulated time series data for any [panel]({{< relref "../../panels-visualizations/" >}}).
|
||||
You can use it to build your own fake and random time series data and render it in any panel, which helps you verify dashboard functionality since you can safely and easily share the data.
|
||||
|
||||
For instructions on how to add a data source to Grafana, refer to the [administration documentation]({{< relref "../../administration/data-source-management/" >}}).
|
||||
@@ -28,7 +28,7 @@ Only users with the organization administrator role can add data sources.
|
||||
|
||||
1. Hover the cursor over the **Configuration** (gear) icon.
|
||||
1. Select **Data Sources**.
|
||||
1. Select the TestData DB data source.
|
||||
1. Select the TestData data source.
|
||||
|
||||
The data source doesn't provide any settings beyond the most basic options common to all data sources:
|
||||
|
||||
@@ -41,11 +41,11 @@ The data source doesn't provide any settings beyond the most basic options commo
|
||||
|
||||
{{< figure src="/static/img/docs/v41/test_data_add.png" class="docs-image--no-shadow" caption="Adding test data" >}}
|
||||
|
||||
Once you've added the TestData DB data source, your Grafana instance's users can use it as a data source in any metric panel.
|
||||
Once you've added the TestData data source, your Grafana instance's users can use it as a data source in any metric panel.
|
||||
|
||||
### Choose a scenario
|
||||
|
||||
Instead of providing a query editor, the TestData DB data source helps you select a **Scenario** that generates simulated data for panels.
|
||||
Instead of providing a query editor, the TestData data source helps you select a **Scenario** that generates simulated data for panels.
|
||||
|
||||
You can assign an **Alias** to each scenario, and many have their own options that appear when selected.
|
||||
|
||||
@@ -81,7 +81,7 @@ You can assign an **Alias** to each scenario, and many have their own options th
|
||||
|
||||
## Import a pre-configured dashboard
|
||||
|
||||
TestData DB also provides an example dashboard.
|
||||
TestData also provides an example dashboard.
|
||||
|
||||
**To import the example dashboard:**
|
||||
|
||||
|
||||
@@ -244,10 +244,11 @@ GET /api/v1/provisioning/alert-rules/{UID}/export
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||
| -------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- |
|
||||
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
|
||||
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
|
||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||
| -------- | ------- | -------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| UID | `path` | string | `string` | | ✓ | | Alert rule UID |
|
||||
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
|
||||
| format | `query` | `string` | string | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. |
|
||||
|
||||
#### All responses
|
||||
|
||||
@@ -322,11 +323,12 @@ GET /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}/export
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||
| --------- | ------- | ------- | -------- | --------- | :------: | ------- | -------------------------------------------------- |
|
||||
| FolderUID | `path` | string | `string` | | ✓ | | |
|
||||
| Group | `path` | string | `string` | | ✓ | | |
|
||||
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
|
||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||
| --------- | ------- | -------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| FolderUID | `path` | string | `string` | | ✓ | | |
|
||||
| Group | `path` | string | `string` | | ✓ | | |
|
||||
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
|
||||
| format | `query` | `string` | string | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. |
|
||||
|
||||
#### All responses
|
||||
|
||||
@@ -381,9 +383,10 @@ GET /api/v1/provisioning/alert-rules/export
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||
| -------- | ------- | ------- | ------- | --------- | :------: | ------- | -------------------------------------------------- |
|
||||
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
|
||||
| Name | Source | Type | Go type | Separator | Required | Default | Description |
|
||||
| -------- | ------- | -------- | ------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. |
|
||||
| format | `query` | `string` | string | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. |
|
||||
|
||||
#### All responses
|
||||
|
||||
@@ -992,14 +995,14 @@ Status: Accepted
|
||||
|
||||
**Properties**
|
||||
|
||||
| Name | Type | Go type | Required | Default | Description | Example |
|
||||
| --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | -------------------------------------------------------------------------------------------------- | ------- |
|
||||
| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '-100' for a Server Side Expression operation. | |
|
||||
| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | |
|
||||
| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. |
|
||||
| Name | Type | Go type | Required | Default | Description | Example |
|
||||
| --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------ | ------- |
|
||||
| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '**expr**' for a Server Side Expression operation. | |
|
||||
| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | |
|
||||
| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. |
|
||||
| It can be used to distinguish different types of queries. | |
|
||||
| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | |
|
||||
| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | |
|
||||
| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | |
|
||||
| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | |
|
||||
|
||||
### <span id="alert-query-export"></span> AlertQueryExport
|
||||
|
||||
@@ -1174,23 +1177,23 @@ Status: Accepted
|
||||
|
||||
**Properties**
|
||||
|
||||
| Name | Type | Go type | Required | Default | Description | Example |
|
||||
| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` |
|
||||
| condition | string | `string` | ✓ | | | `A` |
|
||||
| data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"-100","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` |
|
||||
| execErrState | string | `string` | ✓ | | | |
|
||||
| folderUID | string | `string` | ✓ | | | `project_x` |
|
||||
| for | [Duration](#duration) | `Duration` | ✓ | | | |
|
||||
| id | int64 (formatted integer) | `int64` | | | | |
|
||||
| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` |
|
||||
| noDataState | string | `string` | ✓ | | | |
|
||||
| orgID | int64 (formatted integer) | `int64` | ✓ | | | |
|
||||
| provenance | [Provenance](#provenance) | `Provenance` | | | | |
|
||||
| ruleGroup | string | `string` | ✓ | | | `eval_group_1` |
|
||||
| title | string | `string` | ✓ | | | `Always firing` |
|
||||
| uid | string | `string` | | | | |
|
||||
| updated | date-time (formatted string) | `strfmt.DateTime` | | | | |
|
||||
| Name | Type | Go type | Required | Default | Description | Example |
|
||||
| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` |
|
||||
| condition | string | `string` | ✓ | | | `A` |
|
||||
| data | [][alertquery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"__expr__","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` |
|
||||
| execErrState | string | `string` | ✓ | | | |
|
||||
| folderUID | string | `string` | ✓ | | | `project_x` |
|
||||
| for | [Duration](#duration) | `Duration` | ✓ | | | |
|
||||
| id | int64 (formatted integer) | `int64` | | | | |
|
||||
| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` |
|
||||
| noDataState | string | `string` | ✓ | | | |
|
||||
| orgID | int64 (formatted integer) | `int64` | ✓ | | | |
|
||||
| provenance | [Provenance](#provenance) | `Provenance` | | | | |
|
||||
| ruleGroup | string | `string` | ✓ | | | `eval_group_1` |
|
||||
| title | string | `string` | ✓ | | | `Always firing` |
|
||||
| uid | string | `string` | | | | |
|
||||
| updated | date-time (formatted string) | `strfmt.DateTime` | | | | |
|
||||
|
||||
### <span id="provisioned-alert-rules"></span> ProvisionedAlertRules
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ This guide helps you identify the steps required to update a plugin from the Gra
|
||||
- [Plugin migration guide](#plugin-migration-guide)
|
||||
- [Introduction](#introduction)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [From version 9.3.x to 9.4.x](#from-version-93x-to-94x) - [Forwarded HTTP headers in grafana-plugin-sdk-go
|
||||
](#forwarded-http-headers-in-grafana-plugin-sdk-go)
|
||||
- [From version 9.1.x to 9.2.x](#from-version-91x-to-92x)
|
||||
- [React and React-dom as peer dependencies](#react-and-react-dom-as-peer-dependencies)
|
||||
- [NavModelItem requires a valid icon name](#navmodelitem-requires-a-valid-icon-name)
|
||||
@@ -62,6 +64,19 @@ This guide helps you identify the steps required to update a plugin from the Gra
|
||||
- [Migrate to data frames](#migrate-to-data-frames)
|
||||
- [Troubleshoot plugin migration](#troubleshoot-plugin-migration)
|
||||
|
||||
## From version 9.3.x to 9.4.x
|
||||
|
||||
### Forwarded HTTP headers in grafana-plugin-sdk-go
|
||||
|
||||
It's recommended to use the `<request>.GetHTTPHeader` or `<request>.GetHTTPHeaders` methods when retrieving forwarded HTTP headers. See [Forward OAuth identity for the logged-in user]({{< relref "add-authentication-for-data-source-plugins.md#forward-oauth-identity-for-the-logged-in-user" >}}), [Forward cookies for the logged-in user
|
||||
]({{< relref "add-authentication-for-data-source-plugins.md#forward-cookies-for-the-logged-in-user" >}}) or [Forward user header for the logged-in user]({{< relref "add-authentication-for-data-source-plugins.md#forward-user-header-for-the-logged-in-user" >}}) for example usages.
|
||||
|
||||
#### Technical details
|
||||
|
||||
The grafana-plugin-sdk-go [v0.147.0](https://github.com/grafana/grafana-plugin-sdk-go/releases/tag/v0.147.0) introduces a new interface [ForwardHTTPHeaders](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#ForwardHTTPHeaders) that `QueryDataRequest`, `CheckHealthRequest` and `CallResourceRequest` implements.
|
||||
|
||||
Newly introduced forwarded HTTP headers in Grafana v9.4.0 are `X-Grafana-User`, `X-Panel-Id`, `X-Dashboard-Uid`, `X-Datasource-Uid` and `X-Grafana-Org-Id`. Internally these are prefixed with `http_` and sent as `http_<HTTP header name>` in [CheckHealthRequest.Headers](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#CheckHealthRequest) and [QueryDataRequest.Headers](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#QueryDataRequest). By using the [ForwardHTTPHeaders](https://pkg.go.dev/github.com/grafana/grafana-plugin-sdk-go@v0.147.0/backend#ForwardHTTPHeaders) methods you're guaranteed to be able to operate on HTTP headers without using the prefix, i.e. `X-Grafana-User`, `X-Panel-Id`, `X-Dashboard-Uid`, `X-Datasource-Uid` and `X-Grafana-Org-Id`.
|
||||
|
||||
## From version 9.1.x to 9.2.x
|
||||
|
||||
### React and React-dom as peer dependencies
|
||||
|
||||
@@ -64,7 +64,6 @@ Alpha features might be changed or removed without prior notice.
|
||||
| `live-pipeline` | Enable a generic live processing pipeline |
|
||||
| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread |
|
||||
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
|
||||
| `tempoApmTable` | Show APM table |
|
||||
| `publicDashboards` | Enables public access to dashboards |
|
||||
| `lokiLive` | Support WebSocket streaming for loki (early prototype) |
|
||||
| `lokiDataframeApi` | Use experimental loki api for WebSocket streaming (early prototype) |
|
||||
@@ -98,24 +97,22 @@ Alpha features might be changed or removed without prior notice.
|
||||
| `sessionRemoteCache` | Enable using remote cache for user sessions |
|
||||
| `alertingBacktesting` | Rule backtesting API for alerting |
|
||||
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
|
||||
| `azureMultipleResourcePicker` | Azure multiple resource picker |
|
||||
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
The following toggles require explicitly setting Grafana's [app mode]({{< relref "../_index.md/#app_mode" >}}) to 'development' before you can enable this feature toggle. These features tend to be experimental.
|
||||
|
||||
| Feature toggle name | Description |
|
||||
| -------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI |
|
||||
| `showFeatureFlagsInUI` | Show feature flags in the settings UI |
|
||||
| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails |
|
||||
| `k8s` | Explore native k8s integrations |
|
||||
| `k8sDashboards` | Save dashboards via k8s |
|
||||
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
|
||||
| `export` | Export grafana instance (to git, etc) |
|
||||
| `azureMonitorResourcePickerForMetrics` | New UI for Azure Monitor Metrics Query |
|
||||
| `grpcServer` | Run GRPC server |
|
||||
| `entityStore` | SQL-based entity store (requires storage flag also) |
|
||||
| `queryLibrary` | Reusable query library |
|
||||
| `nestedFolders` | Enable folder nesting |
|
||||
| Feature toggle name | Description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------- |
|
||||
| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI |
|
||||
| `showFeatureFlagsInUI` | Show feature flags in the settings UI |
|
||||
| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails |
|
||||
| `k8s` | Explore native k8s integrations |
|
||||
| `k8sDashboards` | Save dashboards via k8s |
|
||||
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
|
||||
| `export` | Export grafana instance (to git, etc) |
|
||||
| `grpcServer` | Run GRPC server |
|
||||
| `entityStore` | SQL-based entity store (requires storage flag also) |
|
||||
| `queryLibrary` | Reusable query library |
|
||||
| `nestedFolders` | Enable folder nesting |
|
||||
|
||||
@@ -459,3 +459,19 @@ Limit the maximum device scale factor that can be requested. Default is `4`.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Page zoom level
|
||||
|
||||
The following command sets a page zoom level. The default value is `1`. A value of `1.5` equals 150% zoom.
|
||||
|
||||
```bash
|
||||
RENDERING_VIEWPORT_PAGE_ZOOM_LEVEL=1
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"rendering": {
|
||||
"pageZoomLevel": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -187,7 +187,7 @@ Now, when you change the color in the panel editor, the fill color of the circle
|
||||
|
||||
Most panels visualize dynamic data from a Grafana data source. In this step, you'll create one circle per series, each with a radius equal to the last value in the series.
|
||||
|
||||
> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData DB](/docs/grafana/latest/features/datasources/testdata) data source while developing.
|
||||
> To use data from queries in your panel, you need to set up a data source. If you don't have one available, you can use the [TestData](/docs/grafana/latest/features/datasources/testdata) data source while developing.
|
||||
|
||||
The results from a data source query within your panel are available in the `data` property inside your panel component.
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ Each data source provisioning config file contains a _manifest_ that specifies t
|
||||
|
||||
At startup, Grafana loads the configuration files and provisions the data sources listed in the manifests.
|
||||
|
||||
Let's configure a [TestData DB](/docs/grafana/latest/features/datasources/testdata/) data source that you can use for your dashboards.
|
||||
Let's configure a [TestData](/docs/grafana/latest/features/datasources/testdata/) data source that you can use for your dashboards.
|
||||
|
||||
#### Create a data source manifest
|
||||
|
||||
@@ -84,12 +84,12 @@ Let's configure a [TestData DB](/docs/grafana/latest/features/datasources/testda
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: TestData DB
|
||||
- name: TestData
|
||||
type: testdata
|
||||
```
|
||||
|
||||
1. Restart Grafana to load the new changes.
|
||||
1. In the sidebar, hover the cursor over the **Configuration** (gear) icon and click **Data Sources**. The TestData DB appears in the list of data sources.
|
||||
1. In the sidebar, hover the cursor over the **Configuration** (gear) icon and click **Data Sources**. TestData appears in the list of data sources.
|
||||
|
||||
> The configuration options can vary between different types of data sources. For more information on how to configure a specific data source, refer to [Data sources](/docs/grafana/latest/administration/provisioning/#datasources).
|
||||
|
||||
@@ -146,7 +146,7 @@ For more information on how to configure dashboard providers, refer to [Dashboar
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "TestData DB",
|
||||
"datasource": "TestData",
|
||||
"fill": 1,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
|
||||
@@ -248,6 +248,8 @@ e2e.scenario({
|
||||
.parent()
|
||||
.find('input')
|
||||
.type('microsoft.storage/storageaccounts{downArrow}{enter}');
|
||||
e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('button').click();
|
||||
e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('input').type('uk south{downArrow}{enter}');
|
||||
e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource').parent().find('button').click();
|
||||
e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource')
|
||||
.parent()
|
||||
@@ -262,8 +264,7 @@ e2e.scenario({
|
||||
e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription');
|
||||
e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups');
|
||||
e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces');
|
||||
// TODO: Enable this input once multiple resources feature flag is removed
|
||||
// e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region');
|
||||
e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region');
|
||||
e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource');
|
||||
e2eSelectors.queryEditor.resourcePicker.apply.button().click();
|
||||
e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}');
|
||||
|
||||
21
e2e/sql-suite/datasets-response.json
Normal file
21
e2e/sql-suite/datasets-response.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"results": {
|
||||
"datasets": {
|
||||
"status": 200,
|
||||
"frames": [
|
||||
{
|
||||
"schema": {
|
||||
"refId": "datasets",
|
||||
"meta": {
|
||||
"executedQueryString": "SELECT DISTINCT TABLE_SCHEMA from information_schema.TABLES where TABLE_TYPE != 'SYSTEM VIEW' ORDER BY TABLE_SCHEMA"
|
||||
},
|
||||
"fields": [
|
||||
{ "name": "TABLE_SCHEMA", "type": "string", "typeInfo": { "frame": "string", "nullable": true } }
|
||||
]
|
||||
},
|
||||
"data": { "values": [["DataMaker", "mysql", "performance_schema", "sys"]] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
e2e/sql-suite/fields-response.json
Normal file
27
e2e/sql-suite/fields-response.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"results": {
|
||||
"fields": {
|
||||
"status": 200,
|
||||
"frames": [
|
||||
{
|
||||
"schema": {
|
||||
"refId": "fields",
|
||||
"meta": {
|
||||
"executedQueryString": "SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'DataMaker' AND table_name = 'RandomIntsWithTimes' ORDER BY column_name"
|
||||
},
|
||||
"fields": [
|
||||
{ "name": "COLUMN_NAME", "type": "string", "typeInfo": { "frame": "string", "nullable": true } },
|
||||
{ "name": "DATA_TYPE", "type": "string", "typeInfo": { "frame": "string", "nullable": true } }
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"values": [
|
||||
["createdAt", "id", "time", "updatedAt", "bigint"],
|
||||
["datetime", "int", "datetime", "datetime", "int"]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
59
e2e/sql-suite/mysql.spec.ts
Normal file
59
e2e/sql-suite/mysql.spec.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { e2e } from '@grafana/e2e';
|
||||
|
||||
import datasetResponse from './datasets-response.json';
|
||||
import fieldsResponse from './fields-response.json';
|
||||
import tablesResponse from './tables-response.json';
|
||||
|
||||
const tableNameWithSpecialCharacter = tablesResponse.results.tables.frames[0].data.values[0][1];
|
||||
const normalTableName = tablesResponse.results.tables.frames[0].data.values[0][0];
|
||||
|
||||
describe('MySQL datasource', () => {
|
||||
it('code editor autocomplete should handle table name escaping/quoting', () => {
|
||||
e2e.flows.login('admin', 'admin');
|
||||
|
||||
e2e().intercept('POST', '**/api/ds/query', (req) => {
|
||||
if (req.body.queries[0].refId === 'datasets') {
|
||||
req.alias = 'datasets';
|
||||
req.reply({
|
||||
body: datasetResponse,
|
||||
});
|
||||
} else if (req.body.queries[0].refId === 'tables') {
|
||||
req.alias = 'tables';
|
||||
req.reply({
|
||||
body: tablesResponse,
|
||||
});
|
||||
} else if (req.body.queries[0].refId === 'fields') {
|
||||
req.alias = 'fields';
|
||||
req.reply({
|
||||
body: fieldsResponse,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
e2e.pages.Explore.visit();
|
||||
|
||||
e2e.components.DataSourcePicker.container().should('be.visible').type('gdev-mysql{enter}');
|
||||
|
||||
e2e().get("label[for^='option-code']").should('be.visible').click();
|
||||
e2e().get('textarea').type('S{downArrow}{enter}');
|
||||
e2e().wait('@tables');
|
||||
e2e().get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible');
|
||||
e2e().get('textarea').type('{enter}');
|
||||
e2e().get('textarea').should('have.value', `SELECT FROM grafana.\`${tableNameWithSpecialCharacter}\``);
|
||||
|
||||
const deleteTimes = new Array(tableNameWithSpecialCharacter.length + 2).fill(
|
||||
'{backspace}',
|
||||
0,
|
||||
tableNameWithSpecialCharacter.length + 2
|
||||
);
|
||||
e2e().get('textarea').type(deleteTimes.join(''));
|
||||
|
||||
e2e().get('textarea').type('{command}i');
|
||||
e2e().get('.suggest-widget').contains(tableNameWithSpecialCharacter).should('be.visible');
|
||||
e2e().get('textarea').type('S{downArrow}{enter}');
|
||||
e2e().get('textarea').should('have.value', `SELECT FROM grafana.${normalTableName}`);
|
||||
|
||||
e2e().get('textarea').type('.');
|
||||
e2e().get('.suggest-widget').contains('No suggestions.').should('be.visible');
|
||||
});
|
||||
});
|
||||
19
e2e/sql-suite/tables-response.json
Normal file
19
e2e/sql-suite/tables-response.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"results": {
|
||||
"tables": {
|
||||
"status": 200,
|
||||
"frames": [
|
||||
{
|
||||
"schema": {
|
||||
"refId": "tables",
|
||||
"meta": {
|
||||
"executedQueryString": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'DataMaker' ORDER BY table_name"
|
||||
},
|
||||
"fields": [{ "name": "TABLE_NAME", "type": "string", "typeInfo": { "frame": "string", "nullable": true } }]
|
||||
},
|
||||
"data": { "values": [["normalTable", "table-name"]] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
4
go.mod
4
go.mod
@@ -126,7 +126,7 @@ require (
|
||||
gopkg.in/square/go-jose.v2 v2.5.1
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
xorm.io/builder v0.3.6 // indirect
|
||||
xorm.io/builder v0.3.6
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v0.8.2
|
||||
)
|
||||
@@ -408,7 +408,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.20230123091136-3b6b1ec6c3cb
|
||||
replace github.com/crewjam/saml => github.com/grafana/saml v0.4.13-0.20230203140620-5f476db5c00a
|
||||
|
||||
// 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
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1418,8 +1418,8 @@ github.com/grafana/phlare/api v0.1.2 h1:1jrwd3KnsXMzj/tJih9likx5EvbY3pbvLbDqAAYe
|
||||
github.com/grafana/phlare/api v0.1.2/go.mod h1:29vcLwFDmZBDce2jwFIMtzvof7fzPadT8VMKw9ks7FU=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20230119183635-ec19b0a443b7 h1:ma1CfisUaAXQzL24tCao9yMleZYsFJ853m2l0rgahyE=
|
||||
github.com/grafana/prometheus-alertmanager v0.25.1-0.20230119183635-ec19b0a443b7/go.mod h1:MnBfDPXJqXmmfPwQlCLvVUdqfnvrAw+hSPtDeaaFwj4=
|
||||
github.com/grafana/saml v0.4.13-0.20230123091136-3b6b1ec6c3cb h1:9PLj02xp4DeLTM2+ZyBMcN1sh0ir8GuF/1xXKyF+yws=
|
||||
github.com/grafana/saml v0.4.13-0.20230123091136-3b6b1ec6c3cb/go.mod h1:igEejV+fihTIlHXYP8zOec3V5A8y3lws5bQBFsTm4gA=
|
||||
github.com/grafana/saml v0.4.13-0.20230203140620-5f476db5c00a h1:aWSTt/pTOI4uGY9DhBMG1l0GOnGjIYtaqxzYR3/q82o=
|
||||
github.com/grafana/saml v0.4.13-0.20230203140620-5f476db5c00a/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/thema v0.0.0-20230122235053-b4b6714dd1c9 h1:nAdsZkvPYNH6wDPkAi9JaDSIf5i2iVz4+Rqk4AOt6sE=
|
||||
|
||||
@@ -283,10 +283,18 @@ lineage: seqs: [
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
// TODO docs
|
||||
// FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it
|
||||
#Transformation: {
|
||||
#DataTransformerConfig: {
|
||||
@grafana(TSVeneer="type")
|
||||
|
||||
// Unique identifier of transformer
|
||||
id: string
|
||||
options: {...}
|
||||
// Disabled transformations are skipped
|
||||
disabled?: bool
|
||||
// Optional frame matcher. When missing it will be applied to all results
|
||||
filter?: #MatcherConfig
|
||||
// Options to be passed to the transformer
|
||||
// Valid options depend on the transformer id
|
||||
options: _
|
||||
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// 0 for no shared crosshair or tooltip (default).
|
||||
@@ -383,7 +391,7 @@ lineage: seqs: [
|
||||
// TODO docs
|
||||
timeRegions?: [...] @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
transformations: [...#Transformation] @grafanamaturity(NeedsExpertReview)
|
||||
transformations: [...#DataTransformerConfig] @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// TODO docs
|
||||
// TODO tighter constraint
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "9.3.6",
|
||||
"testing": "9.3.0"
|
||||
"testing": "9.4.0-beta1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "9.4.0-pre"
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "9.4.1"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"repository": "github:grafana/grafana",
|
||||
"scripts": {
|
||||
"build": "yarn i18n:compile && NODE_ENV=production webpack --progress --config scripts/webpack/webpack.prod.js",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.1",
|
||||
"@grafana/schema": "9.4.0-pre",
|
||||
"@grafana/schema": "9.4.1",
|
||||
"@types/d3-interpolate": "^3.0.0",
|
||||
"d3-interpolate": "3.0.1",
|
||||
"date-fns": "2.29.3",
|
||||
|
||||
@@ -35,7 +35,6 @@ export enum FrameMatcherID {
|
||||
byName = 'byName',
|
||||
byRefId = 'byRefId',
|
||||
byIndex = 'byIndex',
|
||||
byLabel = 'byLabel',
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,10 +3,11 @@ import { FieldType } from '../types';
|
||||
import { mockTransformationsRegistry } from '../utils/tests/mockTransformationsRegistry';
|
||||
|
||||
import { ReducerID } from './fieldReducer';
|
||||
import { FrameMatcherID } from './matchers/ids';
|
||||
import { transformDataFrame } from './transformDataFrame';
|
||||
import { filterFieldsByNameTransformer } from './transformers/filterByName';
|
||||
import { DataTransformerID } from './transformers/ids';
|
||||
import { reduceTransformer } from './transformers/reduce';
|
||||
import { reduceTransformer, ReduceTransformerMode } from './transformers/reduce';
|
||||
|
||||
const seriesAWithSingleField = toDataFrame({
|
||||
name: 'A',
|
||||
@@ -73,4 +74,44 @@ describe('transformDataFrame', () => {
|
||||
expect(processed[0].fields[0].values.get(0)).toEqual('temperature');
|
||||
});
|
||||
});
|
||||
|
||||
it('Support filtering', async () => {
|
||||
const frameA = toDataFrame({
|
||||
refId: 'A',
|
||||
fields: [{ name: 'value', type: FieldType.number, values: [5, 6] }],
|
||||
});
|
||||
const frameB = toDataFrame({
|
||||
refId: 'B',
|
||||
fields: [{ name: 'value', type: FieldType.number, values: [7, 8] }],
|
||||
});
|
||||
|
||||
const cfg = [
|
||||
{
|
||||
id: DataTransformerID.reduce,
|
||||
filter: {
|
||||
id: FrameMatcherID.byRefId,
|
||||
options: 'A', // Only apply to A
|
||||
},
|
||||
options: {
|
||||
reducers: [ReducerID.first],
|
||||
mode: ReduceTransformerMode.ReduceFields,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Only apply A
|
||||
await expect(transformDataFrame(cfg, [frameA, frameB])).toEmitValuesWith((received) => {
|
||||
const processed = received[0].map((v) => v.fields[0].values.toArray());
|
||||
expect(processed).toBeTruthy();
|
||||
expect(processed).toMatchObject([[5], [7, 8]]);
|
||||
});
|
||||
|
||||
// Only apply to B
|
||||
cfg[0].filter.options = 'B';
|
||||
await expect(transformDataFrame(cfg, [frameA, frameB])).toEmitValuesWith((received) => {
|
||||
const processed = received[0].map((v) => v.fields[0].values.toArray());
|
||||
expect(processed).toBeTruthy();
|
||||
expect(processed).toMatchObject([[5, 6], [7]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { MonoTypeOperatorFunction, Observable, of } from 'rxjs';
|
||||
import { map, mergeMap } from 'rxjs/operators';
|
||||
|
||||
import { DataFrame, DataTransformContext, DataTransformerConfig } from '../types';
|
||||
import { DataFrame, DataTransformContext, DataTransformerConfig, FrameMatcher } from '../types';
|
||||
|
||||
import { getFrameMatchers } from './matchers';
|
||||
import { standardTransformersRegistry, TransformerRegistryItem } from './standardTransformersRegistry';
|
||||
|
||||
const getOperator =
|
||||
@@ -17,15 +18,30 @@ const getOperator =
|
||||
const defaultOptions = info.transformation.defaultOptions ?? {};
|
||||
const options = { ...defaultOptions, ...config.options };
|
||||
|
||||
const matcher = config.filter?.options ? getFrameMatchers(config.filter) : undefined;
|
||||
return source.pipe(
|
||||
mergeMap((before) =>
|
||||
of(before).pipe(info.transformation.operator(options, ctx), postProcessTransform(before, info))
|
||||
of(filterInput(before, matcher)).pipe(
|
||||
info.transformation.operator(options, ctx),
|
||||
postProcessTransform(before, info, matcher)
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
function filterInput(data: DataFrame[], matcher?: FrameMatcher) {
|
||||
if (matcher) {
|
||||
return data.filter((v) => matcher(v));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const postProcessTransform =
|
||||
(before: DataFrame[], info: TransformerRegistryItem<any>): MonoTypeOperatorFunction<DataFrame[]> =>
|
||||
(
|
||||
before: DataFrame[],
|
||||
info: TransformerRegistryItem<any>,
|
||||
matcher?: FrameMatcher
|
||||
): MonoTypeOperatorFunction<DataFrame[]> =>
|
||||
(source) =>
|
||||
source.pipe(
|
||||
map((after) => {
|
||||
@@ -46,6 +62,21 @@ const postProcessTransform =
|
||||
}
|
||||
}
|
||||
|
||||
// Add back the filtered out frames
|
||||
if (matcher) {
|
||||
// keep the frame order the same
|
||||
let insert = 0;
|
||||
const append = before.filter((v, idx) => {
|
||||
const keep = !matcher(v);
|
||||
if (keep && !insert) {
|
||||
insert = idx;
|
||||
}
|
||||
return keep;
|
||||
});
|
||||
if (append.length) {
|
||||
after.splice(insert, 0, ...append);
|
||||
}
|
||||
}
|
||||
return after;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface FeatureToggles {
|
||||
['live-service-web-worker']?: boolean;
|
||||
queryOverLive?: boolean;
|
||||
panelTitleSearch?: boolean;
|
||||
tempoApmTable?: boolean;
|
||||
prometheusAzureOverrideAudience?: boolean;
|
||||
showFeatureFlagsInUI?: boolean;
|
||||
publicDashboards?: boolean;
|
||||
@@ -47,7 +46,6 @@ export interface FeatureToggles {
|
||||
supportBundles?: boolean;
|
||||
dashboardsFromStorage?: boolean;
|
||||
export?: boolean;
|
||||
azureMonitorResourcePickerForMetrics?: boolean;
|
||||
exploreMixedDatasource?: boolean;
|
||||
tracing?: boolean;
|
||||
commandPalette?: boolean;
|
||||
@@ -90,7 +88,6 @@ export interface FeatureToggles {
|
||||
alertingBacktesting?: boolean;
|
||||
editPanelCSVDragAndDrop?: boolean;
|
||||
alertingNoNormalState?: boolean;
|
||||
azureMultipleResourcePicker?: boolean;
|
||||
topNavCommandPalette?: boolean;
|
||||
logsSampleInExplore?: boolean;
|
||||
logsContextDatasourceUi?: boolean;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
export type { MatcherConfig } from '@grafana/schema';
|
||||
import { MonoTypeOperatorFunction } from 'rxjs';
|
||||
|
||||
import { MatcherConfig, DataTransformerConfig } from '@grafana/schema';
|
||||
|
||||
import { RegistryItemWithOptions } from '../utils/Registry';
|
||||
|
||||
import { DataFrame, Field } from './dataFrame';
|
||||
import { InterpolateFunction } from './panel';
|
||||
|
||||
/** deprecated, use it from schema */
|
||||
export type { MatcherConfig };
|
||||
|
||||
/**
|
||||
* Context passed to transformDataFrame and to each transform operator
|
||||
*/
|
||||
@@ -37,22 +41,9 @@ export interface SynchronousDataTransformerInfo<TOptions = any> extends DataTran
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated use TransformationConfig from schema
|
||||
*/
|
||||
export interface DataTransformerConfig<TOptions = any> {
|
||||
/**
|
||||
* Unique identifier of transformer
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Disabled transformations are skipped
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Options to be passed to the transformer
|
||||
*/
|
||||
options: TOptions;
|
||||
}
|
||||
export type { DataTransformerConfig };
|
||||
|
||||
export type FrameMatcher = (frame: DataFrame) => boolean;
|
||||
export type FieldMatcher = (field: Field, frame: DataFrame, allFrames: DataFrame[]) => boolean;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { Location } from 'history';
|
||||
|
||||
import { GrafanaConfig } from '../types';
|
||||
|
||||
import { locationUtil } from './location';
|
||||
|
||||
describe('locationUtil', () => {
|
||||
@@ -29,9 +33,9 @@ describe('locationUtil', () => {
|
||||
describe('when appSubUrl configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: { appSubUrl: '/subUrl' } as any,
|
||||
getVariablesUrlParams: (() => {}) as any,
|
||||
getTimeRangeForUrl: (() => {}) as any,
|
||||
config: { appSubUrl: '/subUrl' } as GrafanaConfig,
|
||||
getVariablesUrlParams: jest.fn(),
|
||||
getTimeRangeForUrl: jest.fn(),
|
||||
});
|
||||
});
|
||||
test('relative url', () => {
|
||||
@@ -65,9 +69,9 @@ describe('locationUtil', () => {
|
||||
describe('when appSubUrl not configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: {} as any,
|
||||
getVariablesUrlParams: (() => {}) as any,
|
||||
getTimeRangeForUrl: (() => {}) as any,
|
||||
config: {} as GrafanaConfig,
|
||||
getVariablesUrlParams: jest.fn(),
|
||||
getTimeRangeForUrl: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,12 +119,102 @@ describe('locationUtil', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUrlForPartial', () => {
|
||||
const mockLocation: Location = {
|
||||
hash: '',
|
||||
pathname: '/',
|
||||
search: '',
|
||||
state: {},
|
||||
};
|
||||
describe('when appSubUrl is not configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: {
|
||||
appSubUrl: '',
|
||||
} as GrafanaConfig,
|
||||
getVariablesUrlParams: jest.fn(),
|
||||
getTimeRangeForUrl: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('can add params', () => {
|
||||
expect(locationUtil.getUrlForPartial(mockLocation, { forceLogin: 'true' })).toEqual('/?forceLogin=true');
|
||||
});
|
||||
|
||||
it('can remove params using undefined', () => {
|
||||
expect(
|
||||
locationUtil.getUrlForPartial(
|
||||
{
|
||||
...mockLocation,
|
||||
search: '?a=1',
|
||||
},
|
||||
{ a: undefined }
|
||||
)
|
||||
).toEqual('/');
|
||||
});
|
||||
|
||||
it('can remove params using null', () => {
|
||||
expect(
|
||||
locationUtil.getUrlForPartial(
|
||||
{
|
||||
...mockLocation,
|
||||
search: '?a=1',
|
||||
},
|
||||
{ a: null }
|
||||
)
|
||||
).toEqual('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when appSubUrl is configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: {
|
||||
appSubUrl: '/subpath',
|
||||
} as GrafanaConfig,
|
||||
getVariablesUrlParams: jest.fn(),
|
||||
getTimeRangeForUrl: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('can add params', () => {
|
||||
expect(locationUtil.getUrlForPartial(mockLocation, { forceLogin: 'true' })).toEqual(
|
||||
'/subpath/?forceLogin=true'
|
||||
);
|
||||
});
|
||||
|
||||
it('can remove params using undefined', () => {
|
||||
expect(
|
||||
locationUtil.getUrlForPartial(
|
||||
{
|
||||
...mockLocation,
|
||||
search: '?a=1',
|
||||
},
|
||||
{ a: undefined }
|
||||
)
|
||||
).toEqual('/subpath/');
|
||||
});
|
||||
|
||||
it('can remove params using null', () => {
|
||||
expect(
|
||||
locationUtil.getUrlForPartial(
|
||||
{
|
||||
...mockLocation,
|
||||
search: '?a=1',
|
||||
},
|
||||
{ a: null }
|
||||
)
|
||||
).toEqual('/subpath/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSearchParams', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: {} as any,
|
||||
getVariablesUrlParams: (() => {}) as any,
|
||||
getTimeRangeForUrl: (() => {}) as any,
|
||||
config: {} as GrafanaConfig,
|
||||
getVariablesUrlParams: jest.fn(),
|
||||
getTimeRangeForUrl: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const getUrlForPartial = (location: Location<any>, searchParamsToUpdate: Record<
|
||||
searchParams[key] = searchParamsToUpdate[key];
|
||||
}
|
||||
}
|
||||
return urlUtil.renderUrl(location.pathname, searchParams);
|
||||
return assureBaseUrl(urlUtil.renderUrl(location.pathname, searchParams));
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e-selectors",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"description": "Grafana End-to-End Test Selectors Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@babel/core": "7.20.5",
|
||||
"@babel/preset-env": "7.20.2",
|
||||
"@cypress/webpack-preprocessor": "5.16.0",
|
||||
"@grafana/e2e-selectors": "9.4.0-pre",
|
||||
"@grafana/e2e-selectors": "9.4.1",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"babel-loader": "9.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@grafana/eslint-plugin",
|
||||
"description": "ESLint rules for use within the Grafana repo. Not suitable (or supported) for external use.",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"main": "./index.cjs",
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -37,10 +37,10 @@
|
||||
"postpack": "mv package.json.bak package.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
"@grafana/e2e-selectors": "9.4.0-pre",
|
||||
"@grafana/data": "9.4.1",
|
||||
"@grafana/e2e-selectors": "9.4.1",
|
||||
"@grafana/faro-web-sdk": "1.0.0-beta2",
|
||||
"@grafana/ui": "9.4.0-pre",
|
||||
"@grafana/ui": "9.4.1",
|
||||
"@sentry/browser": "6.19.7",
|
||||
"history": "4.10.1",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -2,30 +2,30 @@ import { lastValueFrom, merge, Observable, of } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataFrame,
|
||||
dataFrameToJSON,
|
||||
DataQuery,
|
||||
DataQueryRequest,
|
||||
DataQueryResponse,
|
||||
DataSourceApi,
|
||||
DataSourceInstanceSettings,
|
||||
DataQuery,
|
||||
DataSourceJsonData,
|
||||
ScopedVars,
|
||||
makeClassES5Compatible,
|
||||
DataFrame,
|
||||
parseLiveChannelAddress,
|
||||
getDataSourceRef,
|
||||
DataSourceRef,
|
||||
dataFrameToJSON,
|
||||
getDataSourceRef,
|
||||
makeClassES5Compatible,
|
||||
parseLiveChannelAddress,
|
||||
ScopedVars,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { config } from '../config';
|
||||
import {
|
||||
BackendSrvRequest,
|
||||
FetchResponse,
|
||||
getBackendSrv,
|
||||
getDataSourceSrv,
|
||||
getGrafanaLiveSrv,
|
||||
StreamingFrameOptions,
|
||||
StreamingFrameAction,
|
||||
BackendSrvRequest,
|
||||
FetchResponse,
|
||||
StreamingFrameOptions,
|
||||
} from '../services';
|
||||
|
||||
import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/schema",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"description": "Grafana Schema Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -25,7 +25,6 @@ export type {
|
||||
RegexMap,
|
||||
SpecialValueMap,
|
||||
ValueMappingResult,
|
||||
Transformation,
|
||||
LibraryPanelRef,
|
||||
RowPanel,
|
||||
GraphPanel,
|
||||
@@ -62,6 +61,7 @@ export type {
|
||||
Dashboard,
|
||||
VariableModel,
|
||||
DataSourceRef,
|
||||
DataTransformerConfig,
|
||||
Panel,
|
||||
FieldConfigSource,
|
||||
MatcherConfig,
|
||||
|
||||
@@ -353,11 +353,25 @@ export interface ValueMappingResult {
|
||||
|
||||
/**
|
||||
* TODO docs
|
||||
* FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it
|
||||
*/
|
||||
export interface Transformation {
|
||||
export interface DataTransformerConfig {
|
||||
/**
|
||||
* Disabled transformations are skipped
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Optional frame matcher. When missing it will be applied to all results
|
||||
*/
|
||||
filter?: MatcherConfig;
|
||||
/**
|
||||
* Unique identifier of transformer
|
||||
*/
|
||||
id: string;
|
||||
options: Record<string, unknown>;
|
||||
/**
|
||||
* Options to be passed to the transformer
|
||||
* Valid options depend on the transformer id
|
||||
*/
|
||||
options: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,7 +484,7 @@ export interface Panel {
|
||||
* Panel title.
|
||||
*/
|
||||
title?: string;
|
||||
transformations: Array<Transformation>;
|
||||
transformations: Array<DataTransformerConfig>;
|
||||
/**
|
||||
* Whether to display the panel without a background.
|
||||
*/
|
||||
|
||||
@@ -47,6 +47,10 @@ export interface MatcherConfig<TConfig = any> extends raw.MatcherConfig {
|
||||
options?: TConfig;
|
||||
}
|
||||
|
||||
export interface DataTransformerConfig<TOptions = any> extends raw.DataTransformerConfig {
|
||||
options: TOptions;
|
||||
}
|
||||
|
||||
export const defaultDashboard = raw.defaultDashboard as Dashboard;
|
||||
export const defaultVariableModel = {
|
||||
...raw.defaultVariableModel,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -51,10 +51,10 @@
|
||||
"@babel/preset-env": "7.18.9",
|
||||
"@babel/preset-react": "7.18.6",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
"@grafana/data": "9.4.1",
|
||||
"@grafana/eslint-config": "5.0.0",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@grafana/ui": "9.4.0-pre",
|
||||
"@grafana/ui": "9.4.1",
|
||||
"@jest/core": "27.5.1",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/eslint": "8.4.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/ui",
|
||||
"version": "9.4.0-pre",
|
||||
"version": "9.4.1",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -49,9 +49,9 @@
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.10.5",
|
||||
"@emotion/react": "11.10.5",
|
||||
"@grafana/data": "9.4.0-pre",
|
||||
"@grafana/e2e-selectors": "9.4.0-pre",
|
||||
"@grafana/schema": "9.4.0-pre",
|
||||
"@grafana/data": "9.4.1",
|
||||
"@grafana/e2e-selectors": "9.4.1",
|
||||
"@grafana/schema": "9.4.1",
|
||||
"@leeoniya/ufuzzy": "0.9.0",
|
||||
"@monaco-editor/react": "4.4.6",
|
||||
"@popperjs/core": "2.11.6",
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ContextMenu: React.FC<ContextMenuProps> = React.memo(
|
||||
const OFFSET = 5;
|
||||
const collisions = {
|
||||
right: window.innerWidth < x + rect.width,
|
||||
bottom: window.innerHeight < rect.bottom + rect.height + OFFSET,
|
||||
bottom: window.innerHeight < y + rect.height + OFFSET,
|
||||
};
|
||||
|
||||
setPositionStyles({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { usePopperTooltip } from 'react-popper-tooltip';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
|
||||
@@ -12,12 +12,19 @@ export interface Props {
|
||||
overlay: React.ReactElement | (() => React.ReactElement);
|
||||
placement?: TooltipPlacement;
|
||||
children: React.ReactElement | ((isOpen: boolean) => React.ReactElement);
|
||||
/** Amount in pixels to nudge the dropdown vertically and horizontally, respectively. */
|
||||
offset?: [number, number];
|
||||
onVisibleChange?: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export const Dropdown = React.memo(({ children, overlay, placement }: Props) => {
|
||||
export const Dropdown = React.memo(({ children, overlay, placement, offset, onVisibleChange }: Props) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const transitionRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
onVisibleChange?.(show);
|
||||
}, [onVisibleChange, show]);
|
||||
|
||||
const { getArrowProps, getTooltipProps, setTooltipRef, setTriggerRef, visible } = usePopperTooltip({
|
||||
visible: show,
|
||||
placement: placement,
|
||||
@@ -25,7 +32,7 @@ export const Dropdown = React.memo(({ children, overlay, placement }: Props) =>
|
||||
interactive: true,
|
||||
delayHide: 0,
|
||||
delayShow: 0,
|
||||
offset: [0, 8],
|
||||
offset: offset ?? [0, 8],
|
||||
trigger: ['click'],
|
||||
});
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
|
||||
return {
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
gap: ${theme.spacing(labelPadding)};
|
||||
align-items: baseline;
|
||||
position: relative;
|
||||
|
||||
108
packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx
Normal file
108
packages/grafana-ui/src/components/PanelChrome/HoverWidget.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { css } from '@emotion/css';
|
||||
import classnames from 'classnames';
|
||||
import React, { ReactElement, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
import { PanelMenu } from './PanelMenu';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
menu: ReactElement | (() => ReactElement);
|
||||
title?: string;
|
||||
offset?: number;
|
||||
dragClass?: string;
|
||||
}
|
||||
|
||||
export function HoverWidget({ menu, title, dragClass, children, offset = -32 }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const draggableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Capture the pointer to keep the widget visible while dragging
|
||||
const onPointerDown = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
draggableRef.current?.setPointerCapture(e.pointerId);
|
||||
}, []);
|
||||
|
||||
const onPointerUp = useCallback((e: React.PointerEvent<HTMLDivElement>) => {
|
||||
draggableRef.current?.releasePointerCapture(e.pointerId);
|
||||
}, []);
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
if (children === undefined || React.Children.count(children) === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames(styles.container, { 'show-on-hover': !menuOpen })}
|
||||
style={{ top: `${offset}px` }}
|
||||
data-testid="hover-header-container"
|
||||
>
|
||||
<div
|
||||
className={classnames(styles.square, styles.draggable, dragClass)}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
ref={draggableRef}
|
||||
>
|
||||
<Icon name="draggabledots" />
|
||||
</div>
|
||||
{children}
|
||||
<div className={styles.square}>
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
placement="bottom"
|
||||
menuButtonClass={styles.menuButton}
|
||||
onVisibleChange={setMenuOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
hidden: css({
|
||||
visibility: 'hidden',
|
||||
opacity: '0',
|
||||
}),
|
||||
container: css({
|
||||
label: 'hover-container-widget',
|
||||
transition: `all .1s linear`,
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
boxSizing: 'border-box',
|
||||
alignItems: 'center',
|
||||
background: theme.colors.background.secondary,
|
||||
color: theme.colors.text.primary,
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: '1px',
|
||||
height: theme.spacing(4),
|
||||
boxShadow: theme.shadows.z1,
|
||||
}),
|
||||
square: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
width: theme.spacing(4),
|
||||
height: '100%',
|
||||
}),
|
||||
draggable: css({
|
||||
cursor: 'move',
|
||||
}),
|
||||
menuButton: css({
|
||||
color: theme.colors.text.primary,
|
||||
'&:hover': {
|
||||
background: 'inherit',
|
||||
},
|
||||
}),
|
||||
title: css({
|
||||
padding: theme.spacing(0.75),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { PanelChrome, PanelChromeProps } from '@grafana/ui';
|
||||
|
||||
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { HorizontalGroup, VerticalGroup } from '../Layout/Layout';
|
||||
import { HorizontalGroup } from '../Layout/Layout';
|
||||
import { Menu } from '../Menu/Menu';
|
||||
|
||||
const meta: ComponentMeta<typeof PanelChrome> = {
|
||||
@@ -91,8 +91,8 @@ export const Examples = () => {
|
||||
|
||||
return (
|
||||
<DashboardStoryCanvas>
|
||||
<HorizontalGroup spacing="md" align="flex-start">
|
||||
<VerticalGroup spacing="md">
|
||||
<div>
|
||||
<HorizontalGroup spacing="md" align="flex-start" wrap>
|
||||
{renderPanel('Has statusMessage', {
|
||||
title: 'Default title',
|
||||
statusMessage: 'Error text',
|
||||
@@ -116,8 +116,7 @@ export const Examples = () => {
|
||||
title: 'Default title',
|
||||
loadingState: LoadingState.Loading,
|
||||
})}
|
||||
</VerticalGroup>
|
||||
<VerticalGroup spacing="md">
|
||||
|
||||
{renderPanel('Default panel: no non-required props')}
|
||||
{renderPanel('No padding', {
|
||||
padding: 'none',
|
||||
@@ -131,8 +130,7 @@ export const Examples = () => {
|
||||
{renderPanel('No title, loading loadingState', {
|
||||
loadingState: LoadingState.Loading,
|
||||
})}
|
||||
</VerticalGroup>
|
||||
<VerticalGroup spacing="md">
|
||||
|
||||
{renderPanel('Error status, menu', {
|
||||
title: 'Default title',
|
||||
menu,
|
||||
@@ -155,16 +153,11 @@ export const Examples = () => {
|
||||
menu,
|
||||
loadingState: LoadingState.Streaming,
|
||||
})}
|
||||
|
||||
{renderPanel('loadingState is Loading, menu', {
|
||||
title: 'Default title',
|
||||
menu,
|
||||
loadingState: LoadingState.Loading,
|
||||
})}
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup spacing="md" align="flex-start">
|
||||
<VerticalGroup spacing="md">
|
||||
{renderPanel('Deprecated error indicator', {
|
||||
title: 'Default title',
|
||||
leftItems: [
|
||||
@@ -186,8 +179,6 @@ export const Examples = () => {
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
</VerticalGroup>
|
||||
<VerticalGroup spacing="md">
|
||||
{renderPanel('Deprecated error indicator, menu', {
|
||||
title: 'Default title',
|
||||
menu,
|
||||
@@ -199,8 +190,14 @@ export const Examples = () => {
|
||||
/>,
|
||||
],
|
||||
})}
|
||||
</VerticalGroup>
|
||||
</HorizontalGroup>
|
||||
{renderPanel('Display mode = transparent', {
|
||||
title: 'Default title',
|
||||
displayMode: 'transparent',
|
||||
menu,
|
||||
leftItems: [],
|
||||
})}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</DashboardStoryCanvas>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -60,22 +60,12 @@ it('renders panel with a header if prop leftItems', () => {
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// todo implement when hoverHeader is implemented
|
||||
it.skip('renders panel without header if no title, no leftItems, and hoverHeader is undefined', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
it('renders panel with hover header if no title, no leftItems, hoverHeader is undefined but menu is present', () => {
|
||||
setup({ title: '', leftItems: undefined, hoverHeader: undefined, menu: <div>Menu</div> });
|
||||
expect(screen.getByTestId('hover-header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// todo implement when hoverHeader is implemented
|
||||
it.skip('renders panel with a fixed header if prop hoverHeader is false', () => {
|
||||
setup({ hoverHeader: false });
|
||||
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// todo implement when hoverHeader is implemented
|
||||
it.skip('renders panel with a hovering header if prop hoverHeader is true', () => {
|
||||
it('renders panel with a hovering header if prop hoverHeader is true', () => {
|
||||
setup({ title: 'Test Panel Header', hoverHeader: true });
|
||||
|
||||
expect(screen.queryByTestId('header-container')).not.toBeInTheDocument();
|
||||
@@ -97,10 +87,10 @@ it('renders panel with a header with icons in place if prop titleItems', () => {
|
||||
expect(screen.getByTestId('title-items-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders panel with a header if prop menu', () => {
|
||||
setup({ menu: <div> Menu </div> });
|
||||
it('renders panel with a hover header if prop menu is present and hoverHeader is false', () => {
|
||||
setup({ menu: <div> Menu </div>, hoverHeader: false });
|
||||
|
||||
expect(screen.getByTestId('header-container')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('hover-header-container')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders panel with a show-on-hover menu icon if prop menu', () => {
|
||||
|
||||
@@ -5,14 +5,15 @@ import { GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { useStyles2, useTheme2 } from '../../themes';
|
||||
import { Dropdown } from '../Dropdown/Dropdown';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { LoadingBar } from '../LoadingBar/LoadingBar';
|
||||
import { ToolbarButton } from '../ToolbarButton';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
import { HoverWidget } from './HoverWidget';
|
||||
import { PanelDescription } from './PanelDescription';
|
||||
import { PanelMenu } from './PanelMenu';
|
||||
import { PanelStatus } from './PanelStatus';
|
||||
import { TitleItem } from './TitleItem';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -22,9 +23,10 @@ export interface PanelChromeProps {
|
||||
height: number;
|
||||
children: (innerWidth: number, innerHeight: number) => ReactNode;
|
||||
padding?: PanelPadding;
|
||||
hoverHeaderOffset?: number;
|
||||
title?: string;
|
||||
description?: string | (() => string);
|
||||
titleItems?: ReactNode[];
|
||||
titleItems?: ReactNode;
|
||||
menu?: ReactElement | (() => ReactElement);
|
||||
dragClass?: string;
|
||||
dragClassCancel?: string;
|
||||
@@ -50,6 +52,7 @@ export interface PanelChromeProps {
|
||||
* of showing/interacting with the panel's state
|
||||
*/
|
||||
leftItems?: ReactNode[];
|
||||
displayMode?: 'default' | 'transparent';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,11 +70,13 @@ export function PanelChrome({
|
||||
padding = 'md',
|
||||
title = '',
|
||||
description = '',
|
||||
titleItems = [],
|
||||
displayMode = 'default',
|
||||
titleItems,
|
||||
menu,
|
||||
dragClass,
|
||||
dragClassCancel,
|
||||
hoverHeader = false,
|
||||
hoverHeaderOffset,
|
||||
loadingState,
|
||||
statusMessage,
|
||||
statusMessageOnClick,
|
||||
@@ -88,7 +93,7 @@ export function PanelChrome({
|
||||
const hasHeader =
|
||||
hoverHeader === false &&
|
||||
(title.length > 0 ||
|
||||
titleItems.length > 0 ||
|
||||
titleItems !== undefined ||
|
||||
description !== '' ||
|
||||
loadingState === LoadingState.Streaming ||
|
||||
(leftItems?.length ?? 0) > 0);
|
||||
@@ -101,71 +106,84 @@ export function PanelChrome({
|
||||
cursor: dragClass ? 'move' : 'auto',
|
||||
};
|
||||
|
||||
const itemStyles: CSSProperties = {
|
||||
minHeight: headerHeight,
|
||||
minWidth: headerHeight,
|
||||
};
|
||||
|
||||
const containerStyles: CSSProperties = { width, height };
|
||||
if (displayMode === 'transparent') {
|
||||
containerStyles.backgroundColor = 'transparent';
|
||||
containerStyles.border = 'none';
|
||||
}
|
||||
|
||||
const ariaLabel = title ? selectors.components.Panels.Panel.containerByTitle(title) : 'Panel';
|
||||
|
||||
const headerContent = (
|
||||
<>
|
||||
{title && (
|
||||
<h6 title={title} className={styles.title}>
|
||||
{title}
|
||||
</h6>
|
||||
)}
|
||||
|
||||
<PanelDescription description={description} className={dragClassCancel} />
|
||||
|
||||
{titleItems !== undefined && (
|
||||
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
|
||||
{titleItems}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingState === LoadingState.Streaming && (
|
||||
<Tooltip content="Streaming">
|
||||
<TitleItem className={dragClassCancel} data-testid="panel-streaming">
|
||||
<Icon name="circle-mono" size="md" className={styles.streaming} />
|
||||
</TitleItem>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container} style={containerStyles} aria-label={ariaLabel}>
|
||||
<div
|
||||
className={cx(styles.container, { [styles.regularHeader]: hasHeader })}
|
||||
style={containerStyles}
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
<div className={styles.loadingBarContainer}>
|
||||
{loadingState === LoadingState.Loading ? (
|
||||
<LoadingBar width={'28%'} height={'2px'} ariaLabel="Panel loading bar" />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className={cx(styles.headerContainer, dragClass)} style={headerStyles} data-testid="header-container">
|
||||
{title && (
|
||||
<h6 title={title} className={styles.title}>
|
||||
{title}
|
||||
</h6>
|
||||
)}
|
||||
{(hoverHeader || !hasHeader) && menu && (
|
||||
<HoverWidget menu={menu} title={title} offset={hoverHeaderOffset} dragClass={dragClass}>
|
||||
{headerContent}
|
||||
</HoverWidget>
|
||||
)}
|
||||
|
||||
<PanelDescription description={description} className={dragClassCancel} />
|
||||
{hasHeader && (
|
||||
<div className={cx(styles.headerContainer, dragClass)} style={headerStyles} data-testid="header-container">
|
||||
{headerContent}
|
||||
|
||||
{titleItems.length > 0 && (
|
||||
<div className={cx(styles.titleItems, dragClassCancel)} data-testid="title-items-container">
|
||||
{titleItems.map((item) => item)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingState === LoadingState.Streaming && (
|
||||
<div className={styles.item} style={itemStyles} data-testid="panel-streaming">
|
||||
<Tooltip content="Streaming">
|
||||
<Icon name="circle-mono" size="sm" className={styles.streaming} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.rightAligned}>
|
||||
{menu && (
|
||||
<Dropdown overlay={menu} placement="bottom">
|
||||
<ToolbarButton
|
||||
aria-label={`Menu for panel with ${title ? `title ${title}` : 'no title'}`}
|
||||
title="Menu"
|
||||
icon="ellipsis-v"
|
||||
narrow
|
||||
data-testid="panel-menu-button"
|
||||
className={cx(styles.menuItem, dragClassCancel, 'menu-icon')}
|
||||
<div className={styles.rightAligned}>
|
||||
{menu && (
|
||||
<PanelMenu
|
||||
menu={menu}
|
||||
title={title}
|
||||
menuButtonClass={cx(styles.menuItem, dragClassCancel, 'show-on-hover')}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
)}
|
||||
|
||||
{leftItems && <div className={styles.items}>{itemsRenderer(leftItems, (item) => item)}</div>}
|
||||
{leftItems && <div className={styles.leftItems}>{itemsRenderer(leftItems, (item) => item)}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{statusMessage && (
|
||||
<PanelStatus
|
||||
className={cx(styles.errorContainer, dragClassCancel)}
|
||||
message={statusMessage}
|
||||
onClick={statusMessageOnClick}
|
||||
ariaLabel="Panel status"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{statusMessage && (
|
||||
<PanelStatus
|
||||
className={cx(styles.errorContainer, dragClassCancel)}
|
||||
message={statusMessage}
|
||||
onClick={statusMessageOnClick}
|
||||
ariaLabel="Panel status"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className={styles.content} style={contentStyle}>
|
||||
{children(innerWidth, innerHeight)}
|
||||
@@ -210,7 +228,7 @@ const getContentStyle = (
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
const { background, borderColor } = theme.components.panel;
|
||||
const { background, borderColor, padding } = theme.components.panel;
|
||||
|
||||
return {
|
||||
container: css({
|
||||
@@ -224,10 +242,15 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
flexDirection: 'column',
|
||||
flex: '1 1 0',
|
||||
|
||||
'.show-on-hover': {
|
||||
visibility: 'hidden',
|
||||
opacity: '0',
|
||||
},
|
||||
'&:focus-visible, &:hover': {
|
||||
// only show menu icon on hover or focused panel
|
||||
'.menu-icon': {
|
||||
'.show-on-hover': {
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -235,6 +258,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
outline: `1px solid ${theme.colors.action.focus}`,
|
||||
},
|
||||
}),
|
||||
regularHeader: css({
|
||||
'&:focus-within': {
|
||||
'.show-on-hover': {
|
||||
visibility: 'visible',
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
}),
|
||||
loadingBarContainer: css({
|
||||
label: 'panel-loading-bar-container',
|
||||
position: 'absolute',
|
||||
@@ -251,7 +282,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
label: 'panel-header',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing(0, 0, 0, 1),
|
||||
padding: theme.spacing(0, 0, 0, padding),
|
||||
}),
|
||||
streaming: css({
|
||||
label: 'panel-streaming',
|
||||
@@ -265,9 +296,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
title: css({
|
||||
label: 'panel-title',
|
||||
marginBottom: 0, // override default h6 margin-bottom
|
||||
paddingRight: theme.spacing(1),
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: theme.spacing(50),
|
||||
fontSize: theme.typography.h6.fontSize,
|
||||
fontWeight: theme.typography.h6.fontWeight,
|
||||
}),
|
||||
@@ -289,6 +322,14 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: theme.zIndex.tooltip,
|
||||
}),
|
||||
leftItems: css({
|
||||
display: 'flex',
|
||||
paddingRight: theme.spacing(padding),
|
||||
}),
|
||||
rightAligned: css({
|
||||
label: 'right-aligned-container',
|
||||
@@ -298,9 +339,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
titleItems: css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
padding: theme.spacing(1),
|
||||
height: '100%',
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
|
||||
@@ -12,7 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function PanelDescription({ description, className }: Props) {
|
||||
const styles = getStyles();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const getDescriptionContent = (): JSX.Element => {
|
||||
// description
|
||||
@@ -28,13 +31,13 @@ export function PanelDescription({ description, className }: Props) {
|
||||
return description !== '' ? (
|
||||
<Tooltip interactive content={getDescriptionContent}>
|
||||
<TitleItem className={cx(className, styles.description)}>
|
||||
<Icon name="info-circle" size="lg" title="description" />
|
||||
<Icon name="info-circle" size="md" title="description" />
|
||||
</TitleItem>
|
||||
</Tooltip>
|
||||
) : null;
|
||||
}
|
||||
|
||||
const getStyles = () => {
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
description: css({
|
||||
code: {
|
||||
|
||||
40
packages/grafana-ui/src/components/PanelChrome/PanelMenu.tsx
Normal file
40
packages/grafana-ui/src/components/PanelChrome/PanelMenu.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
import { Dropdown } from '../Dropdown/Dropdown';
|
||||
import { ToolbarButton } from '../ToolbarButton';
|
||||
import { TooltipPlacement } from '../Tooltip';
|
||||
|
||||
interface PanelMenuProps {
|
||||
menu: ReactElement | (() => ReactElement);
|
||||
menuButtonClass?: string;
|
||||
dragClassCancel?: string;
|
||||
title?: string;
|
||||
placement?: TooltipPlacement;
|
||||
offset?: [number, number];
|
||||
onVisibleChange?: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export function PanelMenu({
|
||||
menu,
|
||||
title,
|
||||
placement = 'bottom',
|
||||
offset,
|
||||
dragClassCancel,
|
||||
menuButtonClass,
|
||||
onVisibleChange,
|
||||
}: PanelMenuProps) {
|
||||
return (
|
||||
<Dropdown overlay={menu} placement={placement} offset={offset} onVisibleChange={onVisibleChange}>
|
||||
<ToolbarButton
|
||||
aria-label={`Menu for panel with ${title ? `title ${title}` : 'no title'}`}
|
||||
title="Menu"
|
||||
icon="ellipsis-v"
|
||||
iconSize="md"
|
||||
narrow
|
||||
data-testid="panel-menu-button"
|
||||
className={cx(menuButtonClass, dragClassCancel)}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ export function PanelStatus({ className, message, onClick, ariaLabel = 'status'
|
||||
onClick={onClick}
|
||||
variant={'destructive'}
|
||||
icon="exclamation-triangle"
|
||||
iconSize="md"
|
||||
tooltip={message || ''}
|
||||
aria-label={ariaLabel}
|
||||
/>
|
||||
|
||||
@@ -50,12 +50,11 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
item: css({
|
||||
color: `${theme.colors.text.secondary}`,
|
||||
label: 'panel-header-item',
|
||||
backgroundColor: `${theme.colors.background.primary}`,
|
||||
cursor: 'auto',
|
||||
border: 'none',
|
||||
borderRadius: `${theme.shape.borderRadius()}`,
|
||||
padding: `${theme.spacing(0, 1)}`,
|
||||
height: `${theme.spacing(theme.components.height.md)}`,
|
||||
height: `${theme.spacing(theme.components.panel.headerHeight)}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
import { styleMixins, useStyles2 } from '../../themes';
|
||||
import { getFocusStyles, getMouseFocusStyles } from '../../themes/mixins';
|
||||
import { IconSize } from '../../types/icon';
|
||||
import { getPropertiesForVariant } from '../Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
@@ -13,6 +14,8 @@ import { Tooltip } from '../Tooltip';
|
||||
type CommonProps = {
|
||||
/** Icon name */
|
||||
icon?: IconName | React.ReactNode;
|
||||
/** Icon size */
|
||||
iconSize?: IconSize;
|
||||
/** Tooltip */
|
||||
tooltip?: string;
|
||||
/** For image icons */
|
||||
@@ -42,6 +45,7 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
|
||||
{
|
||||
tooltip,
|
||||
icon,
|
||||
iconSize,
|
||||
className,
|
||||
children,
|
||||
imgSrc,
|
||||
@@ -83,7 +87,7 @@ export const ToolbarButton = forwardRef<HTMLButtonElement, ToolbarButtonProps>(
|
||||
aria-expanded={isOpen}
|
||||
{...rest}
|
||||
>
|
||||
{renderIcon(icon)}
|
||||
{renderIcon(icon, iconSize)}
|
||||
{imgSrc && <img className={styles.img} src={imgSrc} alt={imgAlt ?? ''} />}
|
||||
{children && !iconOnly && <div className={contentStyles}>{children}</div>}
|
||||
{isOpen === false && <Icon name="angle-down" />}
|
||||
@@ -108,13 +112,13 @@ function getButtonAriaLabel(ariaLabel: string | undefined, tooltip: string | und
|
||||
return ariaLabel ? ariaLabel : tooltip ? selectors.components.PageToolbar.item(tooltip) : undefined;
|
||||
}
|
||||
|
||||
function renderIcon(icon: IconName | React.ReactNode) {
|
||||
function renderIcon(icon: IconName | React.ReactNode, iconSize?: IconSize) {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isIconName(icon)) {
|
||||
return <Icon name={icon} size="lg" />;
|
||||
return <Icon name={icon} size={`${iconSize ? iconSize : 'lg'}`} />;
|
||||
}
|
||||
|
||||
return icon;
|
||||
|
||||
@@ -739,9 +739,10 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
|
||||
|
||||
version, _ := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 32)
|
||||
query := dashver.GetDashboardVersionQuery{
|
||||
OrgID: c.OrgID,
|
||||
DashboardID: dash.ID,
|
||||
Version: int(version),
|
||||
OrgID: c.OrgID,
|
||||
DashboardID: dash.ID,
|
||||
DashboardUID: dash.UID,
|
||||
Version: int(version),
|
||||
}
|
||||
|
||||
res, err := hs.dashboardVersionService.Get(c.Req.Context(), &query)
|
||||
@@ -757,7 +758,7 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R
|
||||
dashVersionMeta := &dashver.DashboardVersionMeta{
|
||||
ID: res.ID,
|
||||
DashboardID: res.DashboardID,
|
||||
DashboardUID: dashUID,
|
||||
DashboardUID: dash.UID,
|
||||
Data: res.Data,
|
||||
ParentVersion: res.ParentVersion,
|
||||
RestoredFrom: res.RestoredFrom,
|
||||
@@ -989,7 +990,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
versionQuery := dashver.GetDashboardVersionQuery{DashboardID: dashID, Version: apiCmd.Version, OrgID: c.OrgID}
|
||||
versionQuery := dashver.GetDashboardVersionQuery{DashboardID: dashID, DashboardUID: dash.UID, Version: apiCmd.Version, OrgID: c.OrgID}
|
||||
version, err := hs.dashboardVersionService.Get(c.Req.Context(), &versionQuery)
|
||||
if err != nil {
|
||||
return response.Error(404, "Dashboard version not found", nil)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
@@ -268,7 +267,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, enabledPlugin
|
||||
url := ds.Url
|
||||
|
||||
if ds.Access == datasources.DS_ACCESS_PROXY {
|
||||
url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
|
||||
url = "/api/datasources/proxy/uid/" + ds.Uid
|
||||
}
|
||||
|
||||
dsDTO := plugins.DataSourceDTO{
|
||||
|
||||
@@ -52,4 +52,8 @@ var (
|
||||
EnvVars: []string{"GITHUB_TOKEN"},
|
||||
Usage: "GitHub token",
|
||||
}
|
||||
tagFlag = cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Grafana version tag",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -9,6 +9,13 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var additionalCommands []*cli.Command = make([]*cli.Command, 0, 5)
|
||||
|
||||
//nolint:unused
|
||||
func registerAppCommand(c *cli.Command) {
|
||||
additionalCommands = append(additionalCommands, c)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Commands = cli.Commands{
|
||||
@@ -189,6 +196,59 @@ func main() {
|
||||
Name: "artifacts",
|
||||
Usage: "Handle Grafana artifacts",
|
||||
Subcommands: cli.Commands{
|
||||
{
|
||||
Name: "publish",
|
||||
Usage: "Publish Grafana artifacts",
|
||||
Action: PublishArtifactsAction,
|
||||
Flags: []cli.Flag{
|
||||
&editionFlag,
|
||||
&cli.BoolFlag{
|
||||
Name: "security",
|
||||
Usage: "Security release",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "security-dest-bucket",
|
||||
Usage: "Google Cloud Storage bucket for security packages (or $SECURITY_DEST_BUCKET)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Grafana version tag",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "src-bucket",
|
||||
Value: "grafana-prerelease",
|
||||
Usage: "Google Cloud Storage bucket",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dest-bucket",
|
||||
Value: "grafana-downloads",
|
||||
Usage: "Google Cloud Storage bucket for published packages",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "enterprise2-dest-bucket",
|
||||
Value: "grafana-downloads-enterprise2",
|
||||
Usage: "Google Cloud Storage bucket for published packages",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "enterprise2-security-prefix",
|
||||
Usage: "Bucket path prefix for enterprise2 security releases (or $ENTERPRISE2_SECURITY_PREFIX)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "static-assets-bucket",
|
||||
Value: "grafana-static-assets",
|
||||
Usage: "Google Cloud Storage bucket for static assets",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "static-asset-editions",
|
||||
Usage: "All the editions of the static assets (or $STATIC_ASSET_EDITIONS)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "storybook-bucket",
|
||||
Value: "grafana-storybook",
|
||||
Usage: "Google Cloud Storage bucket for storybooks",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "docker",
|
||||
Usage: "Handle Grafana Docker images",
|
||||
@@ -226,10 +286,7 @@ func main() {
|
||||
ArgsUsage: "[version]",
|
||||
Action: NpmReleaseAction,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Grafana version tag",
|
||||
},
|
||||
&tagFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -237,10 +294,7 @@ func main() {
|
||||
Usage: "Store npm packages tarball",
|
||||
Action: NpmStoreAction,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Grafana version tag",
|
||||
},
|
||||
&tagFlag,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -248,10 +302,7 @@ func main() {
|
||||
Usage: "Retrieve npm packages tarball",
|
||||
Action: NpmRetrieveAction,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Grafana version tag",
|
||||
},
|
||||
&tagFlag,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -363,6 +414,8 @@ func main() {
|
||||
},
|
||||
}
|
||||
|
||||
app.Commands = append(app.Commands, additionalCommands...)
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
211
pkg/build/cmd/publishartifacts.go
Normal file
211
pkg/build/cmd/publishartifacts.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/gcloud"
|
||||
"github.com/grafana/grafana/pkg/build/versions"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type publishConfig struct {
|
||||
tag string
|
||||
srcBucket string
|
||||
destBucket string
|
||||
enterprise2DestBucket string
|
||||
enterprise2SecurityPrefix string
|
||||
staticAssetsBucket string
|
||||
staticAssetEditions []string
|
||||
storybookBucket string
|
||||
security bool
|
||||
}
|
||||
|
||||
// requireListWithEnvFallback first checks the CLI for a flag with the required
|
||||
// name. If this is empty, it falls back to taking the environment variable.
|
||||
// Sadly, we cannot use cli.Flag.EnvVars for this due to it potentially leaking
|
||||
// environment variables as default values in usage-errors.
|
||||
func requireListWithEnvFallback(cctx *cli.Context, name string, envName string) ([]string, error) {
|
||||
result := cctx.StringSlice(name)
|
||||
if len(result) == 0 {
|
||||
for _, v := range strings.Split(os.Getenv(envName), ",") {
|
||||
value := strings.TrimSpace(v)
|
||||
if value != "" {
|
||||
result = append(result, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil, cli.Exit(fmt.Sprintf("Required flag (%s) or environment variable (%s) not set", name, envName), 1)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func requireStringWithEnvFallback(cctx *cli.Context, name string, envName string) (string, error) {
|
||||
result := cctx.String(name)
|
||||
if result == "" {
|
||||
result = os.Getenv(envName)
|
||||
}
|
||||
if result == "" {
|
||||
return "", cli.Exit(fmt.Sprintf("Required flag (%s) or environment variable (%s) not set", name, envName), 1)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Action implements the sub-command "publish-artifacts".
|
||||
func PublishArtifactsAction(c *cli.Context) error {
|
||||
if c.NArg() > 0 {
|
||||
if err := cli.ShowSubcommandHelp(c); err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
return cli.Exit("", 1)
|
||||
}
|
||||
|
||||
staticAssetEditions, err := requireListWithEnvFallback(c, "static-asset-editions", "STATIC_ASSET_EDITIONS")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
securityDestBucket, err := requireStringWithEnvFallback(c, "security-dest-bucket", "SECURITY_DEST_BUCKET")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
enterprise2SecurityPrefix, err := requireStringWithEnvFallback(c, "enterprise2-security-prefix", "ENTERPRISE2_SECURITY_PREFIX")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gcloud.ActivateServiceAccount(); err != nil {
|
||||
return fmt.Errorf("error connecting to gcp, %q", err)
|
||||
}
|
||||
|
||||
cfg := publishConfig{
|
||||
srcBucket: c.String("src-bucket"),
|
||||
destBucket: c.String("dest-bucket"),
|
||||
enterprise2DestBucket: c.String("enterprise2-dest-bucket"),
|
||||
enterprise2SecurityPrefix: enterprise2SecurityPrefix,
|
||||
staticAssetsBucket: c.String("static-assets-bucket"),
|
||||
staticAssetEditions: staticAssetEditions,
|
||||
storybookBucket: c.String("storybook-bucket"),
|
||||
security: c.Bool("security"),
|
||||
tag: strings.TrimPrefix(c.String("tag"), "v"),
|
||||
}
|
||||
|
||||
if cfg.security {
|
||||
cfg.destBucket = securityDestBucket
|
||||
}
|
||||
|
||||
err = copyStaticAssets(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyStorybook(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyDownloads(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyEnterprise2Downloads(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyStaticAssets(cfg publishConfig) error {
|
||||
for _, edition := range cfg.staticAssetEditions {
|
||||
log.Printf("Copying static assets for %s", edition)
|
||||
srcURL := fmt.Sprintf("%s/artifacts/static-assets/%s/%s/*", cfg.srcBucket, edition, cfg.tag)
|
||||
destURL := fmt.Sprintf("%s/%s/%s/", cfg.staticAssetsBucket, edition, cfg.tag)
|
||||
err := gcsCopy("static assets", srcURL, destURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying static assets, %q", err)
|
||||
}
|
||||
}
|
||||
log.Printf("Successfully copied static assets!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyStorybook(cfg publishConfig) error {
|
||||
if cfg.security {
|
||||
log.Printf("skipping storybook copy - not needed for a security release")
|
||||
return nil
|
||||
}
|
||||
log.Printf("Copying storybooks...")
|
||||
srcURL := fmt.Sprintf("%s/artifacts/storybook/v%s/*", cfg.srcBucket, cfg.tag)
|
||||
destURL := fmt.Sprintf("%s/%s", cfg.storybookBucket, cfg.tag)
|
||||
err := gcsCopy("storybook", srcURL, destURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying storybook. %q", err)
|
||||
}
|
||||
stableVersion, err := versions.GetLatestVersion(versions.LatestStableVersionURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
isLatest, err := versions.IsGreaterThanOrEqual(cfg.tag, stableVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isLatest {
|
||||
log.Printf("Copying storybooks to latest...")
|
||||
srcURL := fmt.Sprintf("%s/artifacts/storybook/v%s/*", cfg.srcBucket, cfg.tag)
|
||||
destURL := fmt.Sprintf("%s/latest", cfg.storybookBucket)
|
||||
err := gcsCopy("storybook (latest)", srcURL, destURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying storybook to latest. %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Successfully copied storybook!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDownloads(cfg publishConfig) error {
|
||||
for _, edition := range []string{
|
||||
"oss", "enterprise",
|
||||
} {
|
||||
destURL := fmt.Sprintf("%s/%s/", cfg.destBucket, edition)
|
||||
srcURL := fmt.Sprintf("%s/artifacts/downloads/v%s/%s/release/*", cfg.srcBucket, cfg.tag, edition)
|
||||
if !cfg.security {
|
||||
destURL = filepath.Join(destURL, "release")
|
||||
}
|
||||
log.Printf("Copying downloads for %s, from %s bucket to %s bucket", edition, srcURL, destURL)
|
||||
err := gcsCopy("downloads", srcURL, destURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying downloads, %q", err)
|
||||
}
|
||||
}
|
||||
log.Printf("Successfully copied downloads.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyEnterprise2Downloads(cfg publishConfig) error {
|
||||
var prefix string
|
||||
if cfg.security {
|
||||
prefix = cfg.enterprise2SecurityPrefix
|
||||
}
|
||||
srcURL := fmt.Sprintf("%s/artifacts/downloads-enterprise2/v%s/enterprise2/release/*", cfg.srcBucket, cfg.tag)
|
||||
destURL := fmt.Sprintf("%s/enterprise2/%srelease", cfg.enterprise2DestBucket, prefix)
|
||||
log.Printf("Copying downloads for enterprise2, from %s bucket to %s bucket", srcURL, destURL)
|
||||
err := gcsCopy("enterprise2 downloads", srcURL, destURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error copying ")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func gcsCopy(desc, src, dest string) error {
|
||||
args := strings.Split(fmt.Sprintf("-m cp -r gs://%s gs://%s", src, dest), " ")
|
||||
// nolint:gosec
|
||||
cmd := exec.Command("gsutil", args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to publish %s: %w\n%s", desc, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build requires_buildifier
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -334,6 +334,20 @@ type DataSourceRef struct {
|
||||
Uid *string `json:"uid,omitempty"`
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
type DataTransformerConfig struct {
|
||||
// Disabled transformations are skipped
|
||||
Disabled *bool `json:"disabled,omitempty"`
|
||||
Filter *MatcherConfig `json:"filter,omitempty"`
|
||||
|
||||
// Unique identifier of transformer
|
||||
Id string `json:"id"`
|
||||
|
||||
// Options to be passed to the transformer
|
||||
// Valid options depend on the transformer id
|
||||
Options interface{} `json:"options"`
|
||||
}
|
||||
|
||||
// DynamicConfigValue defines model for DynamicConfigValue.
|
||||
type DynamicConfigValue struct {
|
||||
Id string `json:"id"`
|
||||
@@ -545,8 +559,8 @@ type Panel struct {
|
||||
TimeShift *string `json:"timeShift,omitempty"`
|
||||
|
||||
// Panel title.
|
||||
Title *string `json:"title,omitempty"`
|
||||
Transformations []Transformation `json:"transformations"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
Transformations []DataTransformerConfig `json:"transformations"`
|
||||
|
||||
// Whether to display the panel without a background.
|
||||
Transparent bool `json:"transparent"`
|
||||
@@ -707,13 +721,6 @@ type ThresholdsConfig struct {
|
||||
// ThresholdsMode defines model for ThresholdsMode.
|
||||
type ThresholdsMode string
|
||||
|
||||
// TODO docs
|
||||
// FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it
|
||||
type Transformation struct {
|
||||
Id string `json:"id"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
}
|
||||
|
||||
// TODO docs
|
||||
type ValueMap struct {
|
||||
Options map[string]ValueMappingResult `json:"options"`
|
||||
|
||||
@@ -299,7 +299,6 @@ var irregularPluginNames = map[string]string{
|
||||
"azuremonitor": "grafana-azure-monitor-datasource",
|
||||
"microsoftsqlserver": "mssql",
|
||||
"postgresql": "postgres",
|
||||
"testdatadb": "testdata",
|
||||
}
|
||||
|
||||
func buildComposableLinks(pp plugindef.PluginDef, cp kindsys.ComposableProperties) KindLinks {
|
||||
|
||||
@@ -13,10 +13,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
@@ -344,9 +345,9 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
}
|
||||
|
||||
sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *auth.UserToken,
|
||||
clientIP net.IP, userAgent string) (bool, error) {
|
||||
clientIP net.IP, userAgent string) (bool, *auth.UserToken, error) {
|
||||
userToken.UnhashedToken = "rotated"
|
||||
return true, nil
|
||||
return true, userToken, nil
|
||||
}
|
||||
|
||||
maxAge := int(sc.cfg.LoginMaxLifetime.Seconds())
|
||||
|
||||
@@ -106,7 +106,7 @@ func UAEnabled(ctx context.Context) bool {
|
||||
return enabled
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, jsonWithPanels *simplejson.Json, validateAlertFunc func(*models.Alert) bool, logTranslationFailures bool, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) {
|
||||
func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, jsonWithPanels *simplejson.Json, validateAlertFunc func(*models.Alert) error, logTranslationFailures bool, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) {
|
||||
ret := make([]*models.Alert, 0)
|
||||
|
||||
for _, panelObj := range jsonWithPanels.Get("panels").MustArray() {
|
||||
@@ -238,8 +238,8 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !validateAlertFunc(alert) {
|
||||
return nil, ValidationError{Reason: fmt.Sprintf("Panel id is not correct, alertName=%v, panelId=%v", alert.Name, alert.PanelId)}
|
||||
if err := validateAlertFunc(alert); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = append(ret, alert)
|
||||
@@ -248,8 +248,14 @@ func (e *DashAlertExtractorService) getAlertFromPanels(ctx context.Context, json
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func validateAlertRule(alert *models.Alert) bool {
|
||||
return alert.ValidToSave()
|
||||
func validateAlertRule(alert *models.Alert) error {
|
||||
if !alert.ValidDashboardPanel() {
|
||||
return ValidationError{Reason: fmt.Sprintf("Panel id is not correct, alertName=%v, panelId=%v", alert.Name, alert.PanelId)}
|
||||
}
|
||||
if !alert.ValidTags() {
|
||||
return ValidationError{Reason: "Invalid tags, must be less than 100 characters"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAlerts extracts alerts from the dashboard json and does full validation on the alert json data.
|
||||
@@ -257,7 +263,7 @@ func (e *DashAlertExtractorService) GetAlerts(ctx context.Context, dashAlertInfo
|
||||
return e.extractAlerts(ctx, validateAlertRule, true, dashAlertInfo)
|
||||
}
|
||||
|
||||
func (e *DashAlertExtractorService) extractAlerts(ctx context.Context, validateFunc func(alert *models.Alert) bool, logTranslationFailures bool, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) {
|
||||
func (e *DashAlertExtractorService) extractAlerts(ctx context.Context, validateFunc func(alert *models.Alert) error, logTranslationFailures bool, dashAlertInfo DashAlertInfo) ([]*models.Alert, error) {
|
||||
dashboardJSON, err := copyJSON(dashAlertInfo.Dash.Data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -294,8 +300,11 @@ func (e *DashAlertExtractorService) extractAlerts(ctx context.Context, validateF
|
||||
// ValidateAlerts validates alerts in the dashboard json but does not require a valid dashboard id
|
||||
// in the first validation pass.
|
||||
func (e *DashAlertExtractorService) ValidateAlerts(ctx context.Context, dashAlertInfo DashAlertInfo) error {
|
||||
_, err := e.extractAlerts(ctx, func(alert *models.Alert) bool {
|
||||
return alert.OrgId != 0 && alert.PanelId != 0
|
||||
_, err := e.extractAlerts(ctx, func(alert *models.Alert) error {
|
||||
if alert.OrgId == 0 || alert.PanelId == 0 {
|
||||
return errors.New("missing OrgId, PanelId or both")
|
||||
}
|
||||
return nil
|
||||
}, false, dashAlertInfo)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -92,8 +92,17 @@ type Alert struct {
|
||||
Settings *simplejson.Json
|
||||
}
|
||||
|
||||
func (a *Alert) ValidToSave() bool {
|
||||
return a.DashboardId != 0 && a.OrgId != 0 && a.PanelId != 0
|
||||
func (a *Alert) ValidDashboardPanel() bool {
|
||||
return a.OrgId != 0 && a.DashboardId != 0 && a.PanelId != 0
|
||||
}
|
||||
|
||||
func (a *Alert) ValidTags() bool {
|
||||
for _, tag := range a.GetTagsFromSettings() {
|
||||
if len(tag.Key) > 100 || len(tag.Value) > 100 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *Alert) ContainsUpdates(other *Alert) bool {
|
||||
|
||||
@@ -62,7 +62,7 @@ type RevokeAuthTokenCmd struct {
|
||||
type UserTokenService interface {
|
||||
CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*UserToken, error)
|
||||
LookupToken(ctx context.Context, unhashedToken string) (*UserToken, error)
|
||||
TryRotateToken(ctx context.Context, token *UserToken, clientIP net.IP, userAgent string) (bool, error)
|
||||
TryRotateToken(ctx context.Context, token *UserToken, clientIP net.IP, userAgent string) (bool, *UserToken, error)
|
||||
RevokeToken(ctx context.Context, token *UserToken, soft bool) error
|
||||
RevokeAllUserTokens(ctx context.Context, userId int64) error
|
||||
GetUserToken(ctx context.Context, userId, userTokenId int64) (*UserToken, error)
|
||||
|
||||
@@ -5,10 +5,13 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
@@ -41,6 +44,7 @@ func ProvideUserAuthTokenService(sqlStore db.DB,
|
||||
log: log.New("auth"),
|
||||
remoteCache: remoteCache,
|
||||
features: features,
|
||||
singleflight: new(singleflight.Group),
|
||||
}
|
||||
|
||||
defaultLimits, err := readQuotaConfig(cfg)
|
||||
@@ -68,6 +72,7 @@ type UserAuthTokenService struct {
|
||||
log log.Logger
|
||||
remoteCache *remotecache.RemoteCache
|
||||
features *featuremgmt.FeatureManager
|
||||
singleflight *singleflight.Group
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
|
||||
@@ -202,6 +207,7 @@ func (s *UserAuthTokenService) lookupToken(ctx context.Context, unhashedToken st
|
||||
}
|
||||
}
|
||||
|
||||
// Current incoming token is the previous auth token in the DB and the auth_token_seen is true
|
||||
if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen {
|
||||
modelCopy := model
|
||||
modelCopy.AuthTokenSeen = false
|
||||
@@ -229,6 +235,7 @@ func (s *UserAuthTokenService) lookupToken(ctx context.Context, unhashedToken st
|
||||
}
|
||||
}
|
||||
|
||||
// Current incoming token is not seen and it is the latest valid auth token in the db
|
||||
if !model.AuthTokenSeen && model.AuthToken == hashedToken {
|
||||
modelCopy := model
|
||||
modelCopy.AuthTokenSeen = true
|
||||
@@ -268,83 +275,102 @@ func (s *UserAuthTokenService) lookupToken(ctx context.Context, unhashedToken st
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *auth.UserToken,
|
||||
clientIP net.IP, userAgent string) (bool, error) {
|
||||
clientIP net.IP, userAgent string) (bool, *auth.UserToken, error) {
|
||||
if token == nil {
|
||||
return false, nil
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
model, err := userAuthTokenFromUserToken(token)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
now := getTime()
|
||||
|
||||
var needsRotation bool
|
||||
rotatedAt := time.Unix(model.RotatedAt, 0)
|
||||
if model.AuthTokenSeen {
|
||||
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.cfg.TokenRotationIntervalMinutes) * time.Minute))
|
||||
} else {
|
||||
needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime))
|
||||
type rotationResult struct {
|
||||
rotated bool
|
||||
newToken *auth.UserToken
|
||||
}
|
||||
|
||||
if !needsRotation {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ctxLogger := s.log.FromContext(ctx)
|
||||
ctxLogger.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt)
|
||||
|
||||
clientIPStr := clientIP.String()
|
||||
if len(clientIP) == 0 {
|
||||
clientIPStr = ""
|
||||
}
|
||||
newToken, err := util.RandomHex(16)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
hashedToken := hashToken(newToken)
|
||||
|
||||
// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
|
||||
sql := `
|
||||
UPDATE user_auth_token
|
||||
SET
|
||||
seen_at = 0,
|
||||
user_agent = ?,
|
||||
client_ip = ?,
|
||||
prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end,
|
||||
auth_token = ?,
|
||||
auth_token_seen = ?,
|
||||
rotated_at = ?
|
||||
WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)`
|
||||
|
||||
var affected int64
|
||||
err = s.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
||||
res, err := dbSession.Exec(sql, userAgent, clientIPStr, s.sqlStore.GetDialect().BooleanStr(true), hashedToken,
|
||||
s.sqlStore.GetDialect().BooleanStr(false), now.Unix(), model.Id, s.sqlStore.GetDialect().BooleanStr(true),
|
||||
now.Add(-30*time.Second).Unix())
|
||||
if err != nil {
|
||||
return err
|
||||
rotResult, err, _ := s.singleflight.Do(fmt.Sprint(model.Id), func() (interface{}, error) {
|
||||
var needsRotation bool
|
||||
rotatedAt := time.Unix(model.RotatedAt, 0)
|
||||
if model.AuthTokenSeen {
|
||||
needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.cfg.TokenRotationIntervalMinutes) * time.Minute))
|
||||
} else {
|
||||
needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime))
|
||||
}
|
||||
|
||||
affected, err = res.RowsAffected()
|
||||
return err
|
||||
if !needsRotation {
|
||||
return &rotationResult{rotated: false}, nil
|
||||
}
|
||||
|
||||
ctxLogger := s.log.FromContext(ctx)
|
||||
ctxLogger.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt)
|
||||
|
||||
clientIPStr := clientIP.String()
|
||||
if len(clientIP) == 0 {
|
||||
clientIPStr = ""
|
||||
}
|
||||
newToken, err := util.RandomHex(16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hashedToken := hashToken(newToken)
|
||||
|
||||
// very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly
|
||||
sql := `
|
||||
UPDATE user_auth_token
|
||||
SET
|
||||
seen_at = 0,
|
||||
user_agent = ?,
|
||||
client_ip = ?,
|
||||
prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end,
|
||||
auth_token = ?,
|
||||
auth_token_seen = ?,
|
||||
rotated_at = ?
|
||||
WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)`
|
||||
|
||||
var affected int64
|
||||
err = s.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
|
||||
res, err := dbSession.Exec(sql, userAgent, clientIPStr, s.sqlStore.GetDialect().BooleanStr(true), hashedToken,
|
||||
s.sqlStore.GetDialect().BooleanStr(false), now.Unix(), model.Id, s.sqlStore.GetDialect().BooleanStr(true),
|
||||
now.Add(-30*time.Second).Unix())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
affected, err = res.RowsAffected()
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if affected > 0 {
|
||||
ctxLogger.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId)
|
||||
model.UnhashedToken = newToken
|
||||
var result auth.UserToken
|
||||
if err := model.toUserToken(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rotationResult{
|
||||
rotated: true,
|
||||
newToken: &result,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &rotationResult{rotated: false}, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
ctxLogger.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId)
|
||||
if affected > 0 {
|
||||
model.UnhashedToken = newToken
|
||||
if err := model.toUserToken(token); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
result := rotResult.(*rotationResult)
|
||||
|
||||
return false, nil
|
||||
return result.rotated, result.newToken, nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.UserToken, soft bool) error {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
@@ -178,7 +179,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
|
||||
getTime = func() time.Time { return now.Add(time.Hour) }
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
rotated, _, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("192.168.10.11"), "some user agent")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
@@ -262,7 +263,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
prevToken := userToken.AuthToken
|
||||
unhashedPrev := userToken.UnhashedToken
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
rotated, _, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("192.168.10.12"), "a new user agent")
|
||||
require.Nil(t, err)
|
||||
require.False(t, rotated)
|
||||
@@ -280,12 +281,12 @@ func TestUserAuthToken(t *testing.T) {
|
||||
|
||||
getTime = func() time.Time { return now.Add(time.Hour) }
|
||||
|
||||
rotated, err = ctx.tokenService.TryRotateToken(context.Background(), &tok,
|
||||
rotated, newToken, err := ctx.tokenService.TryRotateToken(context.Background(), &tok,
|
||||
net.ParseIP("192.168.10.12"), "a new user agent")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
|
||||
unhashedToken := tok.UnhashedToken
|
||||
unhashedToken := newToken.UnhashedToken
|
||||
|
||||
model, err = ctx.getAuthTokenByID(tok.Id)
|
||||
require.Nil(t, err)
|
||||
@@ -326,7 +327,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
require.NotNil(t, lookedUpModel)
|
||||
require.False(t, lookedUpModel.AuthTokenSeen)
|
||||
|
||||
rotated, err = ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
rotated, _, err = ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("192.168.10.12"), "a new user agent")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
@@ -351,7 +352,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
getTime = func() time.Time { return now.Add(10 * time.Minute) }
|
||||
|
||||
prevToken := userToken.UnhashedToken
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
rotated, _, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
@@ -407,7 +408,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
return now.Add(10 * time.Minute)
|
||||
}
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
rotated, _, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
@@ -429,7 +430,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
return now.Add(20 * time.Minute)
|
||||
}
|
||||
|
||||
rotated, err = ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
rotated, _, err = ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
@@ -456,7 +457,7 @@ func TestUserAuthToken(t *testing.T) {
|
||||
return now.Add(2 * time.Minute)
|
||||
}
|
||||
|
||||
rotated, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
rotated, _, err := ctx.tokenService.TryRotateToken(context.Background(), userToken,
|
||||
net.ParseIP("1.1.1.1"), "firefox")
|
||||
require.Nil(t, err)
|
||||
require.True(t, rotated)
|
||||
@@ -550,9 +551,10 @@ func createTestContext(t *testing.T) *testContext {
|
||||
}
|
||||
|
||||
tokenService := &UserAuthTokenService{
|
||||
sqlStore: sqlstore,
|
||||
cfg: cfg,
|
||||
log: log.New("test-logger"),
|
||||
sqlStore: sqlstore,
|
||||
cfg: cfg,
|
||||
log: log.New("test-logger"),
|
||||
singleflight: new(singleflight.Group),
|
||||
}
|
||||
|
||||
return &testContext{
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type FakeUserAuthTokenService struct {
|
||||
CreateTokenProvider func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error)
|
||||
TryRotateTokenProvider func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error)
|
||||
TryRotateTokenProvider func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, *auth.UserToken, error)
|
||||
LookupTokenProvider func(ctx context.Context, unhashedToken string) (*auth.UserToken, error)
|
||||
RevokeTokenProvider func(ctx context.Context, token *auth.UserToken, soft bool) error
|
||||
RevokeAllUserTokensProvider func(ctx context.Context, userId int64) error
|
||||
@@ -34,8 +34,8 @@ func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
|
||||
UnhashedToken: "",
|
||||
}, nil
|
||||
},
|
||||
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) {
|
||||
return false, nil
|
||||
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, *auth.UserToken, error) {
|
||||
return false, nil, nil
|
||||
},
|
||||
LookupTokenProvider: func(ctx context.Context, unhashedToken string) (*auth.UserToken, error) {
|
||||
return &auth.UserToken{
|
||||
@@ -79,7 +79,7 @@ func (s *FakeUserAuthTokenService) LookupToken(ctx context.Context, unhashedToke
|
||||
}
|
||||
|
||||
func (s *FakeUserAuthTokenService) TryRotateToken(ctx context.Context, token *auth.UserToken, clientIP net.IP,
|
||||
userAgent string) (bool, error) {
|
||||
userAgent string) (bool, *auth.UserToken, error) {
|
||||
return s.TryRotateTokenProvider(context.Background(), token, clientIP, userAgent)
|
||||
}
|
||||
|
||||
|
||||
@@ -107,13 +107,14 @@ func (s *Session) RefreshTokenHook(ctx context.Context, identity *authn.Identity
|
||||
s.log.Debug("failed to get client IP address", "addr", addr, "err", err)
|
||||
ip = nil
|
||||
}
|
||||
rotated, err := s.sessionService.TryRotateToken(ctx, identity.SessionToken, ip, userAgent)
|
||||
rotated, newToken, err := s.sessionService.TryRotateToken(ctx, identity.SessionToken, ip, userAgent)
|
||||
if err != nil {
|
||||
s.log.Error("failed to rotate token", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if rotated {
|
||||
identity.SessionToken = newToken
|
||||
s.log.Debug("rotated session token", "user", identity.ID)
|
||||
|
||||
maxAge := int(s.loginMaxLifetime.Seconds())
|
||||
|
||||
@@ -143,9 +143,9 @@ func (f *fakeResponseWriter) WriteHeader(statusCode int) {
|
||||
|
||||
func TestSession_RefreshHook(t *testing.T) {
|
||||
s := ProvideSession(&authtest.FakeUserAuthTokenService{
|
||||
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, error) {
|
||||
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, *auth.UserToken, error) {
|
||||
token.UnhashedToken = "new-token"
|
||||
return true, nil
|
||||
return true, token, nil
|
||||
},
|
||||
}, &usertest.FakeUserService{}, "grafana-session", 20*time.Second)
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/apikeygen"
|
||||
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
@@ -70,6 +72,7 @@ func ProvideService(cfg *setting.Cfg, tokenService auth.UserTokenService, jwtSer
|
||||
oauthTokenService: oauthTokenService,
|
||||
features: features,
|
||||
authnService: authnService,
|
||||
singleflight: new(singleflight.Group),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ type ContextHandler struct {
|
||||
oauthTokenService oauthtoken.OAuthTokenService
|
||||
features *featuremgmt.FeatureManager
|
||||
authnService authn.Service
|
||||
singleflight *singleflight.Group
|
||||
// GetTime returns the current time.
|
||||
// Stubbable by tests.
|
||||
GetTime func() time.Time
|
||||
@@ -568,15 +572,15 @@ func (h *ContextHandler) rotateEndOfRequestFunc(reqContext *contextmodel.ReqCont
|
||||
ip = nil
|
||||
}
|
||||
|
||||
// FIXME (jguer): rotation should return a new token instead of modifying the existing one.
|
||||
rotated, err := h.AuthTokenService.TryRotateToken(ctx, reqContext.UserToken, ip, reqContext.Req.UserAgent())
|
||||
rotated, newToken, err := h.AuthTokenService.TryRotateToken(ctx, reqContext.UserToken, ip, reqContext.Req.UserAgent())
|
||||
if err != nil {
|
||||
reqContext.Logger.Error("Failed to rotate token", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
if rotated {
|
||||
cookies.WriteSessionCookie(reqContext, h.Cfg, reqContext.UserToken.UnhashedToken, h.Cfg.LoginMaxLifetime)
|
||||
reqContext.UserToken = newToken
|
||||
cookies.WriteSessionCookie(reqContext, h.Cfg, newToken.UnhashedToken, h.Cfg.LoginMaxLifetime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ func TestDontRotateTokensOnCancelledRequests(t *testing.T) {
|
||||
tryRotateCallCount := 0
|
||||
ctxHdlr.AuthTokenService = &authtest.FakeUserAuthTokenService{
|
||||
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP,
|
||||
userAgent string) (bool, error) {
|
||||
userAgent string) (bool, *auth.UserToken, error) {
|
||||
tryRotateCallCount++
|
||||
return false, nil
|
||||
return false, nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -46,11 +46,11 @@ func TestTokenRotationAtEndOfRequest(t *testing.T) {
|
||||
ctxHdlr := getContextHandler(t)
|
||||
ctxHdlr.AuthTokenService = &authtest.FakeUserAuthTokenService{
|
||||
TryRotateTokenProvider: func(ctx context.Context, token *auth.UserToken, clientIP net.IP,
|
||||
userAgent string) (bool, error) {
|
||||
userAgent string) (bool, *auth.UserToken, error) {
|
||||
newToken, err := util.RandomHex(16)
|
||||
require.NoError(t, err)
|
||||
token.AuthToken = newToken
|
||||
return true, nil
|
||||
return true, token, nil
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,8 @@ package dashboards
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
folder "github.com/grafana/grafana/pkg/services/folder"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// FakeDashboardService is an autogenerated mock type for the DashboardService type
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/expr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -737,7 +738,7 @@ func insertTestRule(t *testing.T, sqlStore db.DB, foderOrgID int64, folderUID st
|
||||
Data: []alertQuery{
|
||||
{
|
||||
RefID: "A",
|
||||
DatasourceUID: "-100",
|
||||
DatasourceUID: expr.DatasourceUID,
|
||||
Model: json.RawMessage(`{
|
||||
"type": "math",
|
||||
"expression": "2 + 3 > 1"
|
||||
|
||||
@@ -5,10 +5,11 @@ package dashboards
|
||||
import (
|
||||
context "context"
|
||||
|
||||
folder "github.com/grafana/grafana/pkg/services/folder"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
models "github.com/grafana/grafana/pkg/services/alerting/models"
|
||||
folder "github.com/grafana/grafana/pkg/services/folder"
|
||||
|
||||
quota "github.com/grafana/grafana/pkg/services/quota"
|
||||
)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user