mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'origin/main' into resource-store
This commit is contained in:
commit
0b29ca5eac
12
.drone.yml
12
.drone.yml
@ -865,7 +865,7 @@ services:
|
||||
- commands:
|
||||
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
|
||||
environment: {}
|
||||
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
|
||||
image: grafana/mimir-alpine:r295-a23e559
|
||||
name: mimir_backend
|
||||
- environment: {}
|
||||
image: redis:6.2.11-alpine
|
||||
@ -1312,7 +1312,7 @@ services:
|
||||
- commands:
|
||||
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
|
||||
environment: {}
|
||||
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
|
||||
image: grafana/mimir-alpine:r295-a23e559
|
||||
name: mimir_backend
|
||||
- environment: {}
|
||||
image: redis:6.2.11-alpine
|
||||
@ -2299,7 +2299,7 @@ services:
|
||||
- commands:
|
||||
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
|
||||
environment: {}
|
||||
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
|
||||
image: grafana/mimir-alpine:r295-a23e559
|
||||
name: mimir_backend
|
||||
- environment: {}
|
||||
image: redis:6.2.11-alpine
|
||||
@ -4154,7 +4154,7 @@ services:
|
||||
- commands:
|
||||
- /bin/mimir -target=backend -alertmanager.grafana-alertmanager-compatibility-enabled
|
||||
environment: {}
|
||||
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
|
||||
image: grafana/mimir-alpine:r295-a23e559
|
||||
name: mimir_backend
|
||||
- environment: {}
|
||||
image: redis:6.2.11-alpine
|
||||
@ -4673,7 +4673,7 @@ steps:
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM plugins/slack
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM python:3.8
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM postgres:12.3-alpine
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM grafana/mimir-alpine:r295-a23e559
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:5.7.39
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM mysql:8.0.32
|
||||
- trivy --exit-code 0 --severity UNKNOWN,LOW,MEDIUM redis:6.2.11-alpine
|
||||
@ -4708,7 +4708,7 @@ steps:
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL plugins/slack
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL python:3.8
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL postgres:12.3-alpine
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL grafana/mimir-alpine:r295-a23e559
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL mysql:5.7.39
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL mysql:8.0.32
|
||||
- trivy --exit-code 1 --severity HIGH,CRITICAL redis:6.2.11-alpine
|
||||
|
@ -1,5 +1,5 @@
|
||||
mimir_backend:
|
||||
image: us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP
|
||||
image: grafana/mimir-alpine:r295-a23e559
|
||||
container_name: mimir_backend
|
||||
command:
|
||||
- -target=backend
|
||||
|
@ -151,6 +151,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `pluginsAPIMetrics` | Sends metrics of public grafana packages usage by plugins |
|
||||
| `idForwarding` | Generate signed id token for identity that can be forwarded to plugins and external services |
|
||||
| `enableNativeHTTPHistogram` | Enables native HTTP Histograms |
|
||||
| `disableClassicHTTPHistogram` | Disables classic HTTP Histogram (use with enableNativeHTTPHistogram) |
|
||||
| `kubernetesSnapshots` | Routes snapshot requests from /api to the /apis endpoint |
|
||||
| `kubernetesDashboards` | Use the kubernetes API in the frontend for dashboards |
|
||||
| `datasourceQueryTypes` | Show query type endpoints in datasource API servers (currently hardcoded for testdata, expressions, and prometheus) |
|
||||
|
13
go.work.sum
13
go.work.sum
@ -423,6 +423,7 @@ github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
|
||||
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
|
||||
github.com/KimMachineGun/automemlimit v0.6.0 h1:p/BXkH+K40Hax+PuWWPQ478hPjsp9h1CPDhLlA3Z37E=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
|
||||
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
@ -696,6 +697,7 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
|
||||
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
|
||||
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
|
||||
@ -855,8 +857,6 @@ github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJ
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
|
||||
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd h1:hSkbZ9XSyjyBirMeqSqUrK+9HboWrweVlzRNqoBi2d4=
|
||||
github.com/gobuffalo/depgen v0.1.0 h1:31atYa/UW9V5q8vMJ+W6wd64OaaTHUrCUXER358zLM4=
|
||||
github.com/gobuffalo/envy v1.7.0 h1:GlXgaiBkmrYMHco6t4j7SacKO4XUjvh5pwXh0f4uxXU=
|
||||
@ -915,6 +915,7 @@ github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2c
|
||||
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
|
||||
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720 h1:zC34cGQu69FG7qzJ3WiKW244WfhDC3xxYMeNOX2gtUQ=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
@ -1025,6 +1026,7 @@ github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52Cu
|
||||
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
|
||||
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/jon-whit/go-grpc-prometheus v1.4.0 h1:/wmpGDJcLXuEjXryWhVYEGt9YBRhtLwFEN7T+Flr8sw=
|
||||
github.com/jon-whit/go-grpc-prometheus v1.4.0/go.mod h1:iTPm+Iuhh3IIqR0iGZ91JJEg5ax6YQEe1I0f6vtBuao=
|
||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
github.com/jsternberg/zap-logfmt v1.2.0 h1:1v+PK4/B48cy8cfQbxL4FmmNZrjnIMr2BsnyEmXqv2o=
|
||||
@ -1151,12 +1153,15 @@ github.com/nats-io/jwt/v2 v2.5.0/go.mod h1:24BeQtRwxRV8ruvC4CojXlx/WQ/VjuwlYiH+v
|
||||
github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg=
|
||||
github.com/nats-io/nats.go v1.12.1 h1:+0ndxwUPz3CmQ2vjbXdkC1fo3FdiOQDim4gl3Mge8Qo=
|
||||
github.com/nats-io/nats.go v1.28.0/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc=
|
||||
github.com/nats-io/nats.go v1.31.0 h1:/WFBHEc/dOKBF6qf1TZhrdEfTmOZ5JzdJ+Y3m6Y/p7E=
|
||||
github.com/nats-io/nats.go v1.31.0/go.mod h1:di3Bm5MLsoB4Bx61CBTsxuarI36WbhAwOm8QrW39+i8=
|
||||
github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=
|
||||
github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64=
|
||||
github.com/nats-io/nkeys v0.4.6 h1:IzVe95ru2CT6ta874rt9saQRkWfe2nFj1NtvYSLqMzY=
|
||||
github.com/nats-io/nkeys v0.4.6/go.mod h1:4DxZNzenSVd1cYQoAa8948QY3QDjrHfcfVADymtkpts=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM=
|
||||
github.com/oapi-codegen/testutil v1.0.0/go.mod h1:ttCaYbHvJtHuiyeBF0tPIX+4uhEPTeizXKx28okijLw=
|
||||
github.com/oklog/oklog v0.3.2 h1:wVfs8F+in6nTBMkA7CbRw+zZMIB7nNM825cM1wuzoTk=
|
||||
@ -1236,6 +1241,7 @@ github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAy
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/pact-foundation/pact-go v1.0.4 h1:OYkFijGHoZAYbOIb1LWXrwKQbMMRUv1oQ89blD2Mh2Q=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30 h1:BHT1/DKsYDGkUgQ2jmMaozVcdk+sVfz0+1ZJq4zkWgw=
|
||||
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
@ -1258,6 +1264,7 @@ github.com/pkg/profile v1.2.1 h1:F++O52m40owAmADcojzM+9gyjmMOY/T4oYJkgFDH8RE=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pkg/sftp v1.13.1 h1:I2qBYMChEhIjOgazfJmV3/mZM256btk6wkCDRmW7JYs=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
|
||||
@ -1298,6 +1305,7 @@ github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF
|
||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||
github.com/sagikazarmark/crypt v0.6.0 h1:REOEXCs/NFY/1jOCEouMuT4zEniE5YoXbvpC5X/TLF8=
|
||||
github.com/sagikazarmark/crypt v0.17.0 h1:ZA/7pXyjkHoK4bW4mIdnCLvL8hd+Nrbiw7Dqk7D4qUk=
|
||||
github.com/sagikazarmark/crypt v0.17.0/go.mod h1:SMtHTvdmsZMuY/bpZoqokSoChIrcJ/epOxZN58PbZDg=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
@ -1366,6 +1374,7 @@ github.com/tdewolff/parse/v2 v2.6.8 h1:mhNZXYCx//xG7Yq2e/kVLNZw4YfYmeHbhx+Zc0OvF
|
||||
github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
|
||||
github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
|
@ -244,6 +244,7 @@
|
||||
"@emotion/react": "11.11.4",
|
||||
"@fingerprintjs/fingerprintjs": "^3.4.2",
|
||||
"@floating-ui/react": "0.26.16",
|
||||
"@formatjs/intl-durationformat": "^0.2.4",
|
||||
"@glideapps/glide-data-grid": "^6.0.0",
|
||||
"@grafana/aws-sdk": "0.3.3",
|
||||
"@grafana/azure-sdk": "0.0.3",
|
||||
|
@ -112,6 +112,7 @@ export interface FeatureToggles {
|
||||
externalServiceAccounts?: boolean;
|
||||
panelMonitoring?: boolean;
|
||||
enableNativeHTTPHistogram?: boolean;
|
||||
disableClassicHTTPHistogram?: boolean;
|
||||
formatString?: boolean;
|
||||
transformationsVariableSupport?: boolean;
|
||||
kubernetesPlaylists?: boolean;
|
||||
|
@ -603,4 +603,7 @@ export const Components = {
|
||||
headerOrderSwitch: 'data-testid header-order-switch',
|
||||
headerPreviewSwitch: 'data-testid header-preview-switch',
|
||||
},
|
||||
EntityNotFound: {
|
||||
container: 'data-testid entity-not-found',
|
||||
},
|
||||
};
|
||||
|
@ -505,6 +505,20 @@ describe('Language completion provider', () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('should dont send match[] parameter if there is no metric', async () => {
|
||||
const mockQueries: PromQuery[] = [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: '',
|
||||
},
|
||||
];
|
||||
const fetchLabel = languageProvider.fetchLabels;
|
||||
const requestSpy = jest.spyOn(languageProvider, 'request');
|
||||
await fetchLabel(tr, mockQueries);
|
||||
expect(requestSpy).toHaveBeenCalled();
|
||||
expect(requestSpy.mock.calls[0][0].indexOf('match[]')).toEqual(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -219,11 +219,13 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
const searchParams = new URLSearchParams({ ...timeParams });
|
||||
queries?.forEach((q) => {
|
||||
const visualQuery = buildVisualQueryFromString(q.expr);
|
||||
searchParams.append('match[]', visualQuery.query.metric);
|
||||
if (visualQuery.query.binaryQueries) {
|
||||
visualQuery.query.binaryQueries.forEach((bq) => {
|
||||
searchParams.append('match[]', bq.query.metric);
|
||||
});
|
||||
if (visualQuery.query.metric !== '') {
|
||||
searchParams.append('match[]', visualQuery.query.metric);
|
||||
if (visualQuery.query.binaryQueries) {
|
||||
visualQuery.query.binaryQueries.forEach((bq) => {
|
||||
searchParams.append('match[]', bq.query.metric);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -28,6 +28,10 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] {
|
||||
params: [{ name: 'Identifier', type: 'string' }],
|
||||
defaultParams: ['count'],
|
||||
}),
|
||||
...createAggregationOperationWithParam(PromOperationId.Quantile, {
|
||||
params: [{ name: 'Value', type: 'number' }],
|
||||
defaultParams: [1],
|
||||
}),
|
||||
createAggregationOverTime(PromOperationId.SumOverTime),
|
||||
createAggregationOverTime(PromOperationId.AvgOverTime),
|
||||
createAggregationOverTime(PromOperationId.MinOverTime),
|
||||
|
@ -76,6 +76,10 @@ export const generalTemplates: TemplateData[] = [
|
||||
template: 'count_values("aaaa",metric_a{})',
|
||||
description: 'Count number of label values for a label named "aaaa"',
|
||||
},
|
||||
{
|
||||
template: 'quantile by(l) (1,metric_a)',
|
||||
description: 'Quantile of series in the metric "metric_a" grouped by the label "l"',
|
||||
},
|
||||
];
|
||||
|
||||
export const counterTemplates: TemplateData[] = [
|
||||
|
@ -230,12 +230,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
|
||||
id: PromOperationId.Pi,
|
||||
renderer: (model) => `${model.id}()`,
|
||||
}),
|
||||
createFunction({
|
||||
id: PromOperationId.Quantile,
|
||||
params: [{ name: 'Value', type: 'number' }],
|
||||
defaultParams: [1],
|
||||
renderer: functionRendererLeft,
|
||||
}),
|
||||
createFunction({ id: PromOperationId.Rad }),
|
||||
createRangeFunction(PromOperationId.Resets),
|
||||
createFunction({
|
||||
|
@ -51,12 +51,19 @@ func RequestMetrics(features featuremgmt.FeatureToggles, cfg *setting.Cfg, promR
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagEnableNativeHTTPHistogram) {
|
||||
// the recommended default value from the prom_client
|
||||
// https://github.com/prometheus/client_golang/blob/main/prometheus/histogram.go#L411
|
||||
// Giving this variable an value means the client will expose the histograms as an
|
||||
// native histogram instead of normal a normal histogram.
|
||||
// Giving this variable a value means the client will expose a native
|
||||
// histogram.
|
||||
histogramOptions.NativeHistogramBucketFactor = 1.1
|
||||
// The default value in OTel. It probably good enough for us as well.
|
||||
histogramOptions.NativeHistogramMaxBucketNumber = 160
|
||||
histogramOptions.NativeHistogramMinResetDuration = time.Hour
|
||||
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagDisableClassicHTTPHistogram) {
|
||||
// setting Buckets to nil with native options set means the classic
|
||||
// histogram will no longer be exposed - this can be a good way to
|
||||
// reduce cardinality in the exposed metrics
|
||||
histogramOptions.Buckets = nil
|
||||
}
|
||||
}
|
||||
|
||||
httpRequestDurationHistogram := prometheus.NewHistogramVec(
|
||||
|
86
pkg/services/authz/zanzana/logger.go
Normal file
86
pkg/services/authz/zanzana/logger.go
Normal file
@ -0,0 +1,86 @@
|
||||
package zanzana
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
)
|
||||
|
||||
// zanzanaLogger is a grafana logger wrapper compatible with OpenFGA logger interface
|
||||
type zanzanaLogger struct {
|
||||
logger *log.ConcreteLogger
|
||||
}
|
||||
|
||||
func newZanzanaLogger() *zanzanaLogger {
|
||||
logger := log.New("openfga-server")
|
||||
|
||||
return &zanzanaLogger{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Simple converter for zap logger fields
|
||||
func zapFieldsToArgs(fields []zap.Field) []any {
|
||||
args := make([]any, 0)
|
||||
for _, f := range fields {
|
||||
args = append(args, f.Key)
|
||||
if f.Interface != nil {
|
||||
args = append(args, f.Interface)
|
||||
} else if f.String != "" {
|
||||
args = append(args, f.String)
|
||||
} else {
|
||||
args = append(args, f.Integer)
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) Debug(msg string, fields ...zap.Field) {
|
||||
l.logger.Debug(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) Info(msg string, fields ...zap.Field) {
|
||||
l.logger.Info(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) Warn(msg string, fields ...zap.Field) {
|
||||
l.logger.Warn(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) Error(msg string, fields ...zap.Field) {
|
||||
l.logger.Error(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) Panic(msg string, fields ...zap.Field) {
|
||||
l.logger.Error(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) Fatal(msg string, fields ...zap.Field) {
|
||||
l.logger.Error(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) DebugWithContext(ctx context.Context, msg string, fields ...zap.Field) {
|
||||
l.logger.Debug(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) InfoWithContext(ctx context.Context, msg string, fields ...zap.Field) {
|
||||
l.logger.Info(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) WarnWithContext(ctx context.Context, msg string, fields ...zap.Field) {
|
||||
l.logger.Warn(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) ErrorWithContext(ctx context.Context, msg string, fields ...zap.Field) {
|
||||
l.logger.Error(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) PanicWithContext(ctx context.Context, msg string, fields ...zap.Field) {
|
||||
l.logger.Error(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
||||
|
||||
func (l *zanzanaLogger) FatalWithContext(ctx context.Context, msg string, fields ...zap.Field) {
|
||||
l.logger.Error(msg, zapFieldsToArgs(fields)...)
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package zanzana
|
||||
|
||||
import (
|
||||
"github.com/openfga/openfga/pkg/logger"
|
||||
"github.com/openfga/openfga/pkg/server"
|
||||
"github.com/openfga/openfga/pkg/storage"
|
||||
)
|
||||
@ -10,8 +9,7 @@ func NewServer(store storage.OpenFGADatastore) (*server.Server, error) {
|
||||
// FIXME(kalleep): add support for more options, configure logging, tracing etc
|
||||
opts := []server.OpenFGAServiceV1Option{
|
||||
server.WithDatastore(store),
|
||||
// FIXME(kalleep): Write and log adapter for open fga logging interface
|
||||
server.WithLogger(logger.NewNoopLogger()),
|
||||
server.WithLogger(newZanzanaLogger()),
|
||||
}
|
||||
|
||||
// FIXME(kalleep): Interceptors
|
||||
|
@ -700,11 +700,24 @@ var (
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "enableNativeHTTPHistogram",
|
||||
Description: "Enables native HTTP Histograms",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: hostedGrafanaTeam,
|
||||
Name: "enableNativeHTTPHistogram",
|
||||
Description: "Enables native HTTP Histograms",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaBackendServicesSquad,
|
||||
HideFromAdminPage: true,
|
||||
AllowSelfServe: false,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
FeatureFlag{
|
||||
Name: "disableClassicHTTPHistogram",
|
||||
Description: "Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false,
|
||||
Owner: grafanaBackendServicesSquad,
|
||||
HideFromAdminPage: true,
|
||||
AllowSelfServe: false,
|
||||
RequiresRestart: true,
|
||||
},
|
||||
{
|
||||
Name: "formatString",
|
||||
|
@ -92,7 +92,8 @@ pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,tru
|
||||
idForwarding,experimental,@grafana/identity-access-team,false,false,false
|
||||
externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false
|
||||
panelMonitoring,GA,@grafana/dataviz-squad,false,false,true
|
||||
enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false
|
||||
enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
|
||||
disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false
|
||||
formatString,preview,@grafana/dataviz-squad,false,false,true
|
||||
transformationsVariableSupport,GA,@grafana/dataviz-squad,false,false,true
|
||||
kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false
|
||||
|
|
@ -383,6 +383,10 @@ const (
|
||||
// Enables native HTTP Histograms
|
||||
FlagEnableNativeHTTPHistogram = "enableNativeHTTPHistogram"
|
||||
|
||||
// FlagDisableClassicHTTPHistogram
|
||||
// Disables classic HTTP Histogram (use with enableNativeHTTPHistogram)
|
||||
FlagDisableClassicHTTPHistogram = "disableClassicHTTPHistogram"
|
||||
|
||||
// FlagFormatString
|
||||
// Enable format string transformer
|
||||
FlagFormatString = "formatString"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,6 @@ import (
|
||||
amalert "github.com/prometheus/alertmanager/api/v2/client/alert"
|
||||
amalertgroup "github.com/prometheus/alertmanager/api/v2/client/alertgroup"
|
||||
amgeneral "github.com/prometheus/alertmanager/api/v2/client/general"
|
||||
amreceiver "github.com/prometheus/alertmanager/api/v2/client/receiver"
|
||||
amsilence "github.com/prometheus/alertmanager/api/v2/client/silence"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
@ -487,21 +486,7 @@ func (am *Alertmanager) GetStatus(ctx context.Context) (apimodels.GettableStatus
|
||||
}
|
||||
|
||||
func (am *Alertmanager) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) {
|
||||
params := amreceiver.NewGetReceiversParamsWithContext(ctx)
|
||||
res, err := am.amClient.Receiver.GetReceivers(params)
|
||||
if err != nil {
|
||||
return []apimodels.Receiver{}, err
|
||||
}
|
||||
|
||||
rcvs := make([]apimodels.Receiver, len(res.Payload))
|
||||
for i, rcv := range res.Payload {
|
||||
rcvs[i] = apimodels.Receiver{
|
||||
Name: *rcv.Name,
|
||||
Integrations: []apimodels.Integration{},
|
||||
}
|
||||
}
|
||||
|
||||
return rcvs, nil
|
||||
return am.mimirClient.GetReceivers(ctx)
|
||||
}
|
||||
|
||||
func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*notifier.TestReceiversResult, error) {
|
||||
|
@ -717,7 +717,13 @@ func TestIntegrationRemoteAlertmanagerReceivers(t *testing.T) {
|
||||
// We should start with the default config.
|
||||
rcvs, err := am.GetReceivers(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "empty-receiver", rcvs[0].Name)
|
||||
require.Equal(t, []apimodels.Receiver{
|
||||
{
|
||||
Active: true,
|
||||
Name: "empty-receiver",
|
||||
Integrations: []apimodels.Integration{},
|
||||
},
|
||||
}, rcvs)
|
||||
}
|
||||
|
||||
func genAlert(active bool, labels map[string]string) amv2.PostableAlert {
|
||||
|
@ -11,7 +11,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
grafanaAlertmanagerConfigPath = "/api/v1/grafana/config"
|
||||
grafanaAlertmanagerConfigPath = "/api/v1/grafana/config"
|
||||
grafanaAlertmanagerReceiversPath = "/api/v1/grafana/receivers"
|
||||
)
|
||||
|
||||
type UserGrafanaConfig struct {
|
||||
@ -63,3 +64,16 @@ func (mc *Mimir) CreateGrafanaAlertmanagerConfig(ctx context.Context, cfg *apimo
|
||||
func (mc *Mimir) DeleteGrafanaAlertmanagerConfig(ctx context.Context) error {
|
||||
return mc.doOK(ctx, grafanaAlertmanagerConfigPath, http.MethodDelete, nil)
|
||||
}
|
||||
|
||||
func (mc *Mimir) GetReceivers(ctx context.Context) ([]apimodels.Receiver, error) {
|
||||
response := []apimodels.Receiver{}
|
||||
|
||||
// nolint:bodyclose
|
||||
// closed within `do`
|
||||
_, err := mc.do(ctx, grafanaAlertmanagerReceiversPath, http.MethodGet, nil, &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
@ -29,6 +29,9 @@ type MimirClient interface {
|
||||
DeleteGrafanaAlertmanagerConfig(ctx context.Context) error
|
||||
|
||||
ShouldPromoteConfig() bool
|
||||
|
||||
// Mimir implements an extended version of the receivers API under a different path.
|
||||
GetReceivers(ctx context.Context) ([]apimodels.Receiver, error)
|
||||
}
|
||||
|
||||
type Mimir struct {
|
||||
|
@ -4,10 +4,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@ -62,6 +64,11 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := url.Parse(webhook.Url)
|
||||
if err != nil {
|
||||
// Should not be possible - NewRequestWithContext should also err if the URL is bad.
|
||||
return err
|
||||
}
|
||||
|
||||
if webhook.ContentType == "" {
|
||||
webhook.ContentType = "application/json"
|
||||
@ -80,7 +87,7 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
|
||||
|
||||
resp, err := netClient.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
return redactURL(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
@ -96,16 +103,25 @@ func (ns *NotificationService) sendWebRequestSync(ctx context.Context, webhook *
|
||||
if webhook.Validation != nil {
|
||||
err := webhook.Validation(body, resp.StatusCode)
|
||||
if err != nil {
|
||||
ns.log.Debug("Webhook failed validation", "url", webhook.Url, "statuscode", resp.Status, "body", string(body))
|
||||
ns.log.Debug("Webhook failed validation", "url", url.Redacted(), "statuscode", resp.Status, "body", string(body), "error", err)
|
||||
return fmt.Errorf("webhook failed validation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StatusCode/100 == 2 {
|
||||
ns.log.Debug("Webhook succeeded", "url", webhook.Url, "statuscode", resp.Status)
|
||||
ns.log.Debug("Webhook succeeded", "url", url.Redacted(), "statuscode", resp.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
ns.log.Debug("Webhook failed", "url", webhook.Url, "statuscode", resp.Status, "body", string(body))
|
||||
ns.log.Debug("Webhook failed", "url", url.Redacted(), "statuscode", resp.Status, "body", string(body))
|
||||
return fmt.Errorf("webhook response status %v", resp.Status)
|
||||
}
|
||||
|
||||
func redactURL(err error) error {
|
||||
var e *url.Error
|
||||
if !errors.As(err, &e) {
|
||||
return err
|
||||
}
|
||||
e.URL = "<redacted>"
|
||||
return e
|
||||
}
|
||||
|
@ -25,27 +25,40 @@ func interpolateInterval(flux string, interval time.Duration) string {
|
||||
var fluxVariableFilterExp = regexp.MustCompile(`(?m)([a-zA-Z]+)\.([a-zA-Z]+)`)
|
||||
|
||||
func interpolateFluxSpecificVariables(query queryModel) string {
|
||||
rawQuery := query.RawQuery
|
||||
flux := query.RawQuery
|
||||
|
||||
matches := fluxVariableFilterExp.FindAllStringSubmatch(flux, -1)
|
||||
matches := fluxVariableFilterExp.FindAllStringSubmatchIndex(rawQuery, -1)
|
||||
if matches != nil {
|
||||
timeRange := query.TimeRange
|
||||
from := timeRange.From.UTC().Format(time.RFC3339Nano)
|
||||
to := timeRange.To.UTC().Format(time.RFC3339Nano)
|
||||
for _, match := range matches {
|
||||
switch match[2] {
|
||||
// For query "range(start: v.timeRangeStart, stop: v.timeRangeStop)"
|
||||
// rawQuery[match[0]:match[1]] will be v.timeRangeStart
|
||||
// rawQuery[match[2]:match[3]] will be v
|
||||
// rawQuery[match[4]:match[5]] will be timeRangeStart
|
||||
fullMatch := rawQuery[match[0]:match[1]]
|
||||
key := rawQuery[match[4]:match[5]]
|
||||
|
||||
switch key {
|
||||
case "timeRangeStart":
|
||||
flux = strings.ReplaceAll(flux, match[0], from)
|
||||
flux = strings.ReplaceAll(flux, fullMatch, from)
|
||||
case "timeRangeStop":
|
||||
flux = strings.ReplaceAll(flux, match[0], to)
|
||||
flux = strings.ReplaceAll(flux, fullMatch, to)
|
||||
case "windowPeriod":
|
||||
flux = strings.ReplaceAll(flux, match[0], query.Interval.String())
|
||||
flux = strings.ReplaceAll(flux, fullMatch, query.Interval.String())
|
||||
case "bucket":
|
||||
flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.Bucket+"\"")
|
||||
// Check if 'bucket' is part of a join query
|
||||
beforeMatch := rawQuery[:match[0]]
|
||||
if strings.Contains(beforeMatch, "join.") {
|
||||
continue
|
||||
}
|
||||
flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.Bucket+"\"")
|
||||
case "defaultBucket":
|
||||
flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.DefaultBucket+"\"")
|
||||
flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.DefaultBucket+"\"")
|
||||
case "organization":
|
||||
flux = strings.ReplaceAll(flux, match[0], "\""+query.Options.Organization+"\"")
|
||||
flux = strings.ReplaceAll(flux, fullMatch, "\""+query.Options.Organization+"\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,11 @@ func TestInterpolate(t *testing.T) {
|
||||
before: `v.timeRangeStart, something.timeRangeStop, XYZ.bucket, uuUUu.defaultBucket, aBcDefG.organization, window.windowPeriod, a91{}.bucket, $__interval, $__interval_ms`,
|
||||
after: `2021-09-22T10:12:51.310985041Z, 2021-09-22T11:12:51.310985042Z, "grafana2", "grafana3", "grafana1", 1m1.258s, a91{}.bucket, 1m, 61258`,
|
||||
},
|
||||
{
|
||||
name: "don't interpolate bucket variable in join query",
|
||||
before: `range(start: v.timeRangeStart, stop: v.timeRangeStop) join.left(left: left |> group(), right: right,on:((l,r) => l.bucket == r.id), as: ((l, r) => ({l with name: r.name})))`,
|
||||
after: `range(start: 2021-09-22T10:12:51.310985041Z, stop: 2021-09-22T11:12:51.310985042Z) join.left(left: left |> group(), right: right,on:((l,r) => l.bucket == r.id), as: ((l, r) => ({l with name: r.name})))`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { EmptyState, TextLink, useStyles2 } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
@ -15,7 +16,7 @@ export function EntityNotFound({ entity = 'Page' }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.container} data-testid={selectors.components.EntityNotFound.container}>
|
||||
<EmptyState message={`${entity} not found`} variant="not-found">
|
||||
We're looking but can't seem to find this {entity.toLowerCase()}. Try returning{' '}
|
||||
<TextLink href="/">home</TextLink> or seeking help on the{' '}
|
||||
|
20
public/app/core/internationalization/dates.ts
Normal file
20
public/app/core/internationalization/dates.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import '@formatjs/intl-durationformat/polyfill';
|
||||
|
||||
import { getI18next } from './index';
|
||||
|
||||
export function formatDate(value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string {
|
||||
if (typeof value === 'string') {
|
||||
return formatDate(new Date(value), format);
|
||||
}
|
||||
|
||||
const i18n = getI18next();
|
||||
const dateFormatter = new Intl.DateTimeFormat(i18n.language, format);
|
||||
return dateFormatter.format(value);
|
||||
}
|
||||
|
||||
export function formatDuration(duration: Intl.DurationInput, options: Intl.DurationFormatOptions = {}) {
|
||||
const i18n = getI18next();
|
||||
|
||||
const dateFormatter = new Intl.DurationFormat(i18n.language, options);
|
||||
return dateFormatter.format(duration);
|
||||
}
|
@ -7,6 +7,7 @@ import { DEFAULT_LANGUAGE, NAMESPACES, VALID_LANGUAGES } from './constants';
|
||||
import { loadTranslations } from './loadTranslations';
|
||||
|
||||
let tFunc: TFunction<string[], undefined> | undefined;
|
||||
let i18nInstance: typeof i18n;
|
||||
|
||||
export async function initializeI18n(language: string): Promise<{ language: string | undefined }> {
|
||||
// This is a placeholder so we can put a 'comment' in the message json files.
|
||||
@ -28,7 +29,7 @@ export async function initializeI18n(language: string): Promise<{ language: stri
|
||||
ns: NAMESPACES,
|
||||
};
|
||||
|
||||
let i18nInstance = i18n;
|
||||
i18nInstance = i18n;
|
||||
if (language === 'detect') {
|
||||
i18nInstance = i18nInstance.use(LanguageDetector);
|
||||
const detection: DetectorOptions = { order: ['navigator'], caches: [] };
|
||||
@ -79,10 +80,20 @@ export const t = (id: string, defaultMessage: string, values?: Record<string, un
|
||||
return tFunc(id, defaultMessage, values);
|
||||
};
|
||||
|
||||
export const i18nDate = (value: number | Date | string, format: Intl.DateTimeFormatOptions = {}): string => {
|
||||
if (typeof value === 'string') {
|
||||
return i18nDate(new Date(value), format);
|
||||
export function getI18next() {
|
||||
if (!tFunc) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
console.warn(
|
||||
'An attempt to internationalize was made before it was initialized. This was probably caused by calling a locale-aware function in the root module scope, instead of in render'
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
throw new Error('getI18next was called before i18n was initialized');
|
||||
}
|
||||
|
||||
return i18n;
|
||||
}
|
||||
const dateFormatter = new Intl.DateTimeFormat(i18n.language, format);
|
||||
return dateFormatter.format(value);
|
||||
};
|
||||
|
||||
return i18nInstance || i18n;
|
||||
}
|
||||
|
13
public/app/core/internationalization/types.d.ts
vendored
Normal file
13
public/app/core/internationalization/types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
import {
|
||||
DurationFormatConstructor,
|
||||
DurationFormatOptions as _DurationFormatOptions,
|
||||
DurationInput as _DurationInput,
|
||||
} from '@formatjs/intl-durationformat/src/types';
|
||||
|
||||
declare global {
|
||||
namespace Intl {
|
||||
const DurationFormat: DurationFormatConstructor;
|
||||
type DurationFormatOptions = _DurationFormatOptions;
|
||||
type DurationInput = _DurationInput;
|
||||
}
|
||||
}
|
@ -32,9 +32,17 @@ export class PerformanceBackend implements EchoBackend<PerformanceEvent, Perform
|
||||
return;
|
||||
}
|
||||
|
||||
backendSrv.post('/api/frontend-metrics', {
|
||||
events: this.buffer,
|
||||
});
|
||||
backendSrv
|
||||
.post(
|
||||
'/api/frontend-metrics',
|
||||
{
|
||||
events: this.buffer,
|
||||
},
|
||||
{ showErrorAlert: false }
|
||||
)
|
||||
.catch(() => {
|
||||
// Just swallow this error - it's non-critical
|
||||
});
|
||||
|
||||
this.buffer = [];
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { ConfirmButton, ConfirmModal, Button, Stack } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { i18nDate } from 'app/core/internationalization';
|
||||
import { formatDate } from 'app/core/internationalization/dates';
|
||||
import { AccessControlAction, UserSession } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
@ -68,7 +68,7 @@ class BaseUserSessions extends PureComponent<Props, State> {
|
||||
sessions.map((session, index) => (
|
||||
<tr key={`${session.id}-${index}`}>
|
||||
<td>{session.isActive ? 'Now' : session.seenAt}</td>
|
||||
<td>{i18nDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>{`${session.browser} on ${session.os} ${session.osVersion}`}</td>
|
||||
<td>
|
||||
|
@ -200,6 +200,20 @@ describe('DashboardScene', () => {
|
||||
expect(resoredLayout.state.children.map((c) => c.state.key)).toEqual(originalPanelOrder);
|
||||
});
|
||||
|
||||
it('Should exit edit mode and discard panel changes if leaving the dashboard while in panel edit', () => {
|
||||
const panel = findVizPanelByKey(scene, 'panel-1');
|
||||
const editPanel = buildPanelEditScene(panel!);
|
||||
scene.setState({
|
||||
editPanel,
|
||||
});
|
||||
|
||||
expect(scene.state.editPanel!['_discardChanges']).toBe(false);
|
||||
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
|
||||
expect(scene.state.editPanel!['_discardChanges']).toBe(true);
|
||||
});
|
||||
|
||||
it.each`
|
||||
prop | value
|
||||
${'title'} | ${'new title'}
|
||||
|
@ -317,6 +317,13 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
this.setState({ isEditing: false });
|
||||
}
|
||||
|
||||
// if we are in edit panel, we need to onDiscard()
|
||||
// so the useEffect cleanup comes later and
|
||||
// doesn't try to commit the changes
|
||||
if (this.state.editPanel) {
|
||||
this.state.editPanel.onDiscard();
|
||||
}
|
||||
|
||||
// Disable grid dragging
|
||||
this.propagateEditModeChange();
|
||||
}
|
||||
@ -385,6 +392,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
||||
const { meta, viewPanelScene, editPanel } = this.state;
|
||||
|
||||
if (meta.dashboardNotFound) {
|
||||
return { text: 'Not found' };
|
||||
}
|
||||
|
||||
let pageNav: NavModelItem = {
|
||||
text: this.state.title,
|
||||
url: getDashboardUrl({
|
||||
|
@ -0,0 +1,65 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router';
|
||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
|
||||
describe('DashboardSceneRenderer', () => {
|
||||
it('should render Not Found notice when dashboard is not found', async () => {
|
||||
const scene = transformSaveModelToScene({
|
||||
meta: {
|
||||
isSnapshot: true,
|
||||
dashboardNotFound: true,
|
||||
canStar: false,
|
||||
canDelete: false,
|
||||
canSave: false,
|
||||
canEdit: false,
|
||||
canShare: false,
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Not found',
|
||||
uid: 'uid',
|
||||
schemaVersion: 0,
|
||||
// Disabling build in annotations to avoid mocking Grafana data source
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
builtIn: 1,
|
||||
datasource: {
|
||||
type: 'grafana',
|
||||
uid: '-- Grafana --',
|
||||
},
|
||||
enable: false,
|
||||
hide: true,
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const store = configureStore({});
|
||||
const context = getGrafanaContextMock();
|
||||
|
||||
render(
|
||||
<GrafanaContext.Provider value={context}>
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<scene.Component model={scene} />
|
||||
</Router>
|
||||
</Provider>
|
||||
</GrafanaContext.Provider>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -7,6 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
|
||||
import { useSelector } from 'app/types';
|
||||
@ -35,14 +36,26 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
);
|
||||
}
|
||||
|
||||
const emptyState = <DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} />;
|
||||
const emptyState = (
|
||||
<DashboardEmpty dashboard={model} canCreate={!!model.state.meta.canEdit} key="dashboard-empty-state" />
|
||||
);
|
||||
|
||||
const withPanels = (
|
||||
<div className={cx(styles.body, !hasControls && styles.bodyWithoutControls)}>
|
||||
<div className={cx(styles.body, !hasControls && styles.bodyWithoutControls)} key="dashboard-panels">
|
||||
<bodyToRender.Component model={bodyToRender} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const notFound = meta.dashboardNotFound && <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
|
||||
|
||||
let body = [withPanels];
|
||||
|
||||
if (notFound) {
|
||||
body = [notFound];
|
||||
} else if (isEmpty) {
|
||||
body = [emptyState, withPanels];
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
|
||||
{editPanel && <editPanel.Component model={editPanel} />}
|
||||
@ -55,7 +68,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
scopes && isScopesExpanded && styles.pageContainerWithScopesExpanded
|
||||
)}
|
||||
>
|
||||
{scopes && <scopes.Component model={scopes} />}
|
||||
{scopes && !meta.dashboardNotFound && <scopes.Component model={scopes} />}
|
||||
<NavToolbarActions dashboard={model} />
|
||||
{controls && hasControls && (
|
||||
<div
|
||||
@ -71,10 +84,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
className={styles.scrollbarContainer}
|
||||
testId={selectors.pages.Dashboard.DashNav.scrollContainer}
|
||||
>
|
||||
<div className={cx(styles.canvasContent, isHomePage && styles.homePagePadding)}>
|
||||
<>{isEmpty && emptyState}</>
|
||||
{withPanels}
|
||||
</div>
|
||||
<div className={cx(styles.canvasContent, isHomePage && styles.homePagePadding)}>{body}</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
)}
|
||||
|
@ -8,6 +8,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { DashboardMeta } from 'app/types';
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
|
||||
@ -163,9 +164,27 @@ describe('NavToolbarActions', () => {
|
||||
expect(newShareButton).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Snapshot', () => {
|
||||
it('should show link button when is a snapshot', () => {
|
||||
setup({
|
||||
isSnapshot: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('button-snapshot')).toBeInTheDocument();
|
||||
});
|
||||
it('should not show link button when is not found dashboard', () => {
|
||||
setup({
|
||||
isSnapshot: true,
|
||||
dashboardNotFound: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('button-snapshot')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setup() {
|
||||
function setup(meta?: DashboardMeta) {
|
||||
const dashboard = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
meta: {
|
||||
@ -177,6 +196,7 @@ function setup() {
|
||||
canStar: true,
|
||||
canAdmin: true,
|
||||
canDelete: true,
|
||||
...meta,
|
||||
},
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
|
@ -139,7 +139,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: meta.isSnapshot && !isEditing,
|
||||
condition: meta.isSnapshot && !meta.dashboardNotFound && !isEditing,
|
||||
render: () => (
|
||||
<GoToSnapshotOriginButton
|
||||
key="go-to-snapshot-origin"
|
||||
|
@ -92,7 +92,9 @@ export class DashboardLoaderSrv {
|
||||
return result;
|
||||
})
|
||||
.catch(() => {
|
||||
return this._dashboardLoadFailed('Not found', true);
|
||||
const dash = this._dashboardLoadFailed('Not found', true);
|
||||
dash.dashboard.uid = uid;
|
||||
return dash;
|
||||
});
|
||||
} else {
|
||||
throw new Error('Dashboard uid or slug required');
|
||||
|
@ -4,7 +4,8 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Button, Icon, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { i18nDate, Trans } from 'app/core/internationalization';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { formatDate } from 'app/core/internationalization/dates';
|
||||
import { UserSession } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
@ -50,7 +51,7 @@ class UserSessions extends PureComponent<Props> {
|
||||
{sessions.map((session: UserSession, index) => (
|
||||
<tr key={index}>
|
||||
{session.isActive ? <td>Now</td> : <td>{session.seenAt}</td>}
|
||||
<td>{i18nDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{formatDate(session.createdAt, { dateStyle: 'long' })}</td>
|
||||
<td>{session.clientIp}</td>
|
||||
<td>
|
||||
{session.browser} on {session.os} {session.osVersion}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
durationToMilliseconds,
|
||||
parseDuration,
|
||||
TransformationApplicabilityLevels,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames';
|
||||
import { config } from '@grafana/runtime';
|
||||
@ -292,7 +293,14 @@ export function prepBucketFrames(frames: DataFrame[]): DataFrame[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCalculationOptions): DataFrame {
|
||||
interface HeatmapCalculationOptionsWithTimeRange extends HeatmapCalculationOptions {
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export function calculateHeatmapFromData(
|
||||
frames: DataFrame[],
|
||||
options: HeatmapCalculationOptionsWithTimeRange
|
||||
): DataFrame {
|
||||
// Find fields in the heatmap
|
||||
const { xField, yField, xs, ys } = findHeatmapFields(frames);
|
||||
|
||||
@ -329,6 +337,9 @@ export function calculateHeatmapFromData(frames: DataFrame[], options: HeatmapCa
|
||||
ySize: yBucketsCfg.value ? +yBucketsCfg.value : undefined,
|
||||
yLog:
|
||||
scaleDistribution?.type === ScaleDistribution.Log ? (scaleDistribution?.log as 2 | 10 | undefined) : undefined,
|
||||
|
||||
xMin: options.timeRange?.from.valueOf(),
|
||||
xMax: options.timeRange?.to.valueOf(),
|
||||
});
|
||||
|
||||
const frame = {
|
||||
@ -460,20 +471,23 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
|
||||
let ySorted = opts?.ySorted ?? false;
|
||||
|
||||
// find x and y limits to pre-compute buckets struct
|
||||
let minX = xSorted ? xs[0] : Infinity;
|
||||
let minX = opts?.xMin ?? (xSorted ? xs[0] : Infinity);
|
||||
let minY = ySorted ? ys[0] : Infinity;
|
||||
let maxX = xSorted ? xs[len - 1] : -Infinity;
|
||||
let maxX = opts?.xMax ?? (xSorted ? xs[len - 1] : -Infinity);
|
||||
let maxY = ySorted ? ys[len - 1] : -Infinity;
|
||||
|
||||
let yExp = opts?.yLog;
|
||||
|
||||
let withPredefX = opts?.xMin != null && opts?.xMax != null;
|
||||
let withPredefY = opts?.yMin != null && opts?.yMax != null;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (!xSorted) {
|
||||
if (!xSorted && !withPredefX) {
|
||||
minX = Math.min(minX, xs[i]);
|
||||
maxX = Math.max(maxX, xs[i]);
|
||||
}
|
||||
|
||||
if (!ySorted) {
|
||||
if (!ySorted && !withPredefY) {
|
||||
if (!yExp || ys[i] > 0) {
|
||||
minY = Math.min(minY, ys[i]);
|
||||
maxY = Math.max(maxY, ys[i]);
|
||||
@ -500,7 +514,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
|
||||
}
|
||||
|
||||
if (xMode === HeatmapCalculationMode.Count) {
|
||||
// TODO: optionally use view range min/max instead of data range for bucket sizing
|
||||
let approx = (maxX - minX) / Math.max(xBinIncr - 1, 1);
|
||||
// nice-ify
|
||||
let xIncrs = opts?.xTime ? niceTimeIncrs : niceLinearIncrs;
|
||||
@ -509,7 +522,6 @@ function heatmap(xs: number[], ys: number[], opts?: HeatmapOpts) {
|
||||
}
|
||||
|
||||
if (yMode === HeatmapCalculationMode.Count) {
|
||||
// TODO: optionally use view range min/max instead of data range for bucket sizing
|
||||
let approx = (maxY - minY) / Math.max(yBinIncr - 1, 1);
|
||||
// nice-ify
|
||||
let yIncrs = opts?.yTime ? niceTimeIncrs : niceLinearIncrs;
|
||||
|
@ -78,4 +78,29 @@ describe('toRawSql', () => {
|
||||
const result = toRawSql(testQuery);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not wrap * with quote', () => {
|
||||
const expected = 'SELECT * FROM "TestValue" WHERE "time" >= $__timeFrom AND "time" <= $__timeTo LIMIT 50';
|
||||
const testQuery: SQLQuery = {
|
||||
refId: 'A',
|
||||
sql: {
|
||||
limit: 50,
|
||||
columns: [
|
||||
{
|
||||
parameters: [
|
||||
{
|
||||
name: '*',
|
||||
type: QueryEditorExpressionType.FunctionParameter,
|
||||
},
|
||||
],
|
||||
type: QueryEditorExpressionType.Function,
|
||||
},
|
||||
],
|
||||
},
|
||||
dataset: 'iox',
|
||||
table: 'TestValue',
|
||||
};
|
||||
const result = toRawSql(testQuery);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
@ -26,7 +26,10 @@ export function toRawSql({ sql, table }: SQLQuery): string {
|
||||
}
|
||||
|
||||
// wrapping the column name with quotes
|
||||
const sc = sql.columns.map((c) => ({ ...c, parameters: c.parameters?.map((p) => ({ ...p, name: `"${p.name}"` })) }));
|
||||
const sc = sql.columns.map((c) => ({
|
||||
...c,
|
||||
parameters: c.parameters?.map((p) => ({ ...p, name: formatTableName(p.name) })),
|
||||
}));
|
||||
rawQuery += createSelectClause(sc);
|
||||
|
||||
if (table) {
|
||||
@ -66,6 +69,16 @@ export function toRawSql({ sql, table }: SQLQuery): string {
|
||||
return rawQuery;
|
||||
}
|
||||
|
||||
// When the column name is *, do not wrap the column name in double-quotes.
|
||||
// See: https://github.com/grafana/grafana/issues/88008
|
||||
function formatTableName(parameter: string | undefined): string {
|
||||
if (parameter === '*') {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
return `"${parameter}"`;
|
||||
}
|
||||
|
||||
const isLimit = (limit: number | undefined): boolean => limit !== undefined && limit >= 0;
|
||||
|
||||
// Puts double quotes (") around the identifier if it is necessary.
|
||||
|
@ -40,7 +40,7 @@ export class InfluxVariableSupport extends CustomVariableSupport<InfluxDatasourc
|
||||
query: interpolated,
|
||||
maxDataPoints: request.targets[0].maxDataPoints ?? 1000,
|
||||
},
|
||||
request.range
|
||||
{ range: request.range }
|
||||
)
|
||||
);
|
||||
return metricFindStream.pipe(map((results) => ({ data: results })));
|
||||
|
@ -59,11 +59,19 @@ export const HeatmapPanel = ({
|
||||
|
||||
const info = useMemo(() => {
|
||||
try {
|
||||
return prepareHeatmapData(data.series, data.annotations, options, palette, theme, replaceVariables);
|
||||
return prepareHeatmapData({
|
||||
frames: data.series,
|
||||
annotations: data.annotations,
|
||||
options,
|
||||
palette,
|
||||
theme,
|
||||
replaceVariables,
|
||||
timeRange,
|
||||
});
|
||||
} catch (ex) {
|
||||
return { warning: `${ex}` };
|
||||
}
|
||||
}, [data.series, data.annotations, options, palette, theme, replaceVariables]);
|
||||
}, [data.series, data.annotations, options, palette, theme, replaceVariables, timeRange]);
|
||||
|
||||
const facets = useMemo(() => {
|
||||
let exemplarsXFacet: number[] | undefined = []; // "Time" field
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
GrafanaTheme2,
|
||||
InterpolateFunction,
|
||||
outerJoinDataFrames,
|
||||
TimeRange,
|
||||
ValueFormatter,
|
||||
} from '@grafana/data';
|
||||
import { parseSampleValue, sortSeriesByLabel } from '@grafana/prometheus';
|
||||
@ -65,14 +66,25 @@ export interface HeatmapData {
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
export function prepareHeatmapData(
|
||||
frames: DataFrame[],
|
||||
annotations: DataFrame[] | undefined,
|
||||
options: Options,
|
||||
palette: string[],
|
||||
theme: GrafanaTheme2,
|
||||
replaceVariables: InterpolateFunction = (v) => v
|
||||
): HeatmapData {
|
||||
interface PrepareHeatmapDataOptions {
|
||||
frames: DataFrame[];
|
||||
annotations?: DataFrame[];
|
||||
options: Options;
|
||||
palette: string[];
|
||||
theme: GrafanaTheme2;
|
||||
replaceVariables?: InterpolateFunction;
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export function prepareHeatmapData({
|
||||
frames,
|
||||
annotations,
|
||||
options,
|
||||
palette,
|
||||
theme,
|
||||
replaceVariables = (v) => v,
|
||||
timeRange,
|
||||
}: PrepareHeatmapDataOptions): HeatmapData {
|
||||
if (!frames?.length) {
|
||||
return {};
|
||||
}
|
||||
@ -104,7 +116,7 @@ export function prepareHeatmapData(
|
||||
}
|
||||
|
||||
return getDenseHeatmapData(
|
||||
calculateHeatmapFromData(frames, optionsCopy.calculation ?? {}),
|
||||
calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
|
||||
exemplars,
|
||||
optionsCopy,
|
||||
palette,
|
||||
@ -113,7 +125,7 @@ export function prepareHeatmapData(
|
||||
}
|
||||
|
||||
return getDenseHeatmapData(
|
||||
calculateHeatmapFromData(frames, options.calculation ?? {}),
|
||||
calculateHeatmapFromData(frames, { ...options.calculation, timeRange }),
|
||||
exemplars,
|
||||
options,
|
||||
palette,
|
||||
|
@ -53,7 +53,12 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(HeatmapPanel)
|
||||
// NOTE: this feels like overkill/expensive just to assert if we have an ordinal y
|
||||
// can probably simplify without doing full dataprep
|
||||
const palette = quantizeScheme(opts.color, config.theme2);
|
||||
const v = prepareHeatmapData(context.data, undefined, opts, palette, config.theme2);
|
||||
const v = prepareHeatmapData({
|
||||
frames: context.data,
|
||||
options: opts,
|
||||
palette,
|
||||
theme: config.theme2,
|
||||
});
|
||||
isOrdinalY = readHeatmapRowsCustomMeta(v.heatmap).yOrdinalDisplay != null;
|
||||
} catch {}
|
||||
}
|
||||
|
@ -20,7 +20,12 @@ export class HeatmapSuggestionsSupplier {
|
||||
}
|
||||
|
||||
const palette = quantizeScheme(defaultOptions.color, config.theme2);
|
||||
const info = prepareHeatmapData(builder.data.series, undefined, defaultOptions, palette, config.theme2);
|
||||
const info = prepareHeatmapData({
|
||||
frames: builder.data.series,
|
||||
options: defaultOptions,
|
||||
palette,
|
||||
theme: config.theme2,
|
||||
});
|
||||
if (!info || info.warning) {
|
||||
return;
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ images = {
|
||||
"plugins_slack": "plugins/slack",
|
||||
"python": "python:3.8",
|
||||
"postgres_alpine": "postgres:12.3-alpine",
|
||||
"mimir": "us.gcr.io/kubernetes-dev/mimir:santihernandezc-validate_grafana_am_config-1e903e462-WIP",
|
||||
"mimir": "grafana/mimir-alpine:r295-a23e559",
|
||||
"mysql5": "mysql:5.7.39",
|
||||
"mysql8": "mysql:8.0.32",
|
||||
"redis_alpine": "redis:6.2.11-alpine",
|
||||
|
31
yarn.lock
31
yarn.lock
@ -2503,6 +2503,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/ecma402-abstract@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "@formatjs/ecma402-abstract@npm:2.0.0"
|
||||
dependencies:
|
||||
"@formatjs/intl-localematcher": "npm:0.5.4"
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/41543ba509ea3c7d6530d57b888115f7ca242f13462a951fae4d1d1f28bae10c999f4dea28a71d2f08366d4889a3f5276cae3a16c6f6417b841a84fd314c2234
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/fast-memoize@npm:1.2.6":
|
||||
version: 1.2.6
|
||||
resolution: "@formatjs/fast-memoize@npm:1.2.6"
|
||||
@ -2533,6 +2543,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-durationformat@npm:^0.2.4":
|
||||
version: 0.2.4
|
||||
resolution: "@formatjs/intl-durationformat@npm:0.2.4"
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract": "npm:2.0.0"
|
||||
"@formatjs/intl-localematcher": "npm:0.5.4"
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/5f500409a20d18967e17ffbc222f9b4c4bf7ef08cce20023c33f06d1989c2bc4cf700d1dd1d048748d0a36c882109d5375896a4964d6700f73ec18914c6de4ba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-localematcher@npm:0.2.31":
|
||||
version: 0.2.31
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.2.31"
|
||||
@ -2542,6 +2563,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@formatjs/intl-localematcher@npm:0.5.4":
|
||||
version: 0.5.4
|
||||
resolution: "@formatjs/intl-localematcher@npm:0.5.4"
|
||||
dependencies:
|
||||
tslib: "npm:^2.4.0"
|
||||
checksum: 10/780cb29b42e1ea87f2eb5db268577fcdc53da52d9f096871f3a1bb78603b4ba81d208ea0b0b9bc21548797c941ce435321f62d2522795b83b740f90b0ceb5778
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@gar/promisify@npm:^1.0.1":
|
||||
version: 1.1.3
|
||||
resolution: "@gar/promisify@npm:1.1.3"
|
||||
@ -17036,6 +17066,7 @@ __metadata:
|
||||
"@emotion/react": "npm:11.11.4"
|
||||
"@fingerprintjs/fingerprintjs": "npm:^3.4.2"
|
||||
"@floating-ui/react": "npm:0.26.16"
|
||||
"@formatjs/intl-durationformat": "npm:^0.2.4"
|
||||
"@glideapps/glide-data-grid": "npm:^6.0.0"
|
||||
"@grafana/aws-sdk": "npm:0.3.3"
|
||||
"@grafana/azure-sdk": "npm:0.0.3"
|
||||
|
Loading…
Reference in New Issue
Block a user