diff --git a/.drone.yml b/.drone.yml index 12334803eb9..40305f9faf0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -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 ... diff --git a/CHANGELOG.md b/CHANGELOG.md index bf240475032..9eb26796bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,36 @@ + + +# 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) + + # 8.5.3 diff --git a/conf/defaults.ini b/conf/defaults.ini index 2120725f2ca..1fa6c669021 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -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 diff --git a/docs/sources/alerting/about-alert-rules/_index.md b/docs/sources/alerting/about-alert-rules/_index.md new file mode 100644 index 00000000000..33db80be0bc --- /dev/null +++ b/docs/sources/alerting/about-alert-rules/_index.md @@ -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" >}}) diff --git a/docs/sources/alerting/about-alert-rules/alert-instances.md b/docs/sources/alerting/about-alert-rules/alert-instances.md new file mode 100644 index 00000000000..485c0170b0f --- /dev/null +++ b/docs/sources/alerting/about-alert-rules/alert-instances.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" >}} diff --git a/docs/sources/alerting/about-alert-rules/alert-rule-types.md b/docs/sources/alerting/about-alert-rules/alert-rule-types.md new file mode 100644 index 00000000000..18b9553f0f8 --- /dev/null +++ b/docs/sources/alerting/about-alert-rules/alert-rule-types.md @@ -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. diff --git a/docs/sources/alerting/about-alert-rules/organising-alerts.md b/docs/sources/alerting/about-alert-rules/organising-alerts.md new file mode 100644 index 00000000000..6a6a0cc50fe --- /dev/null +++ b/docs/sources/alerting/about-alert-rules/organising-alerts.md @@ -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. diff --git a/docs/sources/alerting/fundamentals/state-and-health.md b/docs/sources/alerting/fundamentals/state-and-health.md index 796f8e9c2b7..a9e4c92f4b8 100644 --- a/docs/sources/alerting/fundamentals/state-and-health.md +++ b/docs/sources/alerting/fundamentals/state-and-health.md @@ -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. diff --git a/docs/sources/alerting/silences/_index.md b/docs/sources/alerting/silences/_index.md index 9f9f8e43946..1374ade3259 100644 --- a/docs/sources/alerting/silences/_index.md +++ b/docs/sources/alerting/silences/_index.md @@ -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" >}}) diff --git a/docs/sources/alerting/silences/create-silence.md b/docs/sources/alerting/silences/create-silence.md index c12bfc37ad8..10e38417ddd 100644 --- a/docs/sources/alerting/silences/create-silence.md +++ b/docs/sources/alerting/silences/create-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**. diff --git a/docs/sources/alerting/silences/label-matching-alert-suppression.md b/docs/sources/alerting/silences/label-matching-alert-suppression.md deleted file mode 100644 index 5398941947e..00000000000 --- a/docs/sources/alerting/silences/label-matching-alert-suppression.md +++ /dev/null @@ -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. diff --git a/docs/sources/release-notes/_index.md b/docs/sources/release-notes/_index.md index b773f705a1c..d9890a61da9 100644 --- a/docs/sources/release-notes/_index.md +++ b/docs/sources/release-notes/_index.md @@ -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" >}}) diff --git a/docs/sources/release-notes/release-notes-9-0-0-beta1.md b/docs/sources/release-notes/release-notes-9-0-0-beta1.md new file mode 100644 index 00000000000..ccf304ee60a --- /dev/null +++ b/docs/sources/release-notes/release-notes-9-0-0-beta1.md @@ -0,0 +1,36 @@ ++++ +title = "Release notes for Grafana 9.0.0-beta1" +hide_menu = true ++++ + + + +# 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) diff --git a/docs/sources/whatsnew/_index.md b/docs/sources/whatsnew/_index.md index cc674ab767b..f315e852118 100644 --- a/docs/sources/whatsnew/_index.md +++ b/docs/sources/whatsnew/_index.md @@ -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" >}}) diff --git a/docs/sources/whatsnew/whats-new-in-v9-0.md b/docs/sources/whatsnew/whats-new-in-v9-0.md new file mode 100644 index 00000000000..3a565f51143 --- /dev/null +++ b/docs/sources/whatsnew/whats-new-in-v9-0.md @@ -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. + +## 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. + + + +### 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 `//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 //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. diff --git a/packages/grafana-data/src/types/annotations.ts b/packages/grafana-data/src/types/annotations.ts index e701b6b3841..f5c3a72c51b 100644 --- a/packages/grafana-data/src/types/annotations.ts +++ b/packages/grafana-data/src/types/annotations.ts @@ -77,6 +77,12 @@ export interface AnnotationEventFieldMapping { } export type AnnotationEventMappings = Partial>; +type AnnotationQueryEditorProps = QueryEditorProps & { + // 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; + onAnnotationChange?: (annotation: AnnotationQuery) => void; +}; /** * Since Grafana 7.2 @@ -86,7 +92,7 @@ export type AnnotationEventMappings = Partial> { /** * 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>; + QueryEditor?: ComponentType>; } diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx index 8e2397df452..e39ce8157ce 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.tsx @@ -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((props) => { DatePicker.displayName = 'DatePicker'; -const Body = memo(({ value, onChange }) => { +const Body = memo(({ value, minDate, onChange }) => { const styles = useStyles2(getBodyStyles); return ( @@ -45,6 +46,7 @@ const Body = memo(({ value, onChange }) => { className={styles.body} tileClassName={styles.title} value={value || new Date()} + minDate={minDate} nextLabel={} prevLabel={} 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}; + } `, }; }; diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx index ac924d7be24..136cfedce82 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.tsx @@ -12,6 +12,7 @@ export const formatDate = (date: Date | string) => dateTime(date).format('L'); /** @public */ export interface DatePickerWithInputProps extends Omit { 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 { onChange(ev); if (closeOnSelect) { diff --git a/packages/grafana-ui/src/components/Drawer/Drawer.tsx b/packages/grafana-ui/src/components/Drawer/Drawer.tsx index 7bf729d0334..55b61dc0cd5 100644 --- a/packages/grafana-ui/src/components/Drawer/Drawer.tsx +++ b/packages/grafana-ui/src/components/Drawer/Drawer.tsx @@ -55,6 +55,8 @@ export function Drawer({ const { overlayProps } = useOverlay( { isDismissable: true, + isOpen, + onClose, }, overlayRef ); diff --git a/packages/grafana-ui/src/components/Modal/Modal.test.tsx b/packages/grafana-ui/src/components/Modal/Modal.test.tsx index 08d558d395b..7f4a79d8480 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.test.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.test.tsx @@ -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( + +
Content
+
+ ); + + expect(screen.getByRole('dialog')).toBeInTheDocument(); + expect(screen.getByLabelText('Some Title')).toBeInTheDocument(); + expect(screen.getByTestId('modal-content')).toBeInTheDocument(); + + await userEvent.keyboard('{Escape}'); + + expect(onDismiss).toHaveBeenCalled(); + }); }); diff --git a/packages/grafana-ui/src/components/Modal/Modal.tsx b/packages/grafana-ui/src/components/Modal/Modal.tsx index 8b2e6adc175..a3f6aeba5df 100644 --- a/packages/grafana-ui/src/components/Modal/Modal.tsx +++ b/packages/grafana-ui/src/components/Modal/Modal.tsx @@ -53,7 +53,7 @@ export function Modal(props: PropsWithChildren) { // 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 ); diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 012380ef0bf..86243b18d3d 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -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)) diff --git a/pkg/infra/usagestats/service/api_test.go b/pkg/infra/usagestats/service/api_test.go index ea67c868442..b48dfe0ab0f 100644 --- a/pkg/infra/usagestats/service/api_test.go +++ b/pkg/infra/usagestats/service/api_test.go @@ -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) } } diff --git a/pkg/login/social/azuread_oauth.go b/pkg/login/social/azuread_oauth.go index ff1d51a7a4e..774e7bdfa55 100644 --- a/pkg/login/social/azuread_oauth.go +++ b/pkg/login/social/azuread_oauth.go @@ -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") } diff --git a/pkg/middleware/request_metrics.go b/pkg/middleware/request_metrics.go index 6f9b2618993..fd17e256a1f 100644 --- a/pkg/middleware/request_metrics.go +++ b/pkg/middleware/request_metrics.go @@ -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" diff --git a/pkg/middleware/request_tracing.go b/pkg/middleware/request_tracing.go index f877a1f37ef..e477749ce9a 100644 --- a/pkg/middleware/request_tracing.go +++ b/pkg/middleware/request_tracing.go @@ -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, diff --git a/pkg/services/accesscontrol/middleware_test.go b/pkg/services/accesscontrol/middleware_test.go index 887390f34b8..e9d64c2cb91 100644 --- a/pkg/services/accesscontrol/middleware_test.go +++ b/pkg/services/accesscontrol/middleware_test.go @@ -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) } } diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go index c1786f435b5..38d25552d21 100644 --- a/pkg/services/accesscontrol/resourcepermissions/api_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -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) } } diff --git a/pkg/services/alerting/service.go b/pkg/services/alerting/service.go index 34e683561fc..7c30b0a635f 100644 --- a/pkg/services/alerting/service.go +++ b/pkg/services/alerting/service.go @@ -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) } diff --git a/pkg/services/alerting/service_test.go b/pkg/services/alerting/service_test.go index edd58a5da30..0d8e19245a3 100644 --- a/pkg/services/alerting/service_test.go +++ b/pkg/services/alerting/service_test.go @@ -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) { diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 4e09458bd5b..d01a4158dbd 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -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) diff --git a/pkg/services/ngalert/metrics/ngalert.go b/pkg/services/ngalert/metrics/ngalert.go index 6bbf6a601b9..916ac41a21f 100644 --- a/pkg/services/ngalert/metrics/ngalert.go +++ b/pkg/services/ngalert/metrics/ngalert.go @@ -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 diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index c6db4b27b23..e13968e2e5c 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -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 diff --git a/pkg/web/context.go b/pkg/web/context.go index 8cea4cec9cb..06095e70aa6 100644 --- a/pkg/web/context.go +++ b/pkg/web/context.go @@ -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 diff --git a/pkg/web/inject.go b/pkg/web/inject.go deleted file mode 100644 index fb5c27efe87..00000000000 --- a/pkg/web/inject.go +++ /dev/null @@ -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 -} diff --git a/pkg/web/macaron.go b/pkg/web/macaron.go index e6d4a90a7d8..edec73da060 100644 --- a/pkg/web/macaron.go +++ b/pkg/web/macaron.go @@ -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 } diff --git a/pkg/web/router.go b/pkg/web/router.go index feed5e27556..3f32d428c29 100644 --- a/pkg/web/router.go +++ b/pkg/web/router.go @@ -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() } } diff --git a/pkg/web/webtest/webtest.go b/pkg/web/webtest/webtest.go index 8645914ebaf..849a7128d91 100644 --- a/pkg/web/webtest/webtest.go +++ b/pkg/web/webtest/webtest.go @@ -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) } } diff --git a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx index 3cf223b504c..3d4b60e2fad 100644 --- a/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx +++ b/public/app/features/annotations/components/StandardAnnotationQueryEditor.tsx @@ -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 { 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 { + 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
Annotations are not supported. This datasource needs to export a QueryEditor
; @@ -177,8 +188,10 @@ export default class StandardAnnotationQueryEditor extends PureComponent - {datasource.type !== 'datasource' && ( + {shouldUseMappingUI(datasource) && ( <> {this.renderStatus()} diff --git a/public/app/features/annotations/standardAnnotationSupport.ts b/public/app/features/annotations/standardAnnotationSupport.ts index 079a506a94b..710ded708ed 100644 --- a/public/app/features/annotations/standardAnnotationSupport.ts +++ b/public/app/features/annotations/standardAnnotationSupport.ts @@ -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 { @@ -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'; +} diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx new file mode 100644 index 00000000000..a694be7c3f6 --- /dev/null +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.test.tsx @@ -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( + + + + + + ); + + expect(screen.getByText('Foo / Settings')).toBeInTheDocument(); + + await userEvent.keyboard('{Escape}'); + + expect(locationService.partial).toHaveBeenCalledWith({ editview: null }); + }); +}); diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx index d8494fe6c59..363eae8acc6 100644 --- a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -49,7 +49,13 @@ const MakeEditable = (props: { onMakeEditable: () => any }) => ( export function DashboardSettings({ dashboard, editview }: Props) { const ref = useRef(null); - const { overlayProps } = useOverlay({}, ref); + const { overlayProps } = useOverlay( + { + isOpen: true, + onClose, + }, + ref + ); const { dialogProps } = useDialog( { 'aria-label': 'Dashboard settings', diff --git a/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts index 4eadff44e59..05b5bf7a3bc 100644 --- a/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts +++ b/public/app/features/query/state/DashboardQueryRunner/AnnotationsQueryRunner.ts @@ -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 { diff --git a/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts b/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts index 5d94911b9ab..f383897f5d9 100644 --- a/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts +++ b/public/app/features/query/state/DashboardQueryRunner/LegacyAnnotationQueryRunner.ts @@ -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); } diff --git a/public/app/features/search/components/ActionRow.tsx b/public/app/features/search/components/ActionRow.tsx index ef9c5d13c65..12b5c0a67cd 100644 --- a/public/app/features/search/components/ActionRow.tsx +++ b/public/app/features/search/components/ActionRow.tsx @@ -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 = ({ 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 = ({ 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) => { } `, }; -}); +}; diff --git a/public/app/features/search/components/DashboardSearch.tsx b/public/app/features/search/components/DashboardSearch.tsx index 33f7ff2f668..065ce775f38 100644 --- a/public/app/features/search/components/DashboardSearch.tsx +++ b/public/app/features/search/components/DashboardSearch.tsx @@ -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; diff --git a/public/app/features/search/page/components/ActionRow.tsx b/public/app/features/search/page/components/ActionRow.tsx index c6a86240f05..7c309c11cff 100644 --- a/public/app/features/search/page/components/ActionRow.tsx +++ b/public/app/features/search/page/components/ActionRow.tsx @@ -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%; } `, diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx index 39b508b17ab..9b39bf419f9 100644 --- a/public/app/features/search/page/components/FolderSection.tsx +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -130,7 +130,7 @@ export const FolderSection: FC = ({ const renderResults = () => { if (!results.value?.length) { if (results.loading) { - return ; + return ; } 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; + `, }; }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx index 4f43cf5e0d3..fbb5f1d4f82 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryEditorSelector.tsx @@ -99,7 +99,15 @@ export const LokiQueryEditorSelector = React.memo((props) > Run query - + diff --git a/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx new file mode 100644 index 00000000000..cd7bfc987d4 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/AnnotationQueryEditor.tsx @@ -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; + onAnnotationChange?: (annotation: AnnotationQuery) => 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 ( + <> + + 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, + }, + }} + /> + + + + { + onAnnotationChange({ + ...annotation, + titleFormat: event.currentTarget.value, + }); + }} + /> + + + { + onAnnotationChange({ + ...annotation, + tagKeys: event.currentTarget.value, + }); + }} + /> + + + { + onAnnotationChange({ + ...annotation, + textFormat: event.currentTarget.value, + }); + }} + /> + + + { + onAnnotationChange({ + ...annotation, + useValueForTime: event.currentTarget.value, + }); + }} + /> + + + + ); +} diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index 420d203c817..209112270d2 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -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 () => { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx index ab95e2798e4..941bb1e11f3 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/LabelParamEditor.tsx @@ -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>> { 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]; } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx index a4df0c00284..9fc5fe05d38 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.test.tsx @@ -78,6 +78,14 @@ function setup(queryOverrides: Partial = {}) { }, onRunQuery: jest.fn(), onChange: jest.fn(), + uiOptions: { + exemplars: true, + type: true, + format: true, + minStep: true, + legend: true, + resolution: true, + }, }; const { container } = render(); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx index 67ef8b17e7b..17b3b296eea 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderOptions.tsx @@ -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(({ query, app, onChange, onRunQuery }) => { +export const PromQueryBuilderOptions = React.memo(({ query, app, onChange, onRunQuery, uiOptions }) => { const onChangeFormat = (value: SelectableValue) => { onChange({ ...query, format: value.value }); onRunQuery(); @@ -50,42 +60,53 @@ export const PromQueryBuilderOptions = React.memo(({ query, app, onChange return ( - - onChange({ ...query, legendFormat })} - onRunQuery={onRunQuery} - /> - - An additional lower limit for the step parameter of the Prometheus query and for the{' '} - $__interval and $__rate_interval variables. - - } - > - + {uiOptions.legend && ( + onChange({ ...query, legendFormat })} + onRunQuery={onRunQuery} /> - - - + + )} + {uiOptions.type && ( + + + + )} + {uiOptions.exemplars && shouldShowExemplarSwitch(query, app) && ( )} - {query.intervalFactor && query.intervalFactor > 1 && ( + {uiOptions.resolution && query.intervalFactor && query.intervalFactor > 1 && (