Merge remote-tracking branch 'origin/main' into secrets-manager-plugin

This commit is contained in:
Michael Mandrus
2022-05-24 16:14:18 -04:00
59 changed files with 984 additions and 437 deletions
+38 -38
View File
@@ -15,7 +15,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -80,7 +80,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -168,7 +168,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -434,7 +434,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -504,7 +504,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -594,7 +594,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -683,7 +683,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -744,7 +744,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -828,7 +828,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1261,7 +1261,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1334,7 +1334,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: windows-init
@@ -1422,7 +1422,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1507,7 +1507,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1813,7 +1813,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -1937,7 +1937,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2014,7 +2014,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: windows-init
@@ -2073,7 +2073,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2430,7 +2430,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2609,7 +2609,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2733,7 +2733,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -2808,7 +2808,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2886,7 +2886,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -2947,7 +2947,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3026,7 +3026,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3088,7 +3088,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3126,7 +3126,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3164,7 +3164,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3220,7 +3220,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3268,7 +3268,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3317,7 +3317,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3593,7 +3593,7 @@ steps:
name: identify-runner
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3711,7 +3711,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -3782,7 +3782,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/windows/grabpl.exe
-OutFile grabpl.exe
image: grafana/ci-wix:0.1.1
name: windows-init
@@ -3830,7 +3830,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4185,7 +4185,7 @@ services: []
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4355,7 +4355,7 @@ services:
steps:
- commands:
- mkdir -p bin
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/grabpl
- curl -fL -o bin/grabpl https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/grabpl
- chmod +x bin/grabpl
image: byrnedo/alpine-curl:0.1.8
name: grabpl
@@ -4470,7 +4470,7 @@ steps:
name: identify-runner
- commands:
- $$ProgressPreference = "SilentlyContinue"
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.48/windows/grabpl.exe
- Invoke-WebRequest https://grafana-downloads.storage.googleapis.com/grafana-build-pipeline/v2.9.49/windows/grabpl.exe
-OutFile grabpl.exe
- git clone "https://$$env:GITHUB_TOKEN@github.com/grafana/grafana-enterprise.git"
- cd grafana-enterprise
@@ -4667,6 +4667,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: 39c6fd27fc223b7bc99242101d28f508cdbbf01b561330bb52979f1d032af5d5
hmac: a697f4b02af2da0fc6c0d751e40e59db542541d6024e5f0c2b062c7060d635ab
...
+33
View File
@@ -1,3 +1,36 @@
<!-- 9.0.0-beta1 START -->
# 9.0.0-beta1 (2022-05-24)
### Features and enhancements
- **AccessControl:** Add setting for permission cache. (Enterprise)
- **AccessControl:** Check dashboard permissions for reports. (Enterprise)
- **Auth:** Remove grafana ui dependency to the aws sdk. [#43559](https://github.com/grafana/grafana/pull/43559), [@sunker](https://github.com/sunker)
- **BasicRoles:** Add API endpoint to reset basic roles permissions to factory. (Enterprise)
- **LDAP Mapping:** Allow Grafana Admin mapping without org role. [#37189](https://github.com/grafana/grafana/pull/37189), [@krzysdabro](https://github.com/krzysdabro)
- **Licensing:** Only enforce total number of users. (Enterprise)
- **Loki:** do not convert NaN to null. [#45389](https://github.com/grafana/grafana/pull/45389), [@gabor](https://github.com/gabor)
- **Report:** API support for multiple dashboards. (Enterprise)
- **Report:** Support sending embedded image in the report email. (Enterprise)
- **Report:** UI for multiple dashboards. (Enterprise)
- **Reporting:** Remove redundant empty attachment when export to CSV is enabled. (Enterprise)
- **SAML:** Implement Name Templates for assertion_attribute_name option. (Enterprise)
- **SSE/Alerting:** Support prom instant vector responses. [#44865](https://github.com/grafana/grafana/pull/44865), [@kylebrandt](https://github.com/kylebrandt)
- **Tracing:** Add trace to metrics config behind feature toggle. [#46298](https://github.com/grafana/grafana/pull/46298), [@connorlindsey](https://github.com/connorlindsey)
### Bug fixes
- **Fix:** Prevent automatic parsing of string data types to numbers. [#46035](https://github.com/grafana/grafana/pull/46035), [@joshhunt](https://github.com/joshhunt)
- **Prometheus:** Fix inconsistent labels in exemplars resulting in marshal json error. [#46135](https://github.com/grafana/grafana/pull/46135), [@hanjm](https://github.com/hanjm)
### Breaking changes
In the Loki data source, for consistency and performance reasons, we changed how we represent `NaN` (not a number) values received from Loki. In the past versions, we converted these to `null` in the frontend (for dashboard and explore), and kept as `NaN` in the alerting path. Starting with this version, we will always keep it as `NaN`. This change should be mostly invisible for the users. Issue [#45389](https://github.com/grafana/grafana/issues/45389)
The dependency to [grafana/aws-sdk](https://github.com/grafana/grafana-aws-sdk-react) is moved from [grafana/ui](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/package.json) to the plugin. This means that any plugin that use SIGV4 auth need to pass a SIGV4 editor component as a prop to the `DataSourceHttpSettings` component. Issue [#43559](https://github.com/grafana/grafana/issues/43559)
<!-- 9.0.0-beta1 END -->
<!-- 8.5.3 START -->
# 8.5.3
-3
View File
@@ -1174,9 +1174,6 @@ promQueryBuilder = true
# The new loki visual query builder
lokiQueryBuilder = true
# InfluxDB backend migration
influxdbBackendMigration = true
# Experimental Explore to Dashboard workflow
explore2Dashboard = true
@@ -0,0 +1,19 @@
+++
title = "About alert rules"
description = "Learn about Grafana alert rules"
keywords = ["grafana", "alerting", "rules"]
weight = 101
+++
# About alert rules
An alerting rule is a set of evaluation criteria that determines whether an alert instance will fire. The rule consists of one or more queries and expressions, a condition, the frequency of evaluation, and optionally, the duration over which the condition is met.
While queries and expressions select the data set to evaluate, a condition sets the threshold that an alert must meet or exceed to create an alert.
An interval specifies how frequently an alerting rule is evaluated. Duration, when configured, indicates how long a condition must be met. The alert rules can also define alerting behavior in the absence of data.
- [Alert rule types]({{< relref "./alert-rule-types.md" >}})
- [Alert instances]({{< relref "./alert-instances.md" >}})
- [Organising alert rules]({{< relref "./organising-alerts.md" >}})
- [Annotation and labels]({{< relref "../fundamentals/annotation-label/_index.md" >}})
@@ -0,0 +1,22 @@
+++
title = "Alert instances"
description = "Learn about Grafana alert instances"
keywords = ["grafana", "alerting", "instances"]
weight = 103
+++
# Alert instances
Grafana managed alerts support multi-dimensional alerting. Each alert rule can create multiple alert instances. This is exceptionally powerful if you are observing multiple series in a single expression.
Consider the following PromQL expression:
```promql
sum by(cpu) (
rate(node_cpu_seconds_total{mode!="idle"}[1m])
)
```
A rule using this expression will create as many alert instances as the amount of CPUs we are observing after the first evaluation, allowing a single rule to report the status of each CPU.
{{< figure src="/static/img/docs/alerting/unified/multi-dimensional-alert.png" caption="A multi-dimensional Grafana managed alert rule" >}}
@@ -0,0 +1,30 @@
+++
title = "Alert rule types"
description = "Learn about the different alert rule types"
keywords = ["grafana", "alerting", "rule types"]
weight = 102
+++
# Alert rule types
Grafana supports several alert rule types, the following sections will explain their merits and demerits and help you choose the right alert type for your use case.
## Grafana managed rules
Grafana-managed rules are the most flexible alert rule type. They allow you to create alerts that can act on data from any of your existing data sources.
In additional to supporting any datasource you can also add additional [expressions]({{< relref "../../panels/query-a-data-source/use-expressions-to-manipulate-data/_index.md" >}}) to transform your data and express alert conditions.
## Mimir, Loki and Cortex rules
To create Mimir, Loki or Cortex alerts you must have a compatible Prometheus data source. You can check if your data source is compatible by testing the data source and checking the details if the ruler API is supported.
{{< figure src="/static/img/docs/alerting/unified/mimir-datasource-check.png" caption="Successfully connected to a Mimir Prometheus datasource" max-width="40%" >}}
## Recording rules
Recording rules are only available for compatible Prometheus data sources like Mimir, Loki and Cortex.
A recording rule allows you to save an expression's result to a new set of time series. This is useful if you want to run alerts on aggregated data or if you have dashboards that query the same expression repeatedly.
Read more about [recording rules](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/) in Prometheus.
@@ -0,0 +1,22 @@
+++
title = "Organising alert rules"
description = "Learn how to organize alert rules"
keywords = ["grafana", "alerting", "organization"]
weight = 104
+++
## Namespaces and groups
Alerts can be organized using Folders for Grafana-managed rules and namespaces for Mimir or Loki rules and group names.
### Namespaces
When creating Grafana-managed rules, the folder can be used to perform access control and grant or deny access to all rules within a specific folder.
### Groups
All rules within a group are evaluated at the same **interval**.
Alert rules and recording rules within a group will always be evaluated **sequentially**, meaning no rules will be evaluated at the same time and in order of appearance.
> **Note** If you want rules to be evaluated concurrently and with different intervals, consider storing them in different groups.
@@ -7,24 +7,51 @@ title = "State and health of alerting rules"
# State and health of alerting rules
The state and health of alerting rules help you understand several key status indicators about your alerts. There are three key components: alert state, alerting rule state, and alerting rule health. Although related, each component conveys subtly different information.
The state and health of alerting rules help you understand several key status indicators about your alerts.
## Alerting rule state
There are three key components: [alert rule state](#alert-rule-state), [alert instance state](#alert-instance-state), and [alert rule health](#alert-rule-health). Although related, each component conveys subtly different information.
- **Normal**: None of the time series returned by the evaluation engine is in a Pending or Firing state.
- **Pending**: At least one time series returned by the evaluation engine is Pending.
- **Firing**: At least one time series returned by the evaluation engine is Firing.
## Alert rule state
## Alert state
An alert rule can be in either of the following states:
- **Normal**: Condition for the alerting rule is **false** for every time series returned by the evaluation engine.
- **Alerting**: Condition of the alerting rule is **true** for at least one time series returned by the evaluation engine. The duration for which the condition must be true before an alert fires, if set, is met or has exceeded.
- **Pending**: Condition of the alerting rule is **true** for at least one time series returned by the evaluation engine. The duration for which the condition must be true before an alert fires, if set, **has not** been met.
- **NoData**: the alerting rule has not returned a time series, all values for the time series are null, or all values for the time series are zero.
- **Error**: Error when attempting to evaluate an alerting rule.
| State | Description |
| ----------- | ---------------------------------------------------------------------------------------------- |
| **Normal** | None of the time series returned by the evaluation engine is in a `Pending` or `Firing` state. |
| **Pending** | At least one time series returned by the evaluation engine is `Pending`. |
| **Firing** | At least one time series returned by the evaluation engine is `Firing`. |
## Alerting rule health
> **Note:** Alerts will transition first to `pending` and then `firing`, thus it will take at least two evaluation cycles before an alert is fired.
- **Ok**: No error when evaluating an alerting rule.
- **Error**: Error when evaluating an alerting rule.
- **NoData**: The absence of data in at least one time series returned during a rule evaluation.
## Alert instance state
An alert instance can be in either of the following states:
| State | Description |
| ------------ | --------------------------------------------------------------------------------------------- |
| **Normal** | The state of an alert that is neither firing nor pending, everything is working correctly. |
| **Pending** | The state of an alert that has been active for less than the configured threshold duration. |
| **Alerting** | The state of an alert that has been active for longer than the configured threshold duration. |
| **NoData** | No data has been received for the configured time window. |
| **Error** | The error that occurred when attempting to evaluate an alerting rule. |
## Alert rule health
An alert rule can have one the following health statuses:
| State | Description |
| ---------- | ---------------------------------------------------------------------------------- |
| **Ok** | No error when evaluating an alerting rule. |
| **Error** | An error occurred when evaluating an alerting rule. |
| **NoData** | The absence of data in at least one time series returned during a rule evaluation. |
## Special alerts for `NoData` and `Error`
When evaluation of an alerting rule produces state `NoData` or `Error`, Grafana alerting will generate alert instances that have the following additional labels:
| Label | Description |
| ------------------ | ---------------------------------------------------------------------- |
| **alertname** | Either `DatasourceNoData` or `DatasourceError` depending on the state. |
| **datasource_uid** | The UID of the data source that caused the state. |
You can handle these alerts the same way as regular alerts by adding a silence, route to a contact point, and so on.
+2 -2
View File
@@ -10,13 +10,13 @@ aliases = ["/docs/grafana/latest/alerting/unified-alerting/silences/"]
Use silences to stop notifications from one or more alerting rules. Silences do not prevent alert rules from being evaluated. Nor do they not stop alerting instances from being shown in the user interface. Silences only stop notifications from getting created. A silence lasts for only a specified window of time.
Silences do not prevent alert rules from being evaluated. They also do not stop alert instances being shown in the user interface. Silences only prevent notifications from being created.
Silences do not prevent alert rules from being evaluated. They also do not stop alert instances from being shown in the user interface. Silences only prevent notifications from being created.
You can configure Grafana managed silences as well as silences for an [external Alertmanager data source]({{< relref "../../datasources/alertmanager.md" >}}). For more information, see [Alertmanager]({{< relref "../fundamentals/alertmanager.md" >}}).
See also:
- [About label matching for alert suppression]({{< relref "./label-matching-alert-suppression.md" >}})
- [How label matching works]({{< relref "../fundamentals/annotation-label/labels-and-label-matchers.md" >}})
- [Create a silence]({{< relref "./create-silence.md" >}})
- [Create a URL to link to a silence form]({{< relref "./linking-to-silence-form.md" >}})
- [Edit silences]({{< relref "./edit-silence.md" >}})
@@ -13,12 +13,12 @@ Silences stop notifications from getting created and last for only a specified w
To add a silence:
1. In the Grafana menu, click the **Alerting** (bell) icon to open the Alerting page listing existing alerts.
2. In the Alerting page, click **Silences** to open the page listing existing contact points.
2. On the Alerting page, click **Silences** to open the page listing existing contact points.
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.
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. For more information, see [Label matching for alert suppression]({{< relref "./label-matching-alert-suppression.md" >}}).
7. In the **Name** and **Value** fields, enter one or more _Matching Labels_. Matchers determine which rules the silence will apply to. For more information, see [how label matching works]({{< relref "../fundamentals/annotation-label/labels-and-label-matchers.md" >}}).
8. In **Comment**, add details about the silence.
9. In **Creator**, enter the name of the silence owner or keep the default owner.
10. Click **Create**.
@@ -1,21 +0,0 @@
+++
title = "Label matching and alert suppression"
description = "Silences alert notifications"
keywords = ["grafana", "alerting", "silence", "mute"]
weight = 452
aliases = ["/docs/grafana/latest/alerting/unified-alerting/silences/"]
+++
# About label matching for alert suppression
Grafana suppresses notifications only for alerts with labels that match all the "Matching Labels" specified in the silence.
- The **Label** field is the name of the label to match. It must exactly match the label name.
- The **Operator** field is the operator to match against the label value. The available operators are:
- `=`: Select labels that are exactly equal to the provided string.
- `!=`: Select labels that are not equal to the provided string.
- `=~`: Select labels that regex-match the provided string.
- `!~`: Select labels that do not regex-match the provided string.
- The **Value** field matches against the corresponding value for the specified **Label** name. How it matches depends on the **Operator** value.
+1
View File
@@ -9,6 +9,7 @@ weight = 10000
Here you can find detailed release notes that list everything that is included in every release as well as notices
about deprecations, breaking changes as well as changes that relate to plugin development.
- [Release notes for 9.0.0-beta1]({{< relref "release-notes-9-0-0-beta1" >}})
- [Release notes for 8.5.3]({{< relref "release-notes-8-5-3" >}})
- [Release notes for 8.5.2]({{< relref "release-notes-8-5-2" >}})
- [Release notes for 8.5.1]({{< relref "release-notes-8-5-1" >}})
@@ -0,0 +1,36 @@
+++
title = "Release notes for Grafana 9.0.0-beta1"
hide_menu = true
+++
<!-- Auto generated by update changelog github action -->
# Release notes for Grafana 9.0.0-beta1
### Features and enhancements
- **AccessControl:** Add setting for permission cache. (Enterprise)
- **AccessControl:** Check dashboard permissions for reports. (Enterprise)
- **Auth:** Remove grafana ui dependency to the aws sdk. [#43559](https://github.com/grafana/grafana/pull/43559), [@sunker](https://github.com/sunker)
- **BasicRoles:** Add API endpoint to reset basic roles permissions to factory. (Enterprise)
- **LDAP Mapping:** Allow Grafana Admin mapping without org role. [#37189](https://github.com/grafana/grafana/pull/37189), [@krzysdabro](https://github.com/krzysdabro)
- **Licensing:** Only enforce total number of users. (Enterprise)
- **Loki:** do not convert NaN to null. [#45389](https://github.com/grafana/grafana/pull/45389), [@gabor](https://github.com/gabor)
- **Report:** API support for multiple dashboards. (Enterprise)
- **Report:** Support sending embedded image in the report email. (Enterprise)
- **Report:** UI for multiple dashboards. (Enterprise)
- **Reporting:** Remove redundant empty attachment when export to CSV is enabled. (Enterprise)
- **SAML:** Implement Name Templates for assertion_attribute_name option. (Enterprise)
- **SSE/Alerting:** Support prom instant vector responses. [#44865](https://github.com/grafana/grafana/pull/44865), [@kylebrandt](https://github.com/kylebrandt)
- **Tracing:** Add trace to metrics config behind feature toggle. [#46298](https://github.com/grafana/grafana/pull/46298), [@connorlindsey](https://github.com/connorlindsey)
### Bug fixes
- **Fix:** Prevent automatic parsing of string data types to numbers. [#46035](https://github.com/grafana/grafana/pull/46035), [@joshhunt](https://github.com/joshhunt)
- **Prometheus:** Fix inconsistent labels in exemplars resulting in marshal json error. [#46135](https://github.com/grafana/grafana/pull/46135), [@hanjm](https://github.com/hanjm)
### Breaking changes
In the Loki data source, for consistency and performance reasons, we changed how we represent `NaN` (not a number) values received from Loki. In the past versions, we converted these to `null` in the frontend (for dashboard and explore), and kept as `NaN` in the alerting path. Starting with this version, we will always keep it as `NaN`. This change should be mostly invisible for the users. Issue [#45389](https://github.com/grafana/grafana/issues/45389)
The dependency to [grafana/aws-sdk](https://github.com/grafana/grafana-aws-sdk-react) is moved from [grafana/ui](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/package.json) to the plugin. This means that any plugin that use SIGV4 auth need to pass a SIGV4 editor component as a prop to the `DataSourceHttpSettings` component. Issue [#43559](https://github.com/grafana/grafana/issues/43559)
+4
View File
@@ -9,6 +9,10 @@ weight = 1
Grafana is changing all the time. For release highlights checkout links below, if you want a complete list of every change, as well
as info on deprecations, breaking changes and plugin development read the [release notes]({{< relref "../release-notes" >}}).
## Grafana 9
- [What's new in 9.0]({{< relref "whats-new-in-v9-0" >}})
## Grafana 8
- [What's new in 8.5]({{< relref "whats-new-in-v8-5" >}})
+227
View File
@@ -0,0 +1,227 @@
+++
aliases = ["/docs/grafana/latest/guides/whats-new-in-v9-0/", "/docs/grafana/latest/whatsnew/whats-new-in-v9-0/"]
description = "Feature and improvement highlights for Grafana v8.5"
keywords = ["grafana", "new", "documentation", "8.5", "release notes"]
title = "What's new in Grafana v9.0"
weight = -33
[_build]
list = false
+++
# What’s new in Grafana v9.0
As tradition goes, GrafanaCon - our yearly community event for Grafana open source users, is also where we launch the latest software release of Grafana. Keeping up with tradition, we are excited to be announcing Grafana v9.0 - a release that elevates Grafana’s ease of use, discovery of data through new and improved visualizations and a default unified alerting experience.
A big focus for Grafana is making observability and data visualization and analytics easier and more accessible for everyone. For popular data sources like Prometheus and Loki writing and understanding queries can be hard. This is why we are excited to announce that Grafana 10 comes with new visual query builders for both these data sources. These visual query builders will lower the barrier of entry and they help anyone to compose, understand and learn how the underlying query languages.
The release also includes a brand new powerful and fast heatmap visualization, a more accessible navigation menu, improvements to dashboard search, advanced security and authentication features, and more.
We’ve summarized what’s new in the release here, but you might also be interested in the announcement blog post as well. If you’d like all the details you can check out the complete [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md).
## Prometheus query builder
Prometheus queries are not the easiest to write or understand. The PromQL query language is very complex and advanced. Even relatively simple queries are hard to compose and read for new users. This is why we are introducing a new UI query builder that allows anyone to compose and learn how Prometheus queries work.
{{< figure src="/static/img/docs/prometheus/prom_query_builder_light.png" max-width="750px" caption="Prometheus query builder" >}}
### Metric and label filters
This new query builder allows you to easily search and select a metric as well as building label filters. You can start either the selecting a metric or a label filter as they both act as filters on each other. The metric selector allow you to search on multiple parts of the metric name at the same time, just separate each search string with a whitespace.
### Operations
All functions, aggregations and binary operations are added via the + Operation button. Operations are presented in the order they are executed, not in the inverted order they are written in the text query. This makes queries a lot easier to read and reason about as you now clearly see what function parameter is going to which function as well as information about parameter names and function documentation integrated into the builder.
### Range vector
The query builder will automatically mange and add the range vector, it will be shown as a parameter to the operations that require a range (rate, delta, increase etc).
### Binary operations
Simple binary operations like multiple or divide by scaler are represented via simple operation boxes like other functions but the builder also supports binary operations on nested query expressions.
### Switch between code and builder
You can switch between the text edit mode (Code) and the UI builder mode while having your text changes preserved as the visual builder model is derived from the text query using a full featured PromQL query parser.
## Loki query builder
Loki also has complex and powerful query language in LogQL. In many ways it’s more complex and has more syntax to remember than PromQL. Luckily the new query builder we are introducing in Grafana v 9.0 will help you write and understand Loki queries without knowing any of the syntax. Take a look at this example log query.
{{< figure src="/static/img/docs/loki/loki_query_builder_log_query_light_theme.png" max-width="750px" caption="Loki query builder" >}}
In the query builder you can add and edit label filters, line filters, parsers and functions. The image above shows a log query but you can also do metric queries.
{{< figure src="/static/img/docs/loki/loki_query_builder_log_query_light_theme.png" max-width="750px" caption="Loki query builder" >}}
The Loki query builder supports all the features listed for the prometheus query builder above, including support for nested binary operations, explain mode and switching between text editor and builder while preserving changes.
## New heatmap panel
We are replacing the old heatmap panel with a new modern panel that is using the new panel option architecture.
{{< figure src="/static/img/docs/heatmap-panel/heatmap_panel.png" max-width="500px" caption="New heatmap panel" >}}
New new heatmap panel has a number enhancements compared to the old version.
- Multiple orders of magnitude faster
- Supports showing exemplars (traces) overlay
- Supports Prometheus sparse histograms
- Supports changing number of colors steps
- For unbucketed data, it performs smarter auto bucket sizing
- Supports filtering out bucket values close to but not exactly zero
The new heatmap by default assumes that the data is pre-bucked. So if your query returns time series each series is seen as separate bucket (y axis tick). The panel is so much faster than the old one so it can render many time series with thousands of data points each without issue.
{{< figure src="/static/img/docs/heatmap-panel/heatmap_with_time_series_light_theme.png" max-width="500px" caption="Heatmap panel with time series" >}}
## Unified alerting is the default in Grafana 9
Unified alerting is now on by default if you upgrade from an earlier version of Grafana. If you have been using legacy alerting in an earlier version of Grafana and you upgrade to Grafana 9 your alert rules will be automatically migrated and the legacy alerting interface will be replaced by the unified alerting interface.
Unified alerting has been available since June, 2021, it now provides feature parity with legacy alerting and many additional benefits. To find out more on the process to revert back to legacy alerts if needed, click [here]({{< relref "../alerting/opt-in.md#disable-grafana-alertsd#" >}}). Note that if you do revert back (by setting the Grafana config flag GF_UNIFIED_ALERTING_ENABLED to false), that we expect to remove legacy alerting in the next major Grafana release, Grafana 10.
### Alert state history for Grafana managed alerts
Alert state history for Grafana managed alerts indicates the cause of the state. Alert rules can be configured to use the "Alerting" or "OK" states when rule evaluations result in Error or NoData conditions. This change tracks the difference between, for example, Alerting, Alerting (NoData), and Alerting (Error). This change also updates the UI to handle the new serialized states: filtering, sorting, and coloring.
### Images in notifications for Grafana managed alerts
Unified alerting now provides the ability to capture an image for Grafana managed alerts and deliver it via a notification. This is configurable via the contact points tab of Unified alerting . This functionality was available in legacy dashboard alerting, but was missing in unified alerting. It requires the alert to be related to a specific dashboard using the “Dashboard UID” field.
## Envelope encryption is generally available and enabled by default
Grafana now uses envelope encryption to encrypt secrets in the database. Envelope encryption adds a layer of indirection to the encryption process. Instead of encrypting all secrets in the database with a single key, Grafana uses a set of keys called data encryption keys (DEKs) to encrypt them. These data encryption keys are themselves encrypted with a single key encryption key (KEK). This allows you to rotate your KEK and quickly re-encrypt your DEKs in case a key is compromised. In Grafana Enterprise, you can also store your KEK in an external Key Management Service (KMS) like AWS KMS or Azure Key Vault, for extra security. Learn more about envelope encryption in the [docs](https://grafana.com/docs/grafana/next/administration/database-encryption/#envelope-encryption).
{{< figure src="/static/img/docs/enterprise/dashboards/envelope-encryption.png" max-width="750px" caption="Envelope encryption" >}}
## Dashboard previews (opt-in beta)
The dashboard previews initially came to life during our internal [Grafana Labs Hackathon](https://grafana.com/blog/2022/01/14/all-about-the-grafana-labs-hackathon-2.0/?), and are now ready for an opt-in beta release. The previews provide an overview of all available dashboards and help you quickly find the right dashboard when the dashboard names aren’t enough.
Refer to the [Dashboard previews topic](https://grafana.com/docs/grafana/next/dashboards/previews/) for instructions on how to enable the feature and current limitations.
{{< figure src="/static/img/docs/dashboards/dashboard-previews.png" max-width="750px" caption="Dashboard previews" >}}
## Panel title search
In addition to searching dashboards by title, you can now search panels as well. If a panel’s title matches your search query, it will be displayed in the search results.
## Expanding the navigation bar
You can expand the navigation bar for a better overview of Grafana’s features and installed integrations. This is the first improvement to Grafana’s navigation, with more to come in future releases.
{{< figure src="/static/img/docs/expanding-nav-bar-9-0.png" max-width="750px" caption="Expanding nav bar" >}}
## Starred dashboards in the navigation bar
As part of the upcoming improvements to Grafana’s navigation, you can now directly access your starred dashboards from the navigation bar. This is currently an opt-in feature that can be accessed by enabling the savedItems feature flag.
{{< figure src="/static/img/docs/dashboards/starred-dashboards-nav-bar.png" max-width="750px" caption="Starred dashboards" >}}
## Explore to Dashboard
It is now possible to create panels and/or dashboards directly from Explore. When clicking on the “Add to dashboard” button in the Explore toolbar users can now create a panel in a new dashboard or in an existing one. The generated panel contains all the pane’s queries and a default visualization automatically picked from the current results shown in Explore. Dashboards are not automatically saved so that users can go through the current save flow. Optionally, dashboards can be opened in a new tab to preserve Explore’s state.
{{< figure src="/static/img/docs/dashboards/explore-to-dashboard-9-0.png" max-width="500px" caption="Explore to dashboard" >}}
## Command palette
Using cmd+k (macOS) or ctrl+k (Linux/Windows), users can pull up a palette of commands that allow easier navigation and other tasks. <!-- Currently available commands:-->
## List of errors with trace IDs
In order to support debugging issues in Grafana, error alerts that appear in Grafana include a trace ID, and these alerts can be accessed under your Profile. This is currently an opt-in feature that can be accessed by enabling two feature flags:
- `tracing` - enables the tracing id in error messages
- `persistNotifications` - enables the alerts page under Profile
{{< figure src="/static/img/docs/dashboards/errors-with-trace-ids.png" max-width="500px" caption="Errors with trace IDs" >}}
## Query History migration
Query history is a panel that keeps track of all queries that are run in Explore. So far the history was saved in browser local storage causing some problems, for example running out of storage space or not being able to use the same query history in different browsers. Not anymore! We’re migrating all entries from browser local storage to the database. Migration will happen behind the senses when query history panel is opened. This feature can be disabled using config entry query_history in custom.ini.
{{< figure src="/static/img/docs/panels/query-history-9-0.png" max-width="900px" caption="Query history" >}}
## Enterprise
### Access control
### RBAC is generally available
Fine-grained access control is now called role-based access control, or RBAC. It is now enabled by default in Grafana Enterprise and Grafana Cloud Advanced, so you can create and define custom roles like Annotations Writer, Users Viewer, and Dashboard Permissions Writer for users, teams, or service accounts. Learn more about RBAC in the [docs](https://grafana.com/docs/grafana/next/enterprise/access-control/).
{{< figure src="/static/img/docs/enterprise/rbac-ga.png" max-width="755px" caption="RBAC is GA" >}}
## Reporting
### Add multiple dashboards to a single report
If you have information spread across several dashboards that you’d like to send out to a group of recipients periodically, you can now add multiple dashboards to a single report and send it all in one email. Learn more about Reporting in the [Reporting docs](https://grafana.com/docs/grafana/latest/enterprise/reporting/#reporting).
### Embed an image of a dashboard in a report
Reporting is all about convenience - getting info to eyeballs as quickly as possible. Now you can embed an image of a dashboard directly within a report email, so your recipients can see it right when they open the message instead of opening an attached PDF. This is especially handy for SLOs, financial results, or other at-a-glance data. Learn more about Reporting in the [Reporting docs](https://grafana.com/docs/grafana/latest/enterprise/reporting/#reporting).
{{< figure src="/static/img/docs/dashboards/report-embedded-image.png" max-width="750px" caption="Embed report" >}}
## Breaking Changes
This is a partial list of notable breaking changes. For the complete list, see our [Release Notes](LINK-TO-9.0-RELEASE-NOTES).
### Role-based access control: changes for general release
Fine-grained access control is now called "Role-based access control". As part of the Grafana 9.0 release, the service is generally available, and there are several breaking changes:
Built-in roles are now called basic roles, and they now consist of permissions, not roles
The Terraform builtin_role_assignment resource is going away.
Grafana provisioning has a new schema, please refer to the documentation for more information.
Basic roles no longer support permission inheritance. Previously, when permissions of a Viewer basic role were modified, it was propagated to the Editor and Admin basic roles. With the Grafana 9.0 release, this is not the case anymore.
### Loki: logs data format changed
In the Loki data source, the data format used to represent Loki logs-data has been changed to a more efficient format. (NOTE: this change applies to logs data only, it does not apply to numeric data)
The logs are represented by a single dataframe with a "labels" field added, instead of separate dataframes for every label combination. Displaying logs data in explore, or in a dashboard using the logs panel will continue to work without changes. But, when displaying logs data in other dashboard panels, for example in a table visualization, changes will be visible, and configurations might need to be adjusted. For example, if the "Labels to fields" transformation was used, it has to be replaced with an “Extract fields” transformation, where the “labels” field is chosen as the source.
### Loki: NaN values representation changed in numeric data
In the Loki data source, when grafana receives numeric data from Loki, it may contain NaN (not a number) values. For consistency and performance reasons we changed how we represent such values in Grafana. In previous versions, the behavior was different between alerting queries and other queries (like dashboard queries or explore queries). Alerting queries kept NaN values unchanged, but other queries converted these values to “null”. Starting with grafana 9.0.0, we will always keep these values unchanged. In other words, queries in dashboards and explore will behave the same as alerting queries in this regard.
### Elasticsearch: Support for versions after their end of life was removed
Support for Elasticsearch versions that are after their end of life ( based on https://www.elastic.co/support/eol ) was removed. This means that versions older than Elasticseach 7.10.0 will not be supported in Grafana 9.0.0.
### Elasticsearch: Support for browser access mode removed
In the Elasticsearch data source, browser access mode was deprecated in grafana 7.4.0 and removed in 9.0.0. If you used this mode, please switch to server access mode on the data source configuration page.
<!-- ### InfluxDB: Support for browser access mode removed (should this stay??)
In the InfluxDB data source, browser access mode was deprecated in grafana 8.0.0 and we are removing this feature in 9.0.0. If you are using this mode, you need to [switch to server access mode]({{< relref "../datasources/influxdb/_index.md##influxql-classic-influxdb-query" >}}) on the data source configuration page or you can do this via provisioning. -->
### Transformations: Allow more complex regex expressions in rename by regex
The rename by regex transformation has been improved to allow global patterns of the form `/<stringToReplace>/g`. Depending on the regex match used, this may cause some transformations to behave slightly differently. You can guarantee the same behavior as before by wrapping the match string in forward slashes (/), for example, (._) would become /(._)/. ([Github Issue #48179](https://github.com/grafana/grafana/pull/48179))
## ## A note on Grafana Enterprise licensing
The Rename by regex transformation has been improved to allow global patterns of the form /<stringToReplace>/g. Depending on the regex match used, this may cause some transformations to behave slightly differently. You can guarantee the same behavior as before by wrapping the match string in forward slashes (/), e.g. (._) becomes /(._)/
Github Issue #48179
## Deprecation Notices
When we release Grafana 9.0 on June 14th, Grafana will no longer enforce viewers and editor-admins differently. That means that regardless of whether your Grafana Enterprise license is tiered or combined, instead of seeing this on the Stats & Licensing page:
{{< figure src="/static/img/docs/enterprise/separate-licenses.png" max-width="500px" caption="Separate license" >}}
You will see this:
{{< figure src="/static/img/docs/enterprise/combined-licenses.png" max-width="500px" caption="Combined license" >}}
It also means that Grafana will count all users the same, regardless of their role, including org roles (Viewer, Editor, Admin) and fine-grained roles (Dashboard Editor, Reports Editor, etc.). You won’t see a separate warning banner or see users locked out if you hit your limit of viewers or editor-admins, only your total combined limit of active users.
For example, if you have a license for 10 active admins and 100 active viewers in your Grafana Enterprise license, then starting in v9.0 you will have a limit of 110 active users, and it doesn’t matter what roles those users have, they will all be counted and enforced the same.
This is a more permissive policy than before.
@@ -77,6 +77,12 @@ export interface AnnotationEventFieldMapping {
}
export type AnnotationEventMappings = Partial<Record<keyof AnnotationEvent, AnnotationEventFieldMapping>>;
type AnnotationQueryEditorProps<TQuery extends DataQuery> = QueryEditorProps<any, TQuery> & {
// Needs to be optional otherwise component not using these cannot be used, even though they are passed on and can be
// just ignored if not used.
annotation?: AnnotationQuery<TQuery>;
onAnnotationChange?: (annotation: AnnotationQuery<TQuery>) => void;
};
/**
* Since Grafana 7.2
@@ -86,7 +92,7 @@ export type AnnotationEventMappings = Partial<Record<keyof AnnotationEvent, Anno
export interface AnnotationSupport<TQuery extends DataQuery = DataQuery, TAnno = AnnotationQuery<TQuery>> {
/**
* This hook lets you manipulate any existing stored values before running them though the processor.
* This is particularly helpful when dealing with migrating old formats. ie query as a string vs object
* This is particularly helpful when dealing with migrating old formats. ie query as a string vs object.
*/
prepareAnnotation?(json: any): TAnno;
@@ -105,5 +111,5 @@ export interface AnnotationSupport<TQuery extends DataQuery = DataQuery, TAnno =
/**
* Specify a custom QueryEditor for the annotation page. If not specified, the standard one will be used
*/
QueryEditor?: ComponentType<QueryEditorProps<any, TQuery>>;
QueryEditor?: ComponentType<AnnotationQueryEditorProps<TQuery>>;
}
@@ -15,6 +15,7 @@ export interface DatePickerProps {
onClose: () => void;
onChange: (value: Date) => void;
value?: Date;
minDate?: Date;
}
/** @public */
@@ -37,7 +38,7 @@ export const DatePicker = memo<DatePickerProps>((props) => {
DatePicker.displayName = 'DatePicker';
const Body = memo<DatePickerProps>(({ value, onChange }) => {
const Body = memo<DatePickerProps>(({ value, minDate, onChange }) => {
const styles = useStyles2(getBodyStyles);
return (
@@ -45,6 +46,7 @@ const Body = memo<DatePickerProps>(({ value, onChange }) => {
className={styles.body}
tileClassName={styles.title}
value={value || new Date()}
minDate={minDate}
nextLabel={<Icon name="angle-right" />}
prevLabel={<Icon name="angle-left" />}
onChange={(ev: Date | Date[]) => {
@@ -68,6 +70,10 @@ export const getStyles = (theme: GrafanaTheme2) => {
background-color: ${theme.colors.background.primary};
border: 1px solid ${theme.colors.border.weak};
border-radius: 2px 0 0 2px;
button:disabled {
color: ${theme.colors.text.disabled};
}
`,
};
};
@@ -12,6 +12,7 @@ export const formatDate = (date: Date | string) => dateTime(date).format('L');
/** @public */
export interface DatePickerWithInputProps extends Omit<InputProps, 'ref' | 'value' | 'onChange'> {
value?: Date | string;
minDate?: Date;
onChange: (value: Date | string) => void;
/** Hide the calendar when date is selected */
closeOnSelect?: boolean;
@@ -21,6 +22,7 @@ export interface DatePickerWithInputProps extends Omit<InputProps, 'ref' | 'valu
/** @public */
export const DatePickerWithInput = ({
value,
minDate,
onChange,
closeOnSelect,
placeholder = 'Date',
@@ -49,6 +51,7 @@ export const DatePickerWithInput = ({
<DatePicker
isOpen={open}
value={value && typeof value !== 'string' ? value : dateTime().toDate()}
minDate={minDate}
onChange={(ev) => {
onChange(ev);
if (closeOnSelect) {
@@ -55,6 +55,8 @@ export function Drawer({
const { overlayProps } = useOverlay(
{
isDismissable: true,
isOpen,
onClose,
},
overlayRef
);
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Modal } from './Modal';
@@ -22,4 +23,22 @@ describe('Modal', () => {
expect(screen.getByTestId('modal-content')).toBeInTheDocument();
});
it('pressing escape calls onDismiss correctly', async () => {
const onDismiss = jest.fn();
render(
<Modal title="Some Title" isOpen onDismiss={onDismiss}>
<div data-testid="modal-content">Content</div>
</Modal>
);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByLabelText('Some Title')).toBeInTheDocument();
expect(screen.getByTestId('modal-content')).toBeInTheDocument();
await userEvent.keyboard('{Escape}');
expect(onDismiss).toHaveBeenCalled();
});
});
@@ -53,7 +53,7 @@ export function Modal(props: PropsWithChildren<Props>) {
// Handle interacting outside the dialog and pressing
// the Escape key to close the modal.
const { overlayProps, underlayProps } = useOverlay(
{ isKeyboardDismissDisabled: closeOnEscape, isOpen, onClose: onDismiss },
{ isKeyboardDismissDisabled: !closeOnEscape, isOpen, onClose: onDismiss },
ref
);
-3
View File
@@ -411,10 +411,7 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
m.Use(func(c *web.Context) {
initCtx.Context = c
initCtx.Logger = log.New("api-test")
c.Map(initCtx)
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx))
c.Map(c.Req)
})
m.Use(accesscontrol.LoadPermissionsMiddleware(hs.AccessControl))
-3
View File
@@ -103,9 +103,6 @@ func contextProvider(tc *testContext) web.Handler {
SkipCache: true,
Logger: log.New("test"),
}
c.Map(reqCtx)
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx))
c.Map(c.Req)
}
}
+2 -1
View File
@@ -213,7 +213,8 @@ func extractGroups(client *http.Client, claims azureClaims, token *oauth2.Token)
if res.StatusCode != http.StatusOK {
if res.StatusCode == http.StatusForbidden {
logger.Error("AzureAD OAuth: failed to fetch user groups. Token need User.Read and GroupMember.Read.All permission")
logger.Warn("AzureAD OAuh: Token need GroupMember.Read.All permission to fetch all groups")
return []string{}, nil
}
return nil, errors.New("error fetching groups")
}
-1
View File
@@ -56,7 +56,6 @@ func RequestMetrics(features featuremgmt.FeatureToggles) web.Handler {
now := time.Now()
httpRequestsInFlight.Inc()
defer httpRequestsInFlight.Dec()
c.Map(c.Req)
c.Next()
handler := "unknown"
-2
View File
@@ -55,8 +55,6 @@ func RequestTracing(tracer tracing.Tracer) web.Handler {
ctx, span := tracer.Start(req.Context(), fmt.Sprintf("HTTP %s %s", req.Method, req.URL.Path), trace.WithLinks(trace.LinkFromContext(wireContext)))
c.Req = req.WithContext(ctx)
c.Map(c.Req)
c.Next()
// Only call span.Finish when a route operation name have been set,
@@ -90,9 +90,6 @@ func contextProvider() web.Handler {
IsSignedIn: true,
SkipCache: true,
}
c.Map(reqCtx)
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx))
c.Map(c.Req)
}
}
@@ -500,10 +500,7 @@ func contextProvider(tc *testContext) web.Handler {
SkipCache: true,
Logger: log.New("test"),
}
c.Map(reqCtx)
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), reqCtx))
c.Map(c.Req)
}
}
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type AlertNotificationService struct {
@@ -33,6 +34,10 @@ func (s *AlertNotificationService) GetAlertNotifications(ctx context.Context, qu
}
func (s *AlertNotificationService) CreateAlertNotificationCommand(ctx context.Context, cmd *models.CreateAlertNotificationCommand) error {
if util.IsShortUIDTooLong(cmd.Uid) {
return ValidationError{Reason: "Invalid UID: Must be 40 characters or less"}
}
var err error
cmd.EncryptedSecureSettings, err = s.EncryptionService.EncryptJsonData(ctx, cmd.SecureSettings, setting.SecretKey)
if err != nil {
@@ -53,6 +58,10 @@ func (s *AlertNotificationService) CreateAlertNotificationCommand(ctx context.Co
}
func (s *AlertNotificationService) UpdateAlertNotification(ctx context.Context, cmd *models.UpdateAlertNotificationCommand) error {
if util.IsShortUIDTooLong(cmd.Uid) {
return ValidationError{Reason: "Invalid UID: Must be 40 characters or less"}
}
var err error
cmd.EncryptedSecureSettings, err = s.EncryptionService.EncryptJsonData(ctx, cmd.SecureSettings, setting.SecretKey)
if err != nil {
@@ -99,6 +108,10 @@ func (s *AlertNotificationService) GetAlertNotificationsWithUid(ctx context.Cont
}
func (s *AlertNotificationService) UpdateAlertNotificationWithUid(ctx context.Context, cmd *models.UpdateAlertNotificationWithUidCommand) error {
if util.IsShortUIDTooLong(cmd.Uid) || util.IsShortUIDTooLong(cmd.NewUid) {
return ValidationError{Reason: "Invalid UID: Must be 40 characters or less"}
}
return s.SQLStore.UpdateAlertNotificationWithUid(ctx, cmd)
}
+21
View File
@@ -2,6 +2,7 @@ package alerting
import (
"context"
"strings"
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -112,6 +113,26 @@ func TestService(t *testing.T) {
err = s.DeleteAlertNotification(context.Background(), &delCmd)
require.NoError(t, err)
})
t.Run("create alert notification should reject an invalid command", func(t *testing.T) {
uid := strings.Repeat("A", 41)
err := s.CreateAlertNotificationCommand(context.Background(), &models.CreateAlertNotificationCommand{Uid: uid})
require.ErrorIs(t, err, ValidationError{Reason: "Invalid UID: Must be 40 characters or less"})
})
t.Run("update alert notification should reject an invalid command", func(t *testing.T) {
ctx := context.Background()
uid := strings.Repeat("A", 41)
expectedErr := ValidationError{Reason: "Invalid UID: Must be 40 characters or less"}
err := s.UpdateAlertNotification(ctx, &models.UpdateAlertNotificationCommand{Uid: uid})
require.ErrorIs(t, err, expectedErr)
err = s.UpdateAlertNotificationWithUid(ctx, &models.UpdateAlertNotificationWithUidCommand{NewUid: uid})
require.ErrorIs(t, err, expectedErr)
})
}
func registerTestNotifier(notifierType string) {
@@ -95,9 +95,8 @@ func (h *ContextHandler) Middleware(mContext *web.Context) {
Logger: log.New("context"),
}
// Inject ReqContext into a request context and replace the request instance in the macaron context
// Inject ReqContext into http.Request.Context
mContext.Req = mContext.Req.WithContext(ctxkey.Set(mContext.Req.Context(), reqContext))
mContext.Map(mContext.Req)
traceID := tracing.TraceIDFromContext(mContext.Req.Context(), false)
if traceID != "" {
@@ -153,8 +152,6 @@ func (h *ContextHandler) Middleware(mContext *web.Context) {
{Num: reqContext.UserId}},
)
mContext.Map(reqContext)
// update last seen every 5min
if reqContext.ShouldUpdateLastSeenAt() {
reqContext.Logger.Debug("Updating last user_seen_at", "user_id", reqContext.UserId)
+2 -9
View File
@@ -12,7 +12,6 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/models"
legacyMetrics "github.com/grafana/grafana/pkg/services/alerting/metrics"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@@ -279,20 +278,14 @@ func (m *OrgRegistries) RemoveOrgRegistry(org int64) {
func Instrument(
method,
path string,
action interface{},
action func(*models.ReqContext) response.Response,
metrics *API,
) web.Handler {
normalizedPath := MakeLabelValue(path)
return func(c *models.ReqContext) {
start := time.Now()
var res response.Response
val, err := c.Invoke(action)
if err == nil && val != nil && len(val) > 0 {
res = val[0].Interface().(response.Response)
} else {
res = routing.ServerError(err)
}
res := action(c)
// TODO: We could look up the datasource type via our datasource service
var backend string
@@ -238,10 +238,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
SignedInUser: signedUser,
Logger: log.New("serviceaccounts-test"),
}
c.Map(ctx)
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), ctx))
c.Map(c.Req)
})
a.RouterRegister.Register(m.Router)
return m, a
+5 -17
View File
@@ -20,27 +20,16 @@ import (
"net"
"net/http"
"net/url"
"reflect"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/infra/log"
)
// ContextInvoker is an inject.FastInvoker wrapper of func(ctx *Context).
type ContextInvoker func(ctx *Context)
// Invoke implements inject.FastInvoker which simplifies calls of `func(ctx *Context)` function.
func (invoke ContextInvoker) Invoke(params []interface{}) ([]reflect.Value, error) {
invoke(params[0].(*Context))
return nil, nil
}
// Context represents the runtime context of current request of Macaron instance.
// It is the integration of most frequently used middlewares and helper methods.
type Context struct {
Injector
handlers []Handler
handlers []http.Handler
index int
*Router
@@ -50,12 +39,12 @@ type Context struct {
logger log.Logger
}
func (ctx *Context) handler() Handler {
func (ctx *Context) handler() http.Handler {
if ctx.index < len(ctx.handlers) {
return ctx.handlers[ctx.index]
}
if ctx.index == len(ctx.handlers) {
return func() {}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
}
panic("invalid index for context handler")
}
@@ -68,9 +57,8 @@ func (ctx *Context) Next() {
func (ctx *Context) run() {
for ctx.index <= len(ctx.handlers) {
if _, err := ctx.Invoke(ctx.handler()); err != nil {
panic(err)
}
ctx.handler().ServeHTTP(ctx.Resp, ctx.Req)
ctx.index++
if ctx.Resp.Written() {
return
-192
View File
@@ -1,192 +0,0 @@
// Copyright 2013 Jeremy Saenz
// Copyright 2015 The Macaron Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
// Package inject provides utilities for mapping and injecting dependencies in various ways.
package web
import (
"fmt"
"reflect"
)
// Injector represents an interface for mapping and injecting dependencies into structs
// and function arguments.
type Injector interface {
Invoker
TypeMapper
}
// Invoker represents an interface for calling functions via reflection.
type Invoker interface {
// Invoke attempts to call the interface{} provided as a function,
// providing dependencies for function arguments based on Type. Returns
// a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
Invoke(interface{}) ([]reflect.Value, error)
}
// FastInvoker represents an interface in order to avoid the calling function via reflection.
//
// example:
// type handlerFuncHandler func(http.ResponseWriter, *http.Request) error
// func (f handlerFuncHandler)Invoke([]interface{}) ([]reflect.Value, error){
// ret := f(p[0].(http.ResponseWriter), p[1].(*http.Request))
// return []reflect.Value{reflect.ValueOf(ret)}, nil
// }
//
// type funcHandler func(int, string)
// func (f funcHandler)Invoke([]interface{}) ([]reflect.Value, error){
// f(p[0].(int), p[1].(string))
// return nil, nil
// }
type FastInvoker interface {
// Invoke attempts to call the ordinary functions. If f is a function
// with the appropriate signature, f.Invoke([]interface{}) is a Call that calls f.
// Returns a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
Invoke([]interface{}) ([]reflect.Value, error)
}
// IsFastInvoker check interface is FastInvoker
func IsFastInvoker(h interface{}) bool {
_, ok := h.(FastInvoker)
return ok
}
// TypeMapper represents an interface for mapping interface{} values based on type.
type TypeMapper interface {
// Maps the interface{} value based on its immediate type from reflect.TypeOf.
Map(interface{}) TypeMapper
// Maps the interface{} value based on the pointer of an Interface provided.
// This is really only useful for mapping a value as an interface, as interfaces
// cannot at this time be referenced directly without a pointer.
MapTo(interface{}, interface{}) TypeMapper
// Returns the Value that is mapped to the current type. Returns a zeroed Value if
// the Type has not been mapped.
GetVal(reflect.Type) reflect.Value
}
type injector struct {
values map[reflect.Type]reflect.Value
}
// InterfaceOf dereferences a pointer to an Interface type.
// It panics if value is not an pointer to an interface.
func InterfaceOf(value interface{}) reflect.Type {
t := reflect.TypeOf(value)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Interface {
panic("Called inject.InterfaceOf with a value that is not a pointer to an interface. (*MyInterface)(nil)")
}
return t
}
// New returns a new Injector.
func NewInjector() Injector {
return &injector{
values: make(map[reflect.Type]reflect.Value),
}
}
// Invoke attempts to call the interface{} provided as a function,
// providing dependencies for function arguments based on Type.
// Returns a slice of reflect.Value representing the returned values of the function.
// Returns an error if the injection fails.
// It panics if f is not a function
func (inj *injector) Invoke(f interface{}) ([]reflect.Value, error) {
t := reflect.TypeOf(f)
switch v := f.(type) {
case FastInvoker:
return inj.fastInvoke(v, t, t.NumIn())
default:
return inj.callInvoke(f, t, t.NumIn())
}
}
func (inj *injector) fastInvoke(f FastInvoker, t reflect.Type, numIn int) ([]reflect.Value, error) {
var in []interface{}
if numIn > 0 {
in = make([]interface{}, numIn) // Panic if t is not kind of Func
var argType reflect.Type
var val reflect.Value
for i := 0; i < numIn; i++ {
argType = t.In(i)
val = inj.GetVal(argType)
if !val.IsValid() {
return nil, fmt.Errorf("value not found for type %v", argType)
}
in[i] = val.Interface()
}
}
return f.Invoke(in)
}
// callInvoke reflect.Value.Call
func (inj *injector) callInvoke(f interface{}, t reflect.Type, numIn int) ([]reflect.Value, error) {
var in []reflect.Value
if numIn > 0 {
in = make([]reflect.Value, numIn)
var argType reflect.Type
var val reflect.Value
for i := 0; i < numIn; i++ {
argType = t.In(i)
val = inj.GetVal(argType)
if !val.IsValid() {
return nil, fmt.Errorf("value not found for type %v", argType)
}
in[i] = val
}
}
return reflect.ValueOf(f).Call(in), nil
}
// Maps the concrete value of val to its dynamic type using reflect.TypeOf,
// It returns the TypeMapper registered in.
func (inj *injector) Map(val interface{}) TypeMapper {
inj.values[reflect.TypeOf(val)] = reflect.ValueOf(val)
return inj
}
func (inj *injector) MapTo(val interface{}, ifacePtr interface{}) TypeMapper {
inj.values[InterfaceOf(ifacePtr)] = reflect.ValueOf(val)
return inj
}
func (inj *injector) GetVal(t reflect.Type) reflect.Value {
val := inj.values[t]
if val.IsValid() {
return val
}
// no concrete types found, try to find implementors
// if t is an interface
if t.Kind() == reflect.Interface {
for k, v := range inj.values {
if k.Implements(t) {
val = v
break
}
}
}
return val
}
+9 -15
View File
@@ -56,14 +56,14 @@ func hack_wrap(Handler) http.HandlerFunc
// validateAndWrapHandler makes sure a handler is a callable function, it panics if not.
// When the handler is also potential to be any built-in inject.FastInvoker,
// it wraps the handler automatically to have some performance gain.
func validateAndWrapHandler(h Handler) Handler {
func validateAndWrapHandler(h Handler) http.Handler {
return hack_wrap(h)
}
// validateAndWrapHandlers preforms validation and wrapping for each input handler.
// It accepts an optional wrapper function to perform custom wrapping on handlers.
func validateAndWrapHandlers(handlers []Handler) []Handler {
wrappedHandlers := make([]Handler, len(handlers))
func validateAndWrapHandlers(handlers []Handler) []http.Handler {
wrappedHandlers := make([]http.Handler, len(handlers))
for i, h := range handlers {
wrappedHandlers[i] = validateAndWrapHandler(h)
}
@@ -74,7 +74,7 @@ func validateAndWrapHandlers(handlers []Handler) []Handler {
// Macaron represents the top level web application.
// Injector methods can be invoked to map services on a global level.
type Macaron struct {
handlers []Handler
handlers []http.Handler
urlPrefix string // For suburl support.
*Router
@@ -136,35 +136,29 @@ func (m *Macaron) UseMiddleware(middleware func(http.Handler) http.Handler) {
} else {
c.Resp = NewResponseWriter(req.Method, rw)
}
c.Map(req)
c.MapTo(rw, (*http.ResponseWriter)(nil))
c.Next()
})
m.handlers = append(m.handlers, Handler(middleware(next)))
m.handlers = append(m.handlers, middleware(next))
}
// Use adds a middleware Handler to the stack,
// and panics if the handler is not a callable func.
// Middleware Handlers are invoked in the order that they are added.
func (m *Macaron) Use(handler Handler) {
handler = validateAndWrapHandler(handler)
m.handlers = append(m.handlers, handler)
h := validateAndWrapHandler(handler)
m.handlers = append(m.handlers, h)
}
func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Context {
c := &Context{
Injector: NewInjector(),
handlers: m.handlers,
index: 0,
Router: m.Router,
Resp: NewResponseWriter(req.Method, rw),
logger: log.New("macaron.context"),
}
req = req.WithContext(context.WithValue(req.Context(), macaronContextKey{}, c))
c.Map(c)
c.MapTo(c.Resp, (*http.ResponseWriter)(nil))
c.Map(req)
c.Req = req
c.Req = req.WithContext(context.WithValue(req.Context(), macaronContextKey{}, c))
return c
}
+6 -6
View File
@@ -146,13 +146,13 @@ func (r *Router) Handle(method string, pattern string, handlers []Handler) {
h = append(h, handlers...)
handlers = h
}
handlers = validateAndWrapHandlers(handlers)
httpHandlers := validateAndWrapHandlers(handlers)
r.handle(method, pattern, func(resp http.ResponseWriter, req *http.Request, params map[string]string) {
c := r.m.createContext(resp, SetURLParams(req, params))
c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
c.handlers = make([]http.Handler, 0, len(r.m.handlers)+len(handlers))
c.handlers = append(c.handlers, r.m.handlers...)
c.handlers = append(c.handlers, handlers...)
c.handlers = append(c.handlers, httpHandlers...)
c.run()
})
}
@@ -194,12 +194,12 @@ func (r *Router) Any(pattern string, h ...Handler) { r.Handle("*", pattern, h) }
// found. If it is not set, http.NotFound is used.
// Be sure to set 404 response code in your handler.
func (r *Router) NotFound(handlers ...Handler) {
handlers = validateAndWrapHandlers(handlers)
httpHandlers := validateAndWrapHandlers(handlers)
r.notFound = func(rw http.ResponseWriter, req *http.Request) {
c := r.m.createContext(rw, req)
c.handlers = make([]Handler, 0, len(r.m.handlers)+len(handlers))
c.handlers = make([]http.Handler, 0, len(r.m.handlers)+len(handlers))
c.handlers = append(c.handlers, r.m.handlers...)
c.handlers = append(c.handlers, handlers...)
c.handlers = append(c.handlers, httpHandlers...)
c.run()
}
}
+3 -4
View File
@@ -33,10 +33,8 @@ func NewServer(t testing.TB, routeRegister routing.RouteRegister) *Server {
m.Use(func(c *web.Context) {
initCtx.Context = c
initCtx.Logger = log.New("api-test")
c.Map(initCtx)
c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx))
c.Map(c.Req)
})
m.Use(requestContextMiddleware())
@@ -129,7 +127,9 @@ func requestContextFromRequest(req *http.Request) *models.ReqContext {
}
func requestContextMiddleware() web.Handler {
return func(res http.ResponseWriter, req *http.Request, c *models.ReqContext) {
return func(res http.ResponseWriter, req *http.Request) {
c := ctxkey.Get(req.Context()).(*models.ReqContext)
ctx := requestContextFromRequest(req)
if ctx == nil {
c.Next()
@@ -145,6 +145,5 @@ func requestContextMiddleware() web.Handler {
c.RequestNonce = ctx.RequestNonce
c.PerfmonTimer = ctx.PerfmonTimer
c.LookupTokenErr = ctx.LookupTokenErr
c.Map(c)
}
}
@@ -9,7 +9,7 @@ import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { PanelModel } from 'app/features/dashboard/state';
import { executeAnnotationQuery } from '../executeAnnotationQuery';
import { standardAnnotationSupport } from '../standardAnnotationSupport';
import { shouldUseLegacyRunner, shouldUseMappingUI, standardAnnotationSupport } from '../standardAnnotationSupport';
import { AnnotationQueryResponse } from '../types';
import { AnnotationFieldMapper } from './AnnotationResultMapper';
@@ -33,7 +33,7 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
}
componentDidUpdate(oldProps: Props) {
if (this.props.annotation !== oldProps.annotation) {
if (this.props.annotation !== oldProps.annotation && !shouldUseLegacyRunner(this.props.datasource)) {
this.verifyDataSource();
}
}
@@ -57,6 +57,13 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
onRunQuery = async () => {
const { datasource, annotation } = this.props;
if (shouldUseLegacyRunner(datasource)) {
// In the new UI the running of query is done so the data can be mapped. In the legacy annotations this does
// not exist as the annotationQuery already returns annotation events which cannot be mapped. This means that
// right now running a query for data source with legacy runner does not make much sense.
return;
}
const dashboard = getDashboardSrv().getCurrent();
if (!dashboard) {
return;
@@ -156,11 +163,15 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
);
}
onAnnotationChange = (annotation: AnnotationQuery) => {
this.props.onChange(annotation);
};
render() {
const { datasource, annotation } = this.props;
const { response } = this.state;
// Find the annotaiton runner
// Find the annotation runner
let QueryEditor = datasource.annotations?.QueryEditor || datasource.components?.QueryEditor;
if (!QueryEditor) {
return <div>Annotations are not supported. This datasource needs to export a QueryEditor</div>;
@@ -177,8 +188,10 @@ export default class StandardAnnotationQueryEditor extends PureComponent<Props,
onRunQuery={this.onRunQuery}
data={response?.panelData}
range={getTimeSrv().timeRange()}
annotation={annotation}
onAnnotationChange={this.onAnnotationChange}
/>
{datasource.type !== 'datasource' && (
{shouldUseMappingUI(datasource) && (
<>
{this.renderStatus()}
<AnnotationFieldMapper response={response} mappings={annotation.mappings} change={this.onMappingChange} />
@@ -9,6 +9,7 @@ import {
AnnotationQuery,
AnnotationSupport,
DataFrame,
DataSourceApi,
Field,
FieldType,
getFieldDisplayName,
@@ -26,6 +27,7 @@ export const standardAnnotationSupport: AnnotationSupport = {
return {
...rest,
target: {
refId: 'annotation_query',
query,
},
mappings: {},
@@ -35,14 +37,12 @@ export const standardAnnotationSupport: AnnotationSupport = {
},
/**
* Convert the stored JSON model and environment to a standard data source query object.
* This query will be executed in the data source and the results converted into events.
* Returning an undefined result will quietly skip query execution
* Default will just return target from the annotation.
*/
prepareQuery: (anno: AnnotationQuery) => anno.target,
/**
* When the standard frame > event processing is insufficient, this allows explicit control of the mappings
* Provides default processing from dataFrame to annotation events.
*/
processEvents: (anno: AnnotationQuery, data: DataFrame[]) => {
return getAnnotationsFromData(data, anno.mappings);
@@ -50,7 +50,7 @@ export const standardAnnotationSupport: AnnotationSupport = {
};
/**
* Flatten all panel data into a single frame
* Flatten all frames into a single frame with mergeTransformer.
*/
export function singleFrameFromPanelData(): OperatorFunction<DataFrame[], DataFrame | undefined> {
@@ -226,3 +226,21 @@ export function getAnnotationsFromData(
})
);
}
// These opt outs are here only for quicker and easier migration to react based annotations editors and because
// annotation support API needs some work to support less "standard" editors like prometheus and here it is not
// polluting public API.
/**
* Opt out of using the default mapping functionality on frontend.
*/
export function shouldUseMappingUI(datasource: DataSourceApi): boolean {
return datasource.type !== 'prometheus';
}
/**
* Use legacy runner. Used only as an escape hatch for easier transition to React based annotation editor.
*/
export function shouldUseLegacyRunner(datasource: DataSourceApi): boolean {
return datasource.type === 'prometheus';
}
@@ -0,0 +1,51 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import { locationService, setBackendSrv } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore';
import { DashboardModel } from '../../state';
import { DashboardSettings } from './DashboardSettings';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
locationService: {
partial: jest.fn(),
},
}));
setBackendSrv({
get: jest.fn().mockResolvedValue({}),
} as any);
describe('DashboardSettings', () => {
it('pressing escape navigates away correctly', async () => {
jest.spyOn(locationService, 'partial');
const dashboard = new DashboardModel(
{
title: 'Foo',
},
{
folderId: 1,
}
);
const store = configureStore();
render(
<Provider store={store}>
<BrowserRouter>
<DashboardSettings editview="settings" dashboard={dashboard} />
</BrowserRouter>
</Provider>
);
expect(screen.getByText('Foo / Settings')).toBeInTheDocument();
await userEvent.keyboard('{Escape}');
expect(locationService.partial).toHaveBeenCalledWith({ editview: null });
});
});
@@ -49,7 +49,13 @@ const MakeEditable = (props: { onMakeEditable: () => any }) => (
export function DashboardSettings({ dashboard, editview }: Props) {
const ref = useRef<HTMLDivElement>(null);
const { overlayProps } = useOverlay({}, ref);
const { overlayProps } = useOverlay(
{
isOpen: true,
onClose,
},
ref
);
const { dialogProps } = useDialog(
{
'aria-label': 'Dashboard settings',
@@ -15,7 +15,7 @@ export class AnnotationsQueryRunner implements AnnotationQueryRunner {
return false;
}
return !Boolean(datasource.annotationQuery && !datasource.annotations);
return Boolean(!datasource.annotationQuery || datasource.annotations);
}
run({ annotation, datasource, dashboard, range }: AnnotationQueryRunnerOptions): Observable<AnnotationEvent[]> {
@@ -2,6 +2,7 @@ import { from, Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AnnotationEvent, DataSourceApi } from '@grafana/data';
import { shouldUseLegacyRunner } from 'app/features/annotations/standardAnnotationSupport';
import { AnnotationQueryRunner, AnnotationQueryRunnerOptions } from './types';
import { handleAnnotationQueryRunnerError } from './utils';
@@ -12,6 +13,10 @@ export class LegacyAnnotationQueryRunner implements AnnotationQueryRunner {
return false;
}
if (shouldUseLegacyRunner(datasource)) {
return true;
}
return Boolean(datasource.annotationQuery && !datasource.annotations);
}
@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import React, { FC, ChangeEvent, FormEvent } from 'react';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { config } from '@grafana/runtime';
import { HorizontalGroup, RadioButtonGroup, stylesFactory, useTheme, Checkbox, InlineSwitch } from '@grafana/ui';
import { HorizontalGroup, RadioButtonGroup, Checkbox, InlineSwitch, useStyles2 } from '@grafana/ui';
import { SortPicker } from 'app/core/components/Select/SortPicker';
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
import { SearchSrv } from 'app/core/services/search_srv';
@@ -40,8 +40,7 @@ export const ActionRow: FC<Props> = ({
hideLayout,
showPreviews,
}) => {
const theme = useTheme();
const styles = getStyles(theme);
const styles = useStyles2(getStyles);
const previewsEnabled = config.featureToggles.dashboardPreviews;
return (
@@ -78,21 +77,21 @@ export const ActionRow: FC<Props> = ({
ActionRow.displayName = 'ActionRow';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
export const getStyles = (theme: GrafanaTheme2) => {
return {
actionRow: css`
display: none;
@media only screen and (min-width: ${theme.breakpoints.md}) {
${theme.breakpoints.up('md')} {
display: flex;
justify-content: space-between;
align-items: center;
padding: ${theme.spacing.lg} 0;
padding-bottom: ${theme.spacing(2)};
width: 100%;
}
`,
rowContainer: css`
margin-right: ${theme.spacing.md};
margin-right: ${theme.spacing(1)};
`,
checkboxWrapper: css`
label {
@@ -100,4 +99,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
}
`,
};
});
};
@@ -140,11 +140,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
max-width: 1400px;
margin: 0 auto;
padding: ${theme.spacing(2)};
background: ${theme.colors.background.primary};
border: 1px solid ${theme.components.panel.borderColor};
margin-top: ${theme.spacing(4)};
height: 100%;
${theme.breakpoints.up('md')} {
padding: ${theme.spacing(4)};
padding: ${theme.spacing(3)};
}
`,
closeBtn: css`
@@ -160,7 +163,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
display: flex;
flex-direction: column;
height: 100%;
padding-bottom: ${theme.spacing(3)};
padding: ${theme.spacing(2, 0, 3, 0)};
`,
input: css`
box-sizing: border-box;
@@ -104,11 +104,11 @@ export const getStyles = (theme: GrafanaTheme2) => {
actionRow: css`
display: none;
@media only screen and (min-width: ${theme.v1.breakpoints.md}) {
${theme.breakpoints.up('md')} {
display: flex;
justify-content: space-between;
align-items: center;
padding: ${theme.v1.spacing.lg} 0;
padding-bottom: ${theme.spacing(2)};
width: 100%;
}
`,
@@ -130,7 +130,7 @@ export const FolderSection: FC<SectionHeaderProps> = ({
const renderResults = () => {
if (!results.value?.length) {
if (results.loading) {
return <Spinner />;
return <Spinner className={styles.spinner} />;
}
return (
@@ -255,5 +255,10 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
padding-top: 0px;
padding-bottom: 0px;
`,
spinner: css`
display: grid;
place-content: center;
padding-bottom: 1rem;
`,
};
});
@@ -99,7 +99,15 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props)
>
Run query
</Button>
<QueryEditorModeToggle mode={editorMode!} onChange={onEditorModeChange} />
<QueryEditorModeToggle
mode={editorMode!}
onChange={onEditorModeChange}
uiOptions={{
[QueryEditorMode.Explain]: true,
[QueryEditorMode.Code]: true,
[QueryEditorMode.Builder]: true,
}}
/>
</EditorHeader>
<Space v={0.5} />
<EditorRows>
@@ -0,0 +1,121 @@
import React from 'react';
import { AnnotationQuery } from '@grafana/data';
import { EditorRow, EditorField, EditorSwitch, Space } from '@grafana/experimental';
import { Input } from '@grafana/ui';
import { PromQueryEditorSelector } from '../querybuilder/components/PromQueryEditorSelector';
import { QueryEditorMode } from '../querybuilder/shared/types';
import { PromQuery } from '../types';
import { PromQueryEditorProps } from './types';
type Props = PromQueryEditorProps & {
annotation?: AnnotationQuery<PromQuery>;
onAnnotationChange?: (annotation: AnnotationQuery<PromQuery>) => void;
};
export function AnnotationQueryEditor(props: Props) {
// This is because of problematic typing. See AnnotationQueryEditorProps in grafana-data/annotations.ts.
const annotation = props.annotation!;
const onAnnotationChange = props.onAnnotationChange!;
return (
<>
<PromQueryEditorSelector
{...props}
query={{ expr: annotation.expr, refId: annotation.name, interval: annotation.step }}
onChange={(query) =>
onAnnotationChange({
...annotation,
expr: query.expr,
step: query.interval,
})
}
uiOptions={{
modes: {
[QueryEditorMode.Explain]: false,
[QueryEditorMode.Code]: true,
[QueryEditorMode.Builder]: true,
},
runQueryButton: false,
options: {
exemplars: false,
type: false,
format: false,
minStep: true,
legend: false,
resolution: false,
},
}}
/>
<Space v={0.5} />
<EditorRow>
<EditorField
label="Title"
tooltip={
'Use either the name or a pattern. For example, {{instance}} is replaced with label value for the label instance.'
}
>
<Input
type="text"
placeholder="alertname"
value={annotation.titleFormat}
onChange={(event) => {
onAnnotationChange({
...annotation,
titleFormat: event.currentTarget.value,
});
}}
/>
</EditorField>
<EditorField label="Tags">
<Input
type="text"
placeholder="label1,label2"
value={annotation.tagKeys}
onChange={(event) => {
onAnnotationChange({
...annotation,
tagKeys: event.currentTarget.value,
});
}}
/>
</EditorField>
<EditorField
label="Text"
tooltip={
'Use either the name or a pattern. For example, {{instance}} is replaced with label value for the label instance.'
}
>
<Input
type="text"
placeholder="instance"
value={annotation.textFormat}
onChange={(event) => {
onAnnotationChange({
...annotation,
textFormat: event.currentTarget.value,
});
}}
/>
</EditorField>
<EditorField
label="Series value as timestamp"
tooltip={
'The unit of timestamp is milliseconds. If the unit of the series value is seconds, multiply its range vector by 1000.'
}
>
<EditorSwitch
value={annotation.useValueForTime}
onChange={(event) => {
onAnnotationChange({
...annotation,
useValueForTime: event.currentTarget.value,
});
}}
/>
</EditorField>
</EditorRow>
</>
);
}
@@ -40,6 +40,7 @@ import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_sr
import { PromApplication, PromApiFeatures } from 'app/types/unified-alerting-dto';
import { addLabelToQuery } from './add_label_to_query';
import { AnnotationQueryEditor } from './components/AnnotationQueryEditor';
import PrometheusLanguageProvider from './language_provider';
import { expandRecordingRules } from './language_utils';
import { renderLegendFormat } from './legend';
@@ -119,6 +120,14 @@ export class PrometheusDatasource
this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters);
this.variables = new PrometheusVariableSupport(this, this.templateSrv, this.timeSrv);
this.exemplarsAvailable = true;
// This needs to be here and cannot be static because of how annotations typing affects casting of data source
// objects to DataSourceApi types.
// We don't use the default processing for prometheus.
// See standardAnnotationSupport.ts/[shouldUseMappingUI|shouldUseLegacyRunner]
this.annotations = {
QueryEditor: AnnotationQueryEditor,
};
}
init = async () => {
@@ -3,7 +3,6 @@ import React, { useState } from 'react';
import { DataSourceApi, SelectableValue, toOption } from '@grafana/data';
import { Select } from '@grafana/ui';
import { PrometheusDatasource } from '../../datasource';
import { promQueryModeller } from '../PromQueryModeller';
import { getOperationParamId } from '../shared/operationUtils';
import { QueryBuilderLabelFilter, QueryBuilderOperationParamEditorProps } from '../shared/types';
@@ -49,8 +48,8 @@ async function loadGroupByLabels(
): Promise<Array<SelectableValue<any>>> {
let labels: QueryBuilderLabelFilter[] = query.labels;
// This function is used by both Prometheus and Loki and this the only difference
if (datasource instanceof PrometheusDatasource) {
// This function is used by both Prometheus and Loki and this the only difference.
if (datasource.type === 'prometheus') {
labels = [{ label: '__name__', op: '=', value: query.metric }, ...query.labels];
}
@@ -78,6 +78,14 @@ function setup(queryOverrides: Partial<PromQuery> = {}) {
},
onRunQuery: jest.fn(),
onChange: jest.fn(),
uiOptions: {
exemplars: true,
type: true,
format: true,
minStep: true,
legend: true,
resolution: true,
},
};
const { container } = render(<PromQueryBuilderOptions {...props} />);
@@ -12,14 +12,24 @@ import { QueryOptionGroup } from '../shared/QueryOptionGroup';
import { getLegendModeLabel, PromQueryLegendEditor } from './PromQueryLegendEditor';
export interface UIOptions {
exemplars: boolean;
type: boolean;
format: boolean;
minStep: boolean;
legend: boolean;
resolution: boolean;
}
export interface Props {
query: PromQuery;
app?: CoreApp;
onChange: (update: PromQuery) => void;
onRunQuery: () => void;
uiOptions: UIOptions;
}
export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange, onRunQuery }) => {
export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange, onRunQuery, uiOptions }) => {
const onChangeFormat = (value: SelectableValue<string>) => {
onChange({ ...query, format: value.value });
onRunQuery();
@@ -50,42 +60,53 @@ export const PromQueryBuilderOptions = React.memo<Props>(({ query, app, onChange
return (
<EditorRow>
<QueryOptionGroup title="Options" collapsedInfo={getCollapsedInfo(query, formatOption.label!, queryTypeLabel)}>
<PromQueryLegendEditor
legendFormat={query.legendFormat}
onChange={(legendFormat) => onChange({ ...query, legendFormat })}
onRunQuery={onRunQuery}
/>
<EditorField
label="Min step"
tooltip={
<>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>$__interval</code> and <code>$__rate_interval</code> variables.
</>
}
>
<AutoSizeInput
type="text"
aria-label="Set lower limit for the step parameter"
placeholder={'auto'}
minWidth={10}
onCommitChange={onChangeStep}
defaultValue={query.interval}
<QueryOptionGroup
title="Options"
collapsedInfo={getCollapsedInfo(query, formatOption.label!, queryTypeLabel, uiOptions)}
>
{uiOptions.legend && (
<PromQueryLegendEditor
legendFormat={query.legendFormat}
onChange={(legendFormat) => onChange({ ...query, legendFormat })}
onRunQuery={onRunQuery}
/>
</EditorField>
<EditorField label="Format">
<Select value={formatOption} allowCustomValue onChange={onChangeFormat} options={FORMAT_OPTIONS} />
</EditorField>
<EditorField label="Type">
<RadioButtonGroup options={queryTypeOptions} value={queryTypeValue} onChange={onQueryTypeChange} />
</EditorField>
{shouldShowExemplarSwitch(query, app) && (
)}
{uiOptions.minStep && (
<EditorField
label="Min step"
tooltip={
<>
An additional lower limit for the step parameter of the Prometheus query and for the{' '}
<code>$__interval</code> and <code>$__rate_interval</code> variables.
</>
}
>
<AutoSizeInput
type="text"
aria-label="Set lower limit for the step parameter"
placeholder={'auto'}
minWidth={10}
onCommitChange={onChangeStep}
defaultValue={query.interval}
/>
</EditorField>
)}
{uiOptions.format && (
<EditorField label="Format">
<Select value={formatOption} allowCustomValue onChange={onChangeFormat} options={FORMAT_OPTIONS} />
</EditorField>
)}
{uiOptions.type && (
<EditorField label="Type">
<RadioButtonGroup options={queryTypeOptions} value={queryTypeValue} onChange={onQueryTypeChange} />
</EditorField>
)}
{uiOptions.exemplars && shouldShowExemplarSwitch(query, app) && (
<EditorField label="Exemplars">
<EditorSwitch value={query.exemplar} onChange={onExemplarChange} />
</EditorField>
)}
{query.intervalFactor && query.intervalFactor > 1 && (
{uiOptions.resolution && query.intervalFactor && query.intervalFactor > 1 && (
<EditorField label="Resolution">
<Select
aria-label="Select resolution"
@@ -113,19 +134,23 @@ function getQueryTypeValue(query: PromQuery) {
return query.range && query.instant ? 'both' : query.instant ? 'instant' : 'range';
}
function getCollapsedInfo(query: PromQuery, formatOption: string, queryType: string): string[] {
function getCollapsedInfo(query: PromQuery, formatOption: string, queryType: string, uiOptions: UIOptions): string[] {
const items: string[] = [];
items.push(`Legend: ${getLegendModeLabel(query.legendFormat)}`);
items.push(`Format: ${formatOption}`);
if (query.interval) {
if (uiOptions.legend) {
items.push(`Legend: ${getLegendModeLabel(query.legendFormat)}`);
}
if (uiOptions.format) {
items.push(`Format: ${formatOption}`);
}
if (uiOptions.minStep && query.interval) {
items.push(`Step ${query.interval}`);
}
if (uiOptions.type) {
items.push(`Type: ${queryType}`);
}
items.push(`Type: ${queryType}`);
if (query.exemplar) {
if (uiOptions.exemplars && query.exemplar) {
items.push(`Exemplars: true`);
}
@@ -10,17 +10,43 @@ import { PromQuery } from '../../types';
import { promQueryModeller } from '../PromQueryModeller';
import { buildVisualQueryFromString } from '../parsing';
import { FeedbackLink } from '../shared/FeedbackLink';
import { QueryEditorModeToggle } from '../shared/QueryEditorModeToggle';
import { QueryEditorModeToggle, UIOptions as ModeToggleUIOptions } from '../shared/QueryEditorModeToggle';
import { QueryHeaderSwitch } from '../shared/QueryHeaderSwitch';
import { QueryEditorMode } from '../shared/types';
import { changeEditorMode, getQueryWithDefaults } from '../state';
import { PromQueryBuilderContainer } from './PromQueryBuilderContainer';
import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
import { PromQueryBuilderOptions } from './PromQueryBuilderOptions';
import { PromQueryBuilderOptions, UIOptions as OptionsUIOptions } from './PromQueryBuilderOptions';
import { PromQueryCodeEditor } from './PromQueryCodeEditor';
export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props) => {
interface UIOptions {
modes: ModeToggleUIOptions;
runQueryButton: boolean;
options: OptionsUIOptions;
}
const defaultOptions: UIOptions = {
modes: {
[QueryEditorMode.Explain]: true,
[QueryEditorMode.Code]: true,
[QueryEditorMode.Builder]: true,
},
runQueryButton: true,
options: {
exemplars: true,
type: true,
format: true,
minStep: true,
legend: true,
resolution: true,
},
};
type Props = PromQueryEditorProps & { uiOptions?: UIOptions };
export const PromQueryEditorSelector = React.memo<Props>((props) => {
const uiOptions = props.uiOptions || defaultOptions;
const { onChange, onRunQuery, data, app } = props;
const [parseModalOpen, setParseModalOpen] = useState(false);
const [dataIsStale, setDataIsStale] = useState(false);
@@ -104,16 +130,18 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
<FeedbackLink feedbackUrl="https://github.com/grafana/grafana/discussions/47693" />
)}
<FlexItem grow={1} />
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run query
</Button>
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} />
{uiOptions.runQueryButton && (
<Button
variant={dataIsStale ? 'primary' : 'secondary'}
size="sm"
onClick={onRunQuery}
icon={data?.state === LoadingState.Loading ? 'fa fa-spinner' : undefined}
disabled={data?.state === LoadingState.Loading}
>
Run query
</Button>
)}
<QueryEditorModeToggle mode={editorMode} onChange={onEditorModeChange} uiOptions={uiOptions.modes} />
</EditorHeader>
<Space v={0.5} />
<EditorRows>
@@ -129,7 +157,13 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
)}
{editorMode === QueryEditorMode.Explain && <PromQueryBuilderExplained query={query.expr} />}
{editorMode !== QueryEditorMode.Explain && (
<PromQueryBuilderOptions query={query} app={props.app} onChange={onChange} onRunQuery={onRunQuery} />
<PromQueryBuilderOptions
query={query}
app={props.app}
onChange={onChange}
onRunQuery={onRunQuery}
uiOptions={uiOptions.options}
/>
)}
</EditorRows>
</>
@@ -5,9 +5,14 @@ import { RadioButtonGroup, Tag } from '@grafana/ui';
import { QueryEditorMode } from './types';
export type UIOptions = {
[key in QueryEditorMode]: boolean;
};
export interface Props {
mode: QueryEditorMode;
onChange: (mode: QueryEditorMode) => void;
uiOptions: UIOptions;
}
const editorModes = [
@@ -30,10 +35,11 @@ const editorModes = [
{ label: 'Code', value: QueryEditorMode.Code },
];
export function QueryEditorModeToggle({ mode, onChange }: Props) {
export function QueryEditorModeToggle({ mode, onChange, uiOptions }: Props) {
const modes = editorModes.filter((m) => uiOptions[m.value]);
return (
<div data-testid={'QueryEditorModeToggle'}>
<RadioButtonGroup options={editorModes} size="sm" value={mode} onChange={onChange} />
<RadioButtonGroup options={modes} size="sm" value={mode} onChange={onChange} />
</div>
);
}
+4
View File
@@ -152,6 +152,10 @@
"menuTabs": {
"height": 41
},
"textHighlight": {
"text": "#000000",
"background": "#F5B73D"
},
"horizontalDrawer": {
"defaultHeight": 400
}
+4
View File
@@ -152,6 +152,10 @@
"menuTabs": {
"height": 41
},
"textHighlight": {
"text": "#000000",
"background": "#FAD34A"
},
"horizontalDrawer": {
"defaultHeight": 400
}
+1 -1
View File
@@ -1,6 +1,6 @@
load('scripts/drone/vault.star', 'from_secret', 'github_token', 'pull_secret', 'drone_token', 'prerelease_bucket')
grabpl_version = 'v2.9.48'
grabpl_version = 'v2.9.49'
build_image = 'grafana/build-container:1.5.4'
publish_image = 'grafana/grafana-ci-deploy:1.3.1'
deploy_docker_image = 'us.gcr.io/kubernetes-dev/drone/plugins/deploy-image'