Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8766bbf2 | ||
|
|
f6ffc86aa1 | ||
|
|
deea6305c2 | ||
|
|
2f9f74b221 | ||
|
|
62ff483d13 | ||
|
|
4806851ae3 | ||
|
|
c44af3ca20 | ||
|
|
4a29cf13f0 | ||
|
|
883977bc3a | ||
|
|
f42d0b9beb | ||
|
|
667f884db1 | ||
|
|
0ac23b0446 | ||
|
|
acdf2ec806 | ||
|
|
2c033a6540 | ||
|
|
9cddb216d3 | ||
|
|
c051c5d423 | ||
|
|
e38d8cd8af | ||
|
|
9464ffa86b | ||
|
|
60a25b4022 | ||
|
|
6beca4317e | ||
|
|
c9a619e364 | ||
|
|
9b628f4742 | ||
|
|
2bd51b506e | ||
|
|
b88f3a909e | ||
|
|
4f2ffc434f | ||
|
|
8f7c031d1d | ||
|
|
30d5482a48 | ||
|
|
5af06db660 | ||
|
|
8343e79f3e | ||
|
|
ac8f1b5b6b | ||
|
|
5b37d14b7f | ||
|
|
3ce1ca5ac1 | ||
|
|
cdcbf64b67 | ||
|
|
f35f826525 | ||
|
|
30c7df22ee | ||
|
|
0d469694c6 | ||
|
|
9ddcb16298 | ||
|
|
2c1a590752 | ||
|
|
a61b1e4285 | ||
|
|
7c2b1cc9d0 | ||
|
|
872727e745 | ||
|
|
2585ad1ddc | ||
|
|
7c9c60b853 | ||
|
|
e016d4eb1b | ||
|
|
9727ba084c | ||
|
|
e80194610c | ||
|
|
a2f97a68c6 | ||
|
|
8aa31fd2d6 | ||
|
|
eccd87a564 | ||
|
|
f5abd72785 | ||
|
|
db1b31d542 | ||
|
|
7fd39889fa | ||
|
|
64fc2ecb0e | ||
|
|
42668a5e89 | ||
|
|
c335538765 | ||
|
|
3d453a6104 | ||
|
|
918cb580e8 | ||
|
|
6863f49bd0 | ||
|
|
2c502f4b36 | ||
|
|
05ccdac63a | ||
|
|
5ab2ebade3 | ||
|
|
bce40bfcf1 | ||
|
|
5bc1a0ad06 | ||
|
|
a23bacc3e2 | ||
|
|
67a3609cb8 | ||
|
|
f63ec0f43d | ||
|
|
42bffe2310 | ||
|
|
d3756d38ee | ||
|
|
db94117d84 | ||
|
|
2b967ad8ec |
@@ -1,11 +0,0 @@
|
||||
version: 2.1
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: alpine:3.7
|
||||
steps:
|
||||
- run:
|
||||
name: The First Step
|
||||
command: |
|
||||
echo 'Fake step!'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
load('scripts/drone/pipelines/pr.star', 'pr_pipelines')
|
||||
load('scripts/drone/pipelines/main.star', 'main_pipelines')
|
||||
load('scripts/drone/pipelines/release.star', 'release_pipelines', 'test_release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline')
|
||||
load('scripts/drone/pipelines/release.star', 'release_pipelines', 'publish_image_pipelines', 'publish_artifacts_pipelines', 'publish_npm_pipelines', 'publish_packages_pipeline')
|
||||
load('scripts/drone/version.star', 'version_branch_pipelines')
|
||||
load('scripts/drone/pipelines/cron.star', 'cronjobs')
|
||||
load('scripts/drone/vault.star', 'secrets')
|
||||
@@ -17,4 +17,4 @@ def main(ctx):
|
||||
publish_image_pipelines('public') + publish_image_pipelines('security') + \
|
||||
publish_artifacts_pipelines('security') + publish_artifacts_pipelines('public') + \
|
||||
publish_npm_pipelines('public') + publish_packages_pipeline() + \
|
||||
test_release_pipelines() + version_branch_pipelines() + cronjobs(edition=edition) + secrets()
|
||||
version_branch_pipelines() + cronjobs(edition=edition) + secrets()
|
||||
|
||||
1523
.drone.yml
1523
.drone.yml
File diff suppressed because it is too large
Load Diff
@@ -6,5 +6,6 @@ devenv
|
||||
data
|
||||
dist
|
||||
e2e/tmp
|
||||
scripts/grafana-server/tmp
|
||||
public/lib/monaco
|
||||
deployment_tools_config.json
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -13,6 +13,7 @@ public/dist/tsconfig.tsbuildinfo
|
||||
/emails/dist
|
||||
/reports
|
||||
/e2e/tmp
|
||||
/scripts/grafana-server/tmp
|
||||
vendor/
|
||||
/docs/menu.yaml
|
||||
/requests
|
||||
@@ -137,6 +138,9 @@ compilation-stats.json
|
||||
!/e2e/**/screenshots/expected/*
|
||||
/e2e/**/videos/*
|
||||
|
||||
# grafana server
|
||||
/scripts/grafana-server/server.log
|
||||
|
||||
# a11y tests
|
||||
/pa11y-ci-results.json
|
||||
/pa11y-ci-report
|
||||
|
||||
@@ -7,6 +7,7 @@ public/vendor/
|
||||
vendor/
|
||||
/data/
|
||||
e2e/tmp
|
||||
scripts/grafana-server/tmp
|
||||
public/build/
|
||||
public/sass/*.generated.scss
|
||||
devenv/
|
||||
|
||||
91
CHANGELOG.md
91
CHANGELOG.md
@@ -1,3 +1,50 @@
|
||||
<!-- 8.3.5 START -->
|
||||
|
||||
# 8.3.5 (2022-02-08)
|
||||
|
||||
- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
|
||||
<!-- 8.3.5 END -->
|
||||
<!-- 8.3.4 START -->
|
||||
|
||||
# 8.3.4 (2022-01-17)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Alerting:** Allow configuration of non-ready alertmanagers. [#43063](https://github.com/grafana/grafana/pull/43063), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Allow customization of Google chat message. [#43568](https://github.com/grafana/grafana/pull/43568), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Allow customization of Google chat message (#43568). [#43723](https://github.com/grafana/grafana/pull/43723), [@alexweav](https://github.com/alexweav)
|
||||
- **AppPlugins:** Support app plugins with only default nav. [#43016](https://github.com/grafana/grafana/pull/43016), [@torkelo](https://github.com/torkelo)
|
||||
- **InfluxDB:** InfluxQL: query editor: skip fields in metadata queries. [#42543](https://github.com/grafana/grafana/pull/42543), [@gabor](https://github.com/gabor)
|
||||
- **Postgres/MySQL/MSSQL:** Cancel in-flight SQL query if user cancels query in grafana. [#43890](https://github.com/grafana/grafana/pull/43890), [@mdvictor](https://github.com/mdvictor)
|
||||
- **Prometheus:** Forward oauth tokens after prometheus datasource migration. [#43686](https://github.com/grafana/grafana/pull/43686), [@MasslessParticle](https://github.com/MasslessParticle)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Azure Monitor:** Bug fix for variable interpolations in metrics dropdowns. [#43251](https://github.com/grafana/grafana/pull/43251), [@sarahzinger](https://github.com/sarahzinger)
|
||||
- **Azure Monitor:** Improved error messages for variable queries. [#43213](https://github.com/grafana/grafana/pull/43213), [@sunker](https://github.com/sunker)
|
||||
- **CloudMonitoring:** Fixes broken variable queries that use group bys. [#43914](https://github.com/grafana/grafana/pull/43914), [@sunker](https://github.com/sunker)
|
||||
- **Configuration:** You can now see your expired API keys if you have no active ones. [#42452](https://github.com/grafana/grafana/pull/42452), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Elasticsearch:** Fix handling multiple datalinks for a single field. [#44029](https://github.com/grafana/grafana/pull/44029), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Export:** Fix error being thrown when exporting dashboards using query variables that reference the default datasource. [#44034](https://github.com/grafana/grafana/pull/44034), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **ImportDashboard:** Fixes issue with importing dashboard and name ending up in uid. [#43451](https://github.com/grafana/grafana/pull/43451), [@torkelo](https://github.com/torkelo)
|
||||
- **Login:** Page no longer overflows on mobile. [#43739](https://github.com/grafana/grafana/pull/43739), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Plugins:** Set backend metadata property for core plugins. [#43349](https://github.com/grafana/grafana/pull/43349), [@marefr](https://github.com/marefr)
|
||||
- **Prometheus:** Fill missing steps with null values. [#43622](https://github.com/grafana/grafana/pull/43622), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Fix interpolation of $\_\_rate_interval variable. [#44035](https://github.com/grafana/grafana/pull/44035), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Interpolate variables with curly brackets syntax. [#42927](https://github.com/grafana/grafana/pull/42927), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Respect the http-method data source setting. [#42753](https://github.com/grafana/grafana/pull/42753), [@gabor](https://github.com/gabor)
|
||||
- **Table:** Fixes issue with field config applied to wrong fields when hiding columns. [#43376](https://github.com/grafana/grafana/pull/43376), [@torkelo](https://github.com/torkelo)
|
||||
- **Toolkit:** Fix bug with rootUrls not being properly parsed when signing a private plugin. [#43014](https://github.com/grafana/grafana/pull/43014), [@dessen-xu](https://github.com/dessen-xu)
|
||||
- **Variables:** Fix so data source variables are added to adhoc configuration. [#43881](https://github.com/grafana/grafana/pull/43881), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
|
||||
### Plugin development fixes & changes
|
||||
|
||||
- **Toolkit:** Revert build config so tslib is bundled with plugins to prevent plugins from crashing. [#43556](https://github.com/grafana/grafana/pull/43556), [@mckn](https://github.com/mckn)
|
||||
|
||||
<!-- 8.3.4 END -->
|
||||
<!-- 8.3.3 START -->
|
||||
|
||||
# 8.3.3 (2021-12-10)
|
||||
@@ -29,7 +76,7 @@
|
||||
|
||||
# 8.3.2 (2021-12-10)
|
||||
|
||||
- **Security**: Fixes CVE-2021-43813 and CVE-2021-PENDING. For more information, see our [blog](https://grafana.com/blog/2021/12/10/grafana-8.3.2-and-7.5.12-released-with-moderate-severity-security-fix/
|
||||
- **Security**: Fixes CVE-2021-43813 and CVE-2021-43815. For more information, see our [blog](https://grafana.com/blog/2021/12/10/grafana-8.3.2-and-7.5.12-released-with-moderate-severity-security-fix/
|
||||
|
||||
<!-- 8.3.2 END -->
|
||||
|
||||
@@ -213,6 +260,14 @@ The access mode "browser" is deprecated in the following data sources and will b
|
||||
|
||||
- **grafana/ui:** Enable slider marks display. [#41275](https://github.com/grafana/grafana/pull/41275), [@dprokop](https://github.com/dprokop)
|
||||
|
||||
<!-- 8.2.7 START -->
|
||||
|
||||
# 8.2.7 (2021-12-07)
|
||||
|
||||
- **Security**: Fixes CVE-2021-43798. For more information, see our [blog](https://grafana.com/blog/2021/12/07/grafana-8.3.1-8.2.7-8.1.8-and-8.0.7-released-with-high-severity-security-fix/)
|
||||
|
||||
<!-- 8.2.7 END -->
|
||||
|
||||
<!-- 8.2.6 START -->
|
||||
|
||||
# 8.2.6 (2021-12-02)
|
||||
@@ -492,6 +547,14 @@ Panel queries and/or annotation queries that used more than one statistic will b
|
||||
|
||||
<!-- 8.2.0-beta1 END -->
|
||||
|
||||
<!-- 8.1.8 START -->
|
||||
|
||||
# 8.1.8 (2021-12-07)
|
||||
|
||||
- **Security**: Fixes CVE-2021-43798. For more information, see our [blog](https://grafana.com/blog/2021/12/07/grafana-8.3.1-8.2.7-8.1.8-and-8.0.7-released-with-high-severity-security-fix/)
|
||||
|
||||
<!-- 8.1.8 END -->
|
||||
|
||||
<!-- 8.1.7 START -->
|
||||
|
||||
# 8.1.7 (2021-10-06)
|
||||
@@ -766,6 +829,14 @@ Issue [#33879](https://github.com/grafana/grafana/issues/33879)
|
||||
|
||||
<!-- 8.1.0-beta1 END -->
|
||||
|
||||
<!-- 8.0.7 START -->
|
||||
|
||||
# 8.0.7 (2021-12-07)
|
||||
|
||||
- **Security**: Fixes CVE-2021-43798. For more information, see our [blog](https://grafana.com/blog/2021/12/07/grafana-8.3.1-8.2.7-8.1.8-and-8.0.7-released-with-high-severity-security-fix/)
|
||||
|
||||
<!-- 8.0.7 END -->
|
||||
|
||||
<!-- 8.0.6 START -->
|
||||
|
||||
# 8.0.6 (2021-07-14)
|
||||
@@ -1197,6 +1268,24 @@ Issue [#33352](https://github.com/grafana/grafana/issues/33352)
|
||||
- **AGPL License:** Update license from Apache 2.0 to the GNU Affero General Public License (AGPL). [#33184](https://github.com/grafana/grafana/pull/33184)
|
||||
|
||||
<!-- 8.0.0-beta1 END -->
|
||||
<!-- 7.5.15 START -->
|
||||
|
||||
# 7.5.15 (2022-02-08)
|
||||
|
||||
- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
|
||||
<!-- 7.5.15 END -->
|
||||
<!-- 7.5.13 START -->
|
||||
|
||||
# 7.5.13 (2022-01-18)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **[v7.5.x] Alerting:** Fix NoDataFound for alert rules using AND operator (#41305). [#44066](https://github.com/grafana/grafana/pull/44066), [@armandgrillet](https://github.com/armandgrillet)
|
||||
|
||||
<!-- 7.5.13 END -->
|
||||
|
||||
<!-- 7.5.11 START -->
|
||||
|
||||
|
||||
@@ -288,8 +288,8 @@ content_security_policy_template = """script-src 'self' 'unsafe-eval' 'unsafe-in
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
external_enabled = true
|
||||
external_snapshot_url = https://snapshots-origin.raintank.io
|
||||
external_snapshot_name = Publish to snapshot.raintank.io
|
||||
external_snapshot_url = https://snapshots.raintank.io
|
||||
external_snapshot_name = Publish to snapshots.raintank.io
|
||||
|
||||
# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
|
||||
# creating and deleting snapshots.
|
||||
|
||||
@@ -282,8 +282,8 @@
|
||||
[snapshots]
|
||||
# snapshot sharing options
|
||||
;external_enabled = true
|
||||
;external_snapshot_url = https://snapshots-origin.raintank.io
|
||||
;external_snapshot_name = Publish to snapshot.raintank.io
|
||||
;external_snapshot_url = https://snapshots.raintank.io
|
||||
;external_snapshot_name = Publish to snapshots.raintank.io
|
||||
|
||||
# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
|
||||
# creating and deleting snapshots.
|
||||
|
||||
@@ -16,8 +16,8 @@ BASE_URL=http://172.0.10.2:3333 yarn e2e
|
||||
|
||||
The above commands use some utils scripts under [_\<repo-root>/e2e_](../../e2e) that can also be used for more control.
|
||||
|
||||
- `./e2e/start-server` This creates a fresh new grafana server working dir, setup's config and starts the server. It will also kill any previously started server that is still running using pid file at _\<repo-root>/e2e/tmp/pid_.
|
||||
- `./e2e/wait-for-grafana` waits for `$HOST` and `$PORT` to be available. Per default localhost and 3001.
|
||||
- `./scripts/grafana-server/start-server` This creates a fresh new grafana server working dir, setup's config and starts the server. It will also kill any previously started server that is still running using pid file at _\<repo-root>/scripts/grafana-server/tmp/pid_.
|
||||
- `./scripts/grafana-server/wait-for-grafana` waits for `$HOST` and `$PORT` to be available. Per default localhost and 3001.
|
||||
- `./e2e/run-suite <debug|dev|noarg>` Starts cypress in different modes.
|
||||
|
||||
## Test suites
|
||||
|
||||
@@ -565,11 +565,11 @@ Set to `false` to disable external snapshot publish endpoint (default `true`).
|
||||
|
||||
### external_snapshot_url
|
||||
|
||||
Set root URL to a Grafana instance where you want to publish external snapshots (defaults to https://snapshots-origin.raintank.io).
|
||||
Set root URL to a Grafana instance where you want to publish external snapshots (defaults to https://snapshots.raintank.io).
|
||||
|
||||
### external_snapshot_name
|
||||
|
||||
Set name for external snapshot button. Defaults to `Publish to snapshot.raintank.io`.
|
||||
Set name for external snapshot button. Defaults to `Publish to snapshots.raintank.io`.
|
||||
|
||||
### public_mode
|
||||
|
||||
@@ -869,8 +869,6 @@ Email server settings.
|
||||
|
||||
Enable this to allow Grafana to send email. Default is `false`.
|
||||
|
||||
If the password contains `#` or `;`, then you have to wrap it with triple quotes. Example: """#password;"""
|
||||
|
||||
### host
|
||||
|
||||
Default is `localhost:25`.
|
||||
@@ -881,7 +879,7 @@ In case of SMTP auth, default is `empty`.
|
||||
|
||||
### password
|
||||
|
||||
In case of SMTP auth, default is `empty`.
|
||||
In case of SMTP auth, default is `empty`. If the password contains `#` or `;`, then you have to wrap it with triple quotes. Example: """#password;"""
|
||||
|
||||
### cert_file
|
||||
|
||||
|
||||
@@ -12,4 +12,6 @@ Grafana’s database contains secrets, which are used to query data sources, sen
|
||||
|
||||
Grafana encrypts these secrets before they are written to the database, by using a symmetric-key encryption algorithm called Advanced Encryption Standard (AES), and using a [secret key]({{< relref "../administration/configuration/#secret_key" >}}) that you can change when you configure a new Grafana instance.
|
||||
|
||||
You can also use envelope encryption, which complements a KMS integration by adding a layer of indirection to the encryption process.
|
||||
You can choose to use [envelope encryption]({{< relref "./envelope-encryption.md" >}}), which complements a [KMS integration]({{< relref "../enterprise/kms-integration/_index.md" >}}) in Grafana Enterprise by adding a layer of indirection to the encryption process.
|
||||
|
||||
In Grafana Enterprise, you can also choose to [encrypt secrets in AES-GCM mode]({{< relref "../administration/database-encryption-enterprise.md" >}}) instead of AES-CFB.
|
||||
|
||||
21
docs/sources/administration/envelope-encryption.md
Normal file
21
docs/sources/administration/envelope-encryption.md
Normal file
@@ -0,0 +1,21 @@
|
||||
+++
|
||||
title = "Envelope encryption"
|
||||
description = "Envelope encryption"
|
||||
keywords = ["grafana", "envelope encryption", "documentation"]
|
||||
aliases = [""]
|
||||
weight = 430
|
||||
+++
|
||||
|
||||
# Envelope encryption
|
||||
|
||||
In Grafana, you can choose to use envelope encryption. Instead of
|
||||
encrypting all secrets 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).
|
||||
|
||||
To turn on envelope encryption, add the term `envelopeEncryption` to the list of feature toggles in your [Grafana configuration]({{< relref "../administration/configuration/#feature_toggles" >}}).
|
||||
|
||||
> **Note:** Avoid turning off envelope encryption once you have turned it on, and back up your database before turning it on for the first time. If you turn envelope encryption on, create new secrets or update your existing secrets (for example, by creating a new data source or alert notification channel), and then turn envelope encryption off, then those data sources, alert notification channels, and other resources using envelope encryption will stop working and you will experience errors. This is because the secrets encrypted with envelope encryption cannot be decrypted or used by Grafana when envelope encryption is turned off.
|
||||
|
||||
Refer to [Database encryption]({{< relref "../administration/database-encryption.md" >}}) to learn more about how Grafana encrypts secrets in the database.
|
||||
@@ -7,54 +7,32 @@ weight = 401
|
||||
|
||||
# Annotations and labels for alerting rules
|
||||
|
||||
Annotations and labels help customize alert messages so that you can quickly identify the service or application that needs attention.
|
||||
Annotations and labels are key value pairs associated with alerts originating from the alerting rule, datasource response, and as a result of alerting rule evaluation. They can be used in alert notifications directly or in [templates]({{< relref "../message-templating/" >}}) and [template functions]({{< relref "../message-templating/template-functions" >}}) to create notification contact dynamically.
|
||||
|
||||
## Annotations
|
||||
|
||||
Annotations are key-value pairs that provide additional meta-information about an alert. For example: a description, a summary, and runbook URL. These are displayed in rule and alert details in the UI and can be used in contact type message templates. Annotations can also be templated, for example `Instance {{ $labels.instance }} down` will have the evaluated `instance` label value added for every alert this rule produces.
|
||||
Annotations are key-value pairs that provide additional meta-information about an alert. For example: a description, a summary, and runbook URL. These are displayed in rule and alert details in the UI and can be used in contact point message templates.
|
||||
|
||||
## Labels
|
||||
|
||||
Labels are key-value pairs that categorize or identify an alert. Labels are used to match alerts in silences or match and groups alerts in notification policies. Labels are also shown in rule or alert details in the UI and can be used in contact type message templates. For example, you can add a `severity` label, then configure a separate notification policy for each severity. You can also add, for example, a `team` label and configure notification policies specific to the team or silence all alerts for a particular team. Labels can also be templated like annotations, for example, `{{ $labels.namespace }}/{{ $labels.job }}` will produce a new rule label that will have the evaluated `namespace` and `job` label value added for every alert this rule produces. The rule labels take precedence over the labels produced by the query/condition.
|
||||
Labels are key-value pairs that contain information about, and are used to uniquely identify an alert. The label set for an alert is generated and added to throughout the alerting evaluation and notification process.
|
||||
|
||||
### How are labels used?
|
||||
|
||||
- The complete set of labels for an alert is what uniquely identifies an alert within Grafana Alerts.
|
||||
- The Alertmanager uses labels to match alerts for [silences]({{< relref "../silences/" >}}) and [alert groups]({{< relref "../alert-groups/" >}}) in [notification policies]({{< relref "../notification-policies/" >}}).
|
||||
- The alerting UI displays labels for every alert instance generated by the evaluation of that rule.
|
||||
- Contact points can access labels to dynamically generate notifications that contain information specific to the alert that is resulting in a notification.
|
||||
- Labels can be added to an [alerting rule]({{< relref "../alerting-rules/" >}}). These manually configured labels are able to use template functions and reference other labels. Labels added to an alerting rule here take precedence in the event of a collision between labels.
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/unified/rule-edit-details-8-0.png" max-width="550px" caption="Alert details" >}}
|
||||
|
||||
#### Template variables
|
||||
#### Variables available to alerting rule labels and annotations
|
||||
|
||||
The following template variables are available when expanding annotations and labels.
|
||||
|
||||
| Name | Description |
|
||||
| ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. This is unavailable when the rule uses a classic condition. |
|
||||
| $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. This is unavailable when the rule uses a [classic condition]({{< relref "./create-grafana-managed-rule/#single-and-multi-dimensional-rule" >}}) |
|
||||
| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. |
|
||||
|
||||
#### Template functions
|
||||
|
||||
The following template functions are available when expanding annotations and labels.
|
||||
|
||||
| Name | Argument | Return | Description |
|
||||
| ------------------ | ------------------------------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| humanize | number or string | string | Converts a number to a more readable format, using metric prefixes. |
|
||||
| humanize1024 | number or string | string | Like humanize, but uses 1024 as the base rather than 1000. |
|
||||
| humanizeDuration | number or string | string | Converts a duration in seconds to a more readable format. |
|
||||
| humanizePercentage | number or string | string | Converts a ratio value to a fraction of 100. |
|
||||
| humanizeTimestamp | number or string | string | Converts a Unix timestamp in seconds to a more readable format. |
|
||||
| title | string | string | strings.Title, capitalises first character of each word. |
|
||||
| toUpper | string | string | strings.ToUpper, converts all characters to upper case. |
|
||||
| toLower | string | string | strings.ToLower, converts all characters to lower case. |
|
||||
| match | pattern, text | boolean | regexp.MatchString Tests for a unanchored regexp match. |
|
||||
| reReplaceAll | pattern, replacement, text | string | Regexp.ReplaceAllString Regexp substitution, unanchored. |
|
||||
| graphLink | string - JSON Object with `"expr"` and `"datasource"` fields | string | Returns the path to graphical view in [Explore](https://grafana.com/docs/grafana/latest/explore/) for the given expression and data source. |
|
||||
| tableLink | string- JSON Object with `"expr"` and `"datasource"` fields | string | Returns the path to tabular view in [Explore](https://grafana.com/docs/grafana/latest/explore/) for the given expression and data source. |
|
||||
| args | []interface{} | map[string]interface{} | Converts a list of objects to a map with keys, for example, arg0, arg1. Use this function to pass multiple arguments to templates. |
|
||||
| externalURL | nothing | string | Returns a string representing the external URL. |
|
||||
| pathPrefix | nothing | string | Returns the path of the external URL. |
|
||||
| tmpl | string, []interface{} | nothing | Not supported |
|
||||
| safeHtml | string | string | Not supported |
|
||||
| query | query string | []sample | Not supported |
|
||||
| first | []sample | sample | Not supported |
|
||||
| label | label, sample | string | Not supported |
|
||||
| strvalue | []sample | string | Not supported |
|
||||
| value | sample | float64 | Not supported |
|
||||
| sortByLabel | label, []samples | []sample | Not supported |
|
||||
| Name | Description |
|
||||
| ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| $labels | The labels from the query or condition. For example, `{{ $labels.instance }}` and `{{ $labels.job }}`. This is unavailable when the rule uses a [classic condition]({{< relref "./create-grafana-managed-rule/#single-and-multi-dimensional-rule" >}}). |
|
||||
| $values | The values of all reduce and math expressions that were evaluated for this alert rule. For example, `{{ $values.A }}`, `{{ $values.A.Labels }}` and `{{ $values.A.Value }}` where `A` is the `refID` of the expression. This is unavailable when the rule uses a classic condition |
|
||||
| $value | The value string of the alert instance. For example, `[ var='A' labels={instance=foo} value=10 ]`. |
|
||||
|
||||
@@ -11,11 +11,13 @@ Grafana allows you to create alerting rules for an external Cortex or Loki insta
|
||||
|
||||
## Before you begin
|
||||
|
||||
For Cortex and Loki data sources to work with Grafana 8.0 alerting, enable the ruler API by configuring their respective services.
|
||||
- Verify that you have write permission to the Prometheus data source. Otherwise, you will not be able to create or update Cortex managed alerting rules.
|
||||
|
||||
**Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types.
|
||||
- For Cortex and Loki data sources, enable the ruler API by configuring their respective services.
|
||||
|
||||
**Cortex** - When configuring a Grafana Prometheus data source to point to Cortex, use the [legacy `/api/prom` prefix](https://cortexmetrics.io/docs/api/#path-prefixes), not `/prometheus`. The Prometheus data source supports both Cortex and Prometheus, and Grafana expects that both the [Query API](https://cortexmetrics.io/docs/api/#querier--query-frontend) and [Ruler API](https://cortexmetrics.io/docs/api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API.
|
||||
- **Loki** - The `local` rule storage type, default for the Loki data source, supports only viewing of rules. To edit rules, configure one of the other rule storage types.
|
||||
|
||||
- **Cortex** - use the [legacy `/api/prom` prefix](https://cortexmetrics.io/docs/api/#path-prefixes), not `/prometheus`. The Prometheus data source supports both Cortex and Prometheus, and Grafana expects that both the [Query API](https://cortexmetrics.io/docs/api/#querier--query-frontend) and [Ruler API](https://cortexmetrics.io/docs/api/#ruler) are under the same URL. You cannot provide a separate URL for the Ruler API.
|
||||
|
||||
> **Note:** If you do not want to manage alerting rules for a particular Loki or Prometheus data source, go to its settings and clear the **Manage alerts via Alerting UI** checkbox.
|
||||
|
||||
|
||||
@@ -59,17 +59,3 @@ In addition to direct access of data (labels and annotations) stored as KeyValue
|
||||
| Remove | []string | KeyValue | Returns a copy of the Key/Value map without the given keys. |
|
||||
| Names | | []string | List of label names |
|
||||
| Values | | []string | List of label values |
|
||||
|
||||
## Functions
|
||||
|
||||
Some functions to transform values are also available, along with [default functions provided by Go templating](https://golang.org/pkg/text/template/#hdr-Functions).
|
||||
|
||||
| Name | Arguments | Returns |
|
||||
| ------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| title | string | Capitalizes first character of each word. |
|
||||
| toUpper | string | Converts all characters to upper case. |
|
||||
| match | pattern, string | Match a string using RegExp. |
|
||||
| reReplaceAll | pattern, replacement, string | RegExp substitution, unanchored. |
|
||||
| join | string, []string | Concatenates the elements of the second argument to create a single string. First argument is the separator. |
|
||||
| safeHtml | string | Marks string as HTML, not requiring auto-escaping. |
|
||||
| stringSlice | ...string | Returns passed strings as slice of strings. |
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
+++
|
||||
title = "Template functions"
|
||||
keywords = ["grafana", "alerting", "guide", "contact point", "templating"]
|
||||
+++
|
||||
|
||||
# Template Functions
|
||||
|
||||
Template functions allow you to process labels and annotations to generate dynamic notifications.
|
||||
|
||||
| Name | Argument type | Return type | Description |
|
||||
| ----------------------------------------- | ------------------------------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [humanize](#humanize) | number or string | string | Converts a number to a more readable format, using metric prefixes. |
|
||||
| [humanize1024](#humanize1024) | number or string | string | Like humanize, but uses 1024 as the base rather than 1000. |
|
||||
| [humanizeDuration](#humanizeduration) | number or string | string | Converts a duration in seconds to a more readable format. |
|
||||
| [humanizePercentage](#humanizepercentage) | number or string | string | Converts a ratio value to a fraction of 100. |
|
||||
| [humanizeTimestamp](#humanizetimestamp) | number or string | string | Converts a Unix timestamp in seconds to a more readable format. |
|
||||
| [title](#title) | string | string | strings.Title, capitalises first character of each word. |
|
||||
| [toUpper](#toupper) | string | string | strings.ToUpper, converts all characters to upper case. |
|
||||
| [toLower](#tolower) | string | string | strings.ToLower, converts all characters to lower case. |
|
||||
| [match](#match) | pattern, text | boolean | regexp.MatchString Tests for a unanchored regexp match. |
|
||||
| [reReplaceAll](#rereplaceall) | pattern, replacement, text | string | Regexp.ReplaceAllString Regexp substitution, unanchored. |
|
||||
| [graphLink](#graphlink) | string - JSON Object with `"expr"` and `"datasource"` fields | string | Returns the path to graphical view in [Explore](https://grafana.com/docs/grafana/latest/explore/) for the given expression and data source. |
|
||||
| [tableLink](#tablelink) | string- JSON Object with `"expr"` and `"datasource"` fields | string | Returns the path to tabular view in [Explore](https://grafana.com/docs/grafana/latest/explore/) for the given expression and data source. |
|
||||
| [args](#args) | []interface{} | map[string]interface{} | Converts a list of objects to a map with keys, for example, arg0, arg1. Use this function to pass multiple arguments to templates. |
|
||||
| [externalURL](#externalurl) | nothing | string | Returns a string representing the external URL. |
|
||||
| [pathPrefix](#pathprefix) | nothing | string | Returns the path of the external URL. |
|
||||
|
||||
## Examples
|
||||
|
||||
### humanize
|
||||
|
||||
**Template string** `{ humanize $value }`
|
||||
|
||||
**Input** `1234567.0`
|
||||
|
||||
**Expected** `1.235M`
|
||||
|
||||
### humanize1024
|
||||
|
||||
**TemplateString** `{ humanize1024 $value } `
|
||||
|
||||
**Input** `1048576.0`
|
||||
|
||||
**Expected** `1Mi`
|
||||
|
||||
### humanizeDuration
|
||||
|
||||
**TemplateString** `{ humanizeDuration $value }`
|
||||
|
||||
**Input** `899.99`
|
||||
|
||||
**Expected** `14m 59s`
|
||||
|
||||
### humanizePercentage
|
||||
|
||||
**TemplateString** `{ humanizePercentage $value }`
|
||||
|
||||
**Input** `0.1234567`
|
||||
|
||||
**Expected** `12.35%`
|
||||
|
||||
### humanizeTimestamp
|
||||
|
||||
**TemplateString** `{ $value | humanizeTimestamp }`
|
||||
|
||||
**Input** `1435065584.128`
|
||||
|
||||
**Expected** `2015-06-23 13:19:44.128 +0000 UTC`
|
||||
|
||||
### title
|
||||
|
||||
**TemplateString** `{ $value | title }`
|
||||
|
||||
**Input** `aa bb CC`
|
||||
|
||||
**Expected** `Aa Bb Cc`
|
||||
|
||||
### toUpper
|
||||
|
||||
**TemplateString** `{ $value | toUpper }`
|
||||
|
||||
**Input** `aa bb CC`
|
||||
|
||||
**Expected** `AA BB CC`
|
||||
|
||||
### toLower
|
||||
|
||||
**TemplateString** `{ $value | toLower }`
|
||||
|
||||
**Input** `aA bB CC`
|
||||
|
||||
**Expected** `aa bb cc`
|
||||
|
||||
### match
|
||||
|
||||
**TemplateString** `{ match "a+" $labels.instance }`
|
||||
|
||||
**Input** `aa`
|
||||
|
||||
**Expected** `true`
|
||||
|
||||
### reReplaceAll
|
||||
|
||||
**TemplateString** `{{ reReplaceAll "localhost:(.*)" "my.domain:$1" $labels.instance }}`
|
||||
|
||||
**Input** `localhost:3000`
|
||||
|
||||
**Expected** `my.domain:3000`
|
||||
|
||||
### graphLink
|
||||
|
||||
**TemplateString** `{{ graphLink "{\"expr\": \"up\", \"datasource\": \"gdev-prometheus\"}" }}`
|
||||
|
||||
**Expected** `/explore?left=["now-1h","now","gdev-prometheus",{"datasource":"gdev-prometheus","expr":"up","instant":false,"range":true}]`
|
||||
|
||||
### tableLink
|
||||
|
||||
**TemplateString** `{{ tableLink "{\"expr\": \"up\", \"datasource\": \"gdev-prometheus\"}" }}`
|
||||
|
||||
**Expected** `/explore?left=["now-1h","now","gdev-prometheus",{"datasource":"gdev-prometheus","expr":"up","instant":true,"range":false}]`
|
||||
|
||||
### args
|
||||
|
||||
**TemplateString** `{{define "x"}}{{.arg0}} {{.arg1}}{{end}}{{template "x" (args 1 "2")}}`
|
||||
|
||||
**Expected** `1 2`
|
||||
|
||||
### externalURL
|
||||
|
||||
**TemplateString** `{ externalURL }`
|
||||
|
||||
**Expected** `http://localhost/path/prefix`
|
||||
|
||||
### pathPrefix
|
||||
|
||||
**TemplateString** `{ pathPrefix }`
|
||||
|
||||
**Expected** `/path/prefix`
|
||||
@@ -16,7 +16,7 @@ settings page). When you create the application you will need to specify
|
||||
a callback URL. Specify this as callback:
|
||||
|
||||
```bash
|
||||
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/github
|
||||
http://<my_grafana_server_name_or_ip>:<grafana_server_port>/grafana/login/github
|
||||
```
|
||||
|
||||
This callback URL must match the full HTTP address that you use in your
|
||||
|
||||
@@ -7,7 +7,7 @@ weight = 7
|
||||
|
||||
# Time range controls
|
||||
|
||||
Grafana provides several ways to manage the time ranges of the data being visualized, both at the dashboard level and the panel level.
|
||||
Grafana provides several ways to manage the time ranges of the data being visualized, for dashboard, panels and also for alerting.
|
||||
|
||||
This page describes supported time units and relative ranges, the common time controls, dashboard-wide time settings, and panel-specific time settings.
|
||||
|
||||
@@ -34,11 +34,18 @@ Here are some examples:
|
||||
| This Year | `now/Y` | `now/Y` |
|
||||
| Previous fiscal year | `now-1y/fy` | `now-1y/fy` |
|
||||
|
||||
### Note about Grafana alerting
|
||||
|
||||
For Grafana alerting, we do not support are the following syntaxes at this time.
|
||||
|
||||
- now+n for future timestamps.
|
||||
- now-1n/n for "start of n until end of n" since this is an absolute timestamp.
|
||||
|
||||
## Common time range controls
|
||||
|
||||
The dashboard and panel time controls have a common user interface (UI).
|
||||
|
||||
<img class="no-shadow" src="/static/img/docs/time-range-controls/common-time-controls-8-2.png" max-width="700px">
|
||||
<img class="no-shadow" src="/static/img/docs/time-range-controls/common-time-controls-7-0.png" max-width="700px">
|
||||
|
||||
The options are defined below.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Grafana ships with a built-in PostgreSQL data source plugin that allows you to q
|
||||
|
||||
## PostgreSQL settings
|
||||
|
||||
To access PostgreSQL settings, hover your mouse over the **Configuration** (gear) icon, then click **Data Sources**, and then click the Prometheus data source.
|
||||
To access PostgreSQL settings, hover your mouse over the **Configuration** (gear) icon, then click **Data Sources**, and then click the PostgreSQL data source.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -7,7 +7,7 @@ aliases = ["/docs/grafana/latest/plugins/developing/snapshot-mode/"]
|
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/Grafana-snapshot-example.png" caption="A dashboard using snapshot data and not live data." >}}
|
||||
|
||||
Grafana has this great feature where you can [save a snapshot of your dashboard]({{< relref "../../../dashboards/json-model.md" >}}). Instead of sending a screenshot of a dashboard to someone, you can send them a working, interactive Grafana dashboard with the snapshot data embedded inside it. The snapshot can be saved on your Grafana server and is available to all your co-workers. Raintank also hosts a [snapshot server](http://snapshot.raintank.io/) if you want to send the snapshot to someone who does not have access to your Grafana server.
|
||||
Grafana has this great feature where you can [save a snapshot of your dashboard]({{< relref "../../../dashboards/json-model.md" >}}). Instead of sending a screenshot of a dashboard to someone, you can send them a working, interactive Grafana dashboard with the snapshot data embedded inside it. The snapshot can be saved on your Grafana server and is available to all your co-workers. Raintank also hosts a [snapshot server](https://snapshots.raintank.io) if you want to send the snapshot to someone who does not have access to your Grafana server.
|
||||
|
||||
{{< figure class="float-right" src="/static/img/docs/animated_gifs/snapshots.gif" caption="Selecting a snapshot" >}}
|
||||
|
||||
@@ -72,4 +72,4 @@ loadLocationDataFromFile(reload) {
|
||||
|
||||
It is really easy to forget to add this support but it enables a great feature and can be used to demo your panel.
|
||||
|
||||
If there is a panel plugin that you would like to be installed on the Raintank Snapshot server then please contact us via [Slack](https://raintank.slack.com) or [GitHub](https://github.com/grafana/grafana).
|
||||
If there is a panel plugin that you would like to be installed on the Raintank Snapshot server then please contact us via [Slack](https://slack.grafana.com) or [GitHub](https://github.com/grafana/grafana).
|
||||
|
||||
@@ -11,5 +11,7 @@ You can choose to encrypt secrets stored in the Grafana database using a key fro
|
||||
|
||||
Grafana integrates with the following key management systems:
|
||||
|
||||
- AWS KMS
|
||||
- Azure Key Vault
|
||||
- [AWS KMS]({{< relref "/using-aws-kms-to-encrypt-database-secrets.md" >}})
|
||||
- [Azure Key Vault]({{< relref "/using-azure-key-vault-to-encrypt-database-secrets.md" >}})
|
||||
|
||||
Refer to [Database encryption]({{< relref "../../administration/database-encryption.md" >}}) to learn more about how Grafana encrypts secrets in the database.
|
||||
|
||||
@@ -39,7 +39,6 @@ You can use an encryption key from AWS Key Management Service to encrypt secrets
|
||||
|
||||
- `access_key_id`: The AWS Access Key ID that you previously generated.
|
||||
- `secret_access_key`: The AWS Secret Access Key you previously generated.
|
||||
- `token`: (Optional) An AWS Session Token, which you must provide if you created temporary credentials.
|
||||
- `region`: The AWS region where you created the KMS key. The region is contained in the key’s ARN. For example: `arn:aws:kms:*us-east-2*:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab`
|
||||
|
||||
An example of an AWS KMS provider section in the `grafana.ini` file is as follows:
|
||||
@@ -53,8 +52,6 @@ You can use an encryption key from AWS Key Management Service to encrypt secrets
|
||||
;access_key_id = AKIAIOSFODNN7EXAMPLE
|
||||
# AWS secret access key
|
||||
;secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
|
||||
# AWS session token, optional
|
||||
;token = AQoDYXdzEJr...<REMAINDER OF SECURITY TOKEN>
|
||||
# AWS region, for example eu-north-1
|
||||
;region = eu-north-1
|
||||
```
|
||||
|
||||
@@ -13,25 +13,28 @@ dashboards, creating users, and updating data sources.
|
||||
|
||||
## HTTP APIs
|
||||
|
||||
- [Admin API]({{< relref "admin.md" >}})
|
||||
- [Alerting Notification Channels API]({{< relref "alerting_notification_channels.md" >}})
|
||||
- [Alerting API]({{< relref "alerting.md" >}})
|
||||
- [Annotations API]({{< relref "annotations.md" >}})
|
||||
- [Authentication API]({{< relref "auth.md" >}})
|
||||
- [Dashboard API]({{< relref "dashboard.md" >}})
|
||||
- [Dashboard versions API]({{< relref "dashboard_versions.md" >}})
|
||||
- [Dashboard permissions API]({{< relref "dashboard_permissions.md" >}})
|
||||
- [Folder API]({{< relref "folder.md" >}})
|
||||
- [Folder permissions API]({{< relref "folder_permissions.md" >}})
|
||||
- [Folder/dashboard search API]({{< relref "folder_dashboard_search.md" >}})
|
||||
- [Dashboard Permissions API]({{< relref "dashboard_permissions.md" >}})
|
||||
- [Dashboard Versions API]({{< relref "dashboard_versions.md" >}})
|
||||
- [Data source API]({{< relref "data_source.md" >}})
|
||||
- [Folder API]({{< relref "folder.md" >}})
|
||||
- [Folder Permissions API]({{< relref "folder_permissions.md" >}})
|
||||
- [Folder/Dashboard Search API]({{< relref "folder_dashboard_search.md" >}})
|
||||
- [Library Element API]({{< relref "library_element.md" >}})
|
||||
- [Organization API]({{< relref "org.md" >}})
|
||||
- [Snapshot API]({{< relref "snapshot.md" >}})
|
||||
- [Annotations API]({{< relref "annotations.md" >}})
|
||||
- [Playlists API]({{< relref "playlist.md" >}})
|
||||
- [Alerting API]({{< relref "alerting.md" >}})
|
||||
- [Alert notification channels API]({{< relref "alerting_notification_channels.md" >}})
|
||||
- [User API]({{< relref "user.md" >}})
|
||||
- [Team API]({{< relref "team.md" >}})
|
||||
- [Admin API]({{< relref "admin.md" >}})
|
||||
- [Preferences API]({{< relref "preferences.md" >}})
|
||||
- [Other API]({{< relref "other.md" >}})
|
||||
- [Playlists API]({{< relref "playlist.md" >}})
|
||||
- [Preferences API]({{< relref "preferences.md" >}})
|
||||
- [Service account API]({{< relref "serviceaccount.md" >}})
|
||||
- [Short URL API]({{< relref "short_url.md" >}})
|
||||
- [Snapshot API]({{< relref "snapshot.md" >}})
|
||||
- [Team API]({{< relref "team.md" >}})
|
||||
- [User API]({{< relref "user.md" >}})
|
||||
|
||||
## Grafana Enterprise HTTP APIs
|
||||
|
||||
|
||||
@@ -176,35 +176,37 @@ HTTP/1.1 200
|
||||
Status Codes:
|
||||
|
||||
- **200** – Created
|
||||
- **400** – Errors (for example, name or UID already exists, invalid JSON, missing or invalid fields, and so on).
|
||||
- **401** – Unauthorized
|
||||
- **403** – Access denied
|
||||
|
||||
## Update library element
|
||||
|
||||
`PATCH /api/library-elements/:uid`
|
||||
|
||||
Updates an existing library element identified by uid.
|
||||
|
||||
JSON Body schema:
|
||||
|
||||
- **folderId** – ID of the folder where the library element is stored.
|
||||
- **name** – Name of the library element.
|
||||
- **model** – The JSON model for the library element.
|
||||
- **kind** – Kind of element to create. Use `1` for library panels or `2` for library variables.
|
||||
- **version** – Version of the library element you are updating.
|
||||
- **uid** – Optional, the [unique identifier](/http_api/library_element/#identifier-id-vs-unique-identifier-uid).
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PATCH /api/library-elements/nErXDvCkzz HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
- **400** – Errors (for example, name or UID already exists, invalid JSON, missing or invalid fields, and so on).
|
||||
- **401** – Unauthorized
|
||||
- **403** – Access denied
|
||||
|
||||
## Update library element
|
||||
|
||||
`PATCH /api/library-elements/:uid`
|
||||
|
||||
Updates an existing library element identified by uid.
|
||||
|
||||
JSON Body schema:
|
||||
|
||||
- **folderId** – ID of the folder where the library element is stored.
|
||||
- **name** – Name of the library element.
|
||||
- **model** – The JSON model for the library element.
|
||||
- **kind** – Kind of element to create. Use `1` for library panels or `2` for library variables.
|
||||
- **version** – Version of the library element you are updating.
|
||||
- **uid** – Optional, the [unique identifier](/http_api/library_element/#identifier-id-vs-unique-identifier-uid).
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PATCH /api/library-elements/nErXDvCkzz HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
```
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -7,7 +7,13 @@ aliases = ["/docs/grafana/latest/http_api/team/"]
|
||||
|
||||
# Team API
|
||||
|
||||
This API can be used to create/update/delete Teams and to add/remove users to Teams. All actions require that the user has the Admin role for the organization.
|
||||
This API can be used to manage Teams and Team Memberships.
|
||||
|
||||
Access to these API endpoints is restricted as follows:
|
||||
|
||||
- All authenticated users are able to view details of teams they are a member of.
|
||||
- Organization Admins are able to manage all teams and team members.
|
||||
- If the `editors_can_admin` configuration flag is enabled, Organization Editors are able to view details of all teams and to manage teams that they are Admin members of.
|
||||
|
||||
## Team Search With Paging
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ http {
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_pass http://grafana;
|
||||
}
|
||||
|
||||
@@ -8,12 +8,16 @@ 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 8.4.0-beta1]({{< relref "release-notes-8-4-0-beta1" >}})
|
||||
- [Release notes for 8.3.5]({{< relref "release-notes-8-3-5" >}})
|
||||
- [Release notes for 8.3.4]({{< relref "release-notes-8-3-4" >}})
|
||||
- [Release notes for 8.3.3]({{< relref "release-notes-8-3-3" >}})
|
||||
- [Release notes for 8.3.2]({{< relref "release-notes-8-3-2" >}})
|
||||
- [Release notes for 8.3.1]({{< relref "release-notes-8-3-1" >}})
|
||||
- [Release notes for 8.3.0]({{< relref "release-notes-8-3-0" >}})
|
||||
- [Release notes for 8.3.0-beta2]({{< relref "release-notes-8-3-0-beta2" >}})
|
||||
- [Release notes for 8.3.0-beta1]({{< relref "release-notes-8-3-0-beta1" >}})
|
||||
- [Release notes for 8.2.7]({{< relref "release-notes-8-2-7" >}})
|
||||
- [Release notes for 8.2.6]({{< relref "release-notes-8-2-6" >}})
|
||||
- [Release notes for 8.2.5]({{< relref "release-notes-8-2-5" >}})
|
||||
- [Release notes for 8.2.4]({{< relref "release-notes-8-2-4" >}})
|
||||
@@ -23,6 +27,7 @@ about deprecations, breaking changes as well as changes that relate to plugin de
|
||||
- [Release notes for 8.2.0]({{< relref "release-notes-8-2-0" >}})
|
||||
- [Release notes for 8.2.0-beta2]({{< relref "release-notes-8-2-0-beta2" >}})
|
||||
- [Release notes for 8.2.0-beta1]({{< relref "release-notes-8-2-0-beta1" >}})
|
||||
- [Release notes for 8.1.8]({{< relref "release-notes-8-1-8" >}})
|
||||
- [Release notes for 8.1.7]({{< relref "release-notes-8-1-7" >}})
|
||||
- [Release notes for 8.1.6]({{< relref "release-notes-8-1-6" >}})
|
||||
- [Release notes for 8.1.5]({{< relref "release-notes-8-1-5" >}})
|
||||
@@ -34,6 +39,7 @@ about deprecations, breaking changes as well as changes that relate to plugin de
|
||||
- [Release notes for 8.1.0-beta3]({{< relref "release-notes-8-1-0-beta3" >}})
|
||||
- [Release notes for 8.1.0-beta2]({{< relref "release-notes-8-1-0-beta2" >}})
|
||||
- [Release notes for 8.1.0-beta1]({{< relref "release-notes-8-1-0-beta1" >}})
|
||||
- [Release notes for 8.0.7]({{< relref "release-notes-8-0-7" >}})
|
||||
- [Release notes for 8.0.6]({{< relref "release-notes-8-0-6" >}})
|
||||
- [Release notes for 8.0.5]({{< relref "release-notes-8-0-5" >}})
|
||||
- [Release notes for 8.0.4]({{< relref "release-notes-8-0-4" >}})
|
||||
@@ -44,6 +50,8 @@ about deprecations, breaking changes as well as changes that relate to plugin de
|
||||
- [Release notes for 8.0.0-beta3]({{< relref "release-notes-8-0-0-beta3" >}})
|
||||
- [Release notes for 8.0.0-beta2]({{< relref "release-notes-8-0-0-beta2" >}})
|
||||
- [Release notes for 8.0.0-beta1]({{< relref "release-notes-8-0-0-beta1" >}})
|
||||
- [Release notes for 7.5.15]({{< relref "release-notes-7-5-15" >}})
|
||||
- [Release notes for 7.5.13]({{< relref "release-notes-7-5-13" >}})
|
||||
- [Release notes for 7.5.12]({{< relref "release-notes-7-5-12" >}})
|
||||
- [Release notes for 7.5.11]({{< relref "release-notes-7-5-11" >}})
|
||||
- [Release notes for 7.5.10]({{< relref "release-notes-7-5-10" >}})
|
||||
|
||||
11
docs/sources/release-notes/release-notes-7-5-15.md
Normal file
11
docs/sources/release-notes/release-notes-7-5-15.md
Normal file
@@ -0,0 +1,11 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 7.5.15"
|
||||
[_build]
|
||||
list = false
|
||||
+++
|
||||
|
||||
# Release notes for Grafana 7.5.15
|
||||
|
||||
- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
42
docs/sources/release-notes/release-notes-8-3-4.md
Normal file
42
docs/sources/release-notes/release-notes-8-3-4.md
Normal file
@@ -0,0 +1,42 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.3.4"
|
||||
[_build]
|
||||
list = false
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.3.4
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **Alerting:** Allow configuration of non-ready alertmanagers. [#43063](https://github.com/grafana/grafana/pull/43063), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Allow customization of Google chat message. [#43568](https://github.com/grafana/grafana/pull/43568), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Allow customization of Google chat message (#43568). [#43723](https://github.com/grafana/grafana/pull/43723), [@alexweav](https://github.com/alexweav)
|
||||
- **AppPlugins:** Support app plugins with only default nav. [#43016](https://github.com/grafana/grafana/pull/43016), [@torkelo](https://github.com/torkelo)
|
||||
- **InfluxDB:** InfluxQL: query editor: skip fields in metadata queries. [#42543](https://github.com/grafana/grafana/pull/42543), [@gabor](https://github.com/gabor)
|
||||
- **Postgres/MySQL/MSSQL:** Cancel in-flight SQL query if user cancels query in grafana. [#43890](https://github.com/grafana/grafana/pull/43890), [@mdvictor](https://github.com/mdvictor)
|
||||
- **Prometheus:** Forward oauth tokens after prometheus datasource migration. [#43686](https://github.com/grafana/grafana/pull/43686), [@MasslessParticle](https://github.com/MasslessParticle)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Azure Monitor:** Bug fix for variable interpolations in metrics dropdowns. [#43251](https://github.com/grafana/grafana/pull/43251), [@sarahzinger](https://github.com/sarahzinger)
|
||||
- **Azure Monitor:** Improved error messages for variable queries. [#43213](https://github.com/grafana/grafana/pull/43213), [@sunker](https://github.com/sunker)
|
||||
- **CloudMonitoring:** Fixes broken variable queries that use group bys. [#43914](https://github.com/grafana/grafana/pull/43914), [@sunker](https://github.com/sunker)
|
||||
- **Configuration:** You can now see your expired API keys if you have no active ones. [#42452](https://github.com/grafana/grafana/pull/42452), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Elasticsearch:** Fix handling multiple datalinks for a single field. [#44029](https://github.com/grafana/grafana/pull/44029), [@Elfo404](https://github.com/Elfo404)
|
||||
- **Export:** Fix error being thrown when exporting dashboards using query variables that reference the default datasource. [#44034](https://github.com/grafana/grafana/pull/44034), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **ImportDashboard:** Fixes issue with importing dashboard and name ending up in uid. [#43451](https://github.com/grafana/grafana/pull/43451), [@torkelo](https://github.com/torkelo)
|
||||
- **Login:** Page no longer overflows on mobile. [#43739](https://github.com/grafana/grafana/pull/43739), [@ashharrison90](https://github.com/ashharrison90)
|
||||
- **Plugins:** Set backend metadata property for core plugins. [#43349](https://github.com/grafana/grafana/pull/43349), [@marefr](https://github.com/marefr)
|
||||
- **Prometheus:** Fill missing steps with null values. [#43622](https://github.com/grafana/grafana/pull/43622), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Fix interpolation of $\_\_rate_interval variable. [#44035](https://github.com/grafana/grafana/pull/44035), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Interpolate variables with curly brackets syntax. [#42927](https://github.com/grafana/grafana/pull/42927), [@ivanahuckova](https://github.com/ivanahuckova)
|
||||
- **Prometheus:** Respect the http-method data source setting. [#42753](https://github.com/grafana/grafana/pull/42753), [@gabor](https://github.com/gabor)
|
||||
- **Table:** Fixes issue with field config applied to wrong fields when hiding columns. [#43376](https://github.com/grafana/grafana/pull/43376), [@torkelo](https://github.com/torkelo)
|
||||
- **Toolkit:** Fix bug with rootUrls not being properly parsed when signing a private plugin. [#43014](https://github.com/grafana/grafana/pull/43014), [@dessen-xu](https://github.com/dessen-xu)
|
||||
- **Variables:** Fix so data source variables are added to adhoc configuration. [#43881](https://github.com/grafana/grafana/pull/43881), [@hugohaggmark](https://github.com/hugohaggmark)
|
||||
|
||||
### Plugin development fixes & changes
|
||||
|
||||
- **Toolkit:** Revert build config so tslib is bundled with plugins to prevent plugins from crashing. [#43556](https://github.com/grafana/grafana/pull/43556), [@mckn](https://github.com/mckn)
|
||||
11
docs/sources/release-notes/release-notes-8-3-5.md
Normal file
11
docs/sources/release-notes/release-notes-8-3-5.md
Normal file
@@ -0,0 +1,11 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.3.5"
|
||||
[_build]
|
||||
list = false
|
||||
+++
|
||||
|
||||
# Release notes for Grafana 8.3.5
|
||||
|
||||
- **Security**: Fixes CVE-2022-21702. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21703. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
- **Security**: Fixes CVE-2022-21713. For more information, see our [blog](https://grafana.com/blog/2022/02/08/grafana-7.5.15-and-8.3.5-released-with-moderate-severity-security-fixes/)
|
||||
@@ -30,14 +30,14 @@ To share a direct link:
|
||||
A dashboard snapshot shares an interactive dashboard publicly. Grafana strips sensitive data like queries
|
||||
(metric, template and annotation) and panel links, leaving only the visible metric data and series names embedded into your dashboard. Dashboard snapshots can be accessed by anyone with the link.
|
||||
|
||||
You can publish snapshots to your local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The latter is a free service
|
||||
You can publish snapshots to your local instance or to [snapshots.raintank.io](http://snapshots.raintank.io). The latter is a free service
|
||||
provided by Grafana Labs that allows you to publish dashboard snapshots to an external Grafana instance. The same rules still apply: anyone with the link can view it. You can set an expiration time if you want the snapshot removed after a certain time period.
|
||||
|
||||

|
||||
|
||||
To publish a snapshot:
|
||||
|
||||
1. Click on **Local Snapshot** or **Publish to snapshot.raintank.io**. This generates the link of the snapshot.
|
||||
1. Click on **Local Snapshot** or **Publish to snapshots.raintank.io**. This generates the link of the snapshot.
|
||||
1. Copy the snapshot link, and share it either within your organization or publicly on the web.
|
||||
|
||||
In case you created a snapshot by mistake, click **delete snapshot** to remove the snapshot from your Grafana instance.
|
||||
|
||||
@@ -46,14 +46,14 @@ https://play.grafana.org/d/000000012/grafana-play-home?orgId=1&from=156871968017
|
||||
|
||||
A panel snapshot shares an interactive panel publicly. Grafana strips sensitive data leaving only the visible metric data and series names embedded into your dashboard. Panel snapshots can be accessed by anyone with the link.
|
||||
|
||||
You can publish snapshots to your local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The latter is a free service provided by [Raintank](http://raintank.io), that allows you to publish dashboard snapshots to an external Grafana instance. You can optionally set an expiration time if you want the snapshot to be removed after a certain time period.
|
||||
You can publish snapshots to your local instance or to [snapshots.raintank.io](http://snapshots.raintank.io). The latter is a free service provided by [Grafana Labs](https://grafana.com), that allows you to publish dashboard snapshots to an external Grafana instance. You can optionally set an expiration time if you want the snapshot to be removed after a certain time period.
|
||||
|
||||

|
||||
|
||||
To publish a snapshot:
|
||||
|
||||
1. In the Share Panel dialog, click **Snapshot** to open the tab.
|
||||
1. Click on **Local Snapshot** or **Publish to snapshot.raintank.io**. This generates the link of the snapshot.
|
||||
1. Click on **Local Snapshot** or **Publish to snapshots.raintank.io**. This generates the link of the snapshot.
|
||||
1. Copy the snapshot link, and share it either within your organization or publicly on the web.
|
||||
|
||||
If you created a snapshot by mistake, click **delete snapshot** to remove the snapshot from your Grafana instance.
|
||||
@@ -70,7 +70,7 @@ Here is an example of the HTML code:
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://snapshot.raintank.io/dashboard-solo/snapshot/y7zwi2bZ7FcoTlB93WN7yWO4aMiz3pZb?from=1493369923321&to=1493377123321&panelId=4"
|
||||
src="https://snapshots.raintank.io/dashboard-solo/snapshot/y7zwi2bZ7FcoTlB93WN7yWO4aMiz3pZb?from=1493369923321&to=1493377123321&panelId=4"
|
||||
width="650"
|
||||
height="300"
|
||||
frameborder="0"
|
||||
@@ -79,7 +79,7 @@ Here is an example of the HTML code:
|
||||
|
||||
The result is an interactive Grafana graph embedded in an iframe:
|
||||
|
||||
<iframe src="https://snapshot.raintank.io/dashboard-solo/snapshot/y7zwi2bZ7FcoTlB93WN7yWO4aMiz3pZb?from=1493369923321&to=1493377123321&panelId=4" width="650" height="300" frameborder="0"></iframe>
|
||||
<iframe src="https://snapshots.raintank.io/dashboard-solo/snapshot/y7zwi2bZ7FcoTlB93WN7yWO4aMiz3pZb?from=1493369923321&to=1493377123321&panelId=4" width="650" height="300" frameborder="0"></iframe>
|
||||
|
||||
## Library panel
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ They're a great way to communicate about a particular incident with specific peo
|
||||
|
||||
### Publish snapshots
|
||||
|
||||
You can publish snapshots locally or to [snapshot.raintank.io](http://snapshot.raintank.io). snapshot.raintank is a free service provided by [raintank](http://raintank.io) for hosting external Grafana snapshots.
|
||||
You can publish snapshots locally or to [snapshots.raintank.io](https://snapshots.raintank.io). snapshots.raintank.io is a free service provided by [Grafana Labs](https://grafana.com) for hosting external Grafana snapshots.
|
||||
|
||||
Either way, anyone with the link (and access to your Grafana instance for local snapshots) can view it.
|
||||
|
||||
@@ -82,11 +82,11 @@ Currently you can only override the dashboard time with relative time ranges, no
|
||||
|
||||
You can embed a single panel on another web page or your own application using the panel share dialog.
|
||||
|
||||
Below you should see an iframe with a graph panel (taken from a Dashboard snapshot at [snapshot.raintank.io](http://snapshot.raintank.io).
|
||||
Below you should see an iframe with a graph panel (taken from a Dashboard snapshot at [snapshots.raintank.io](http://snapshots.raintank.io).
|
||||
|
||||
Try hovering or zooming on the panel below!
|
||||
|
||||
<iframe src="https://snapshot.raintank.io/dashboard-solo/snapshot/4IKyWYNEQll1B9FXcN3RIgx4M2VGgU8d?panelId=4&fullscreen" width="650" height="300" frameborder="0"></iframe>
|
||||
<iframe src="https://snapshots.raintank.io/dashboard-solo/snapshot/4IKyWYNEQll1B9FXcN3RIgx4M2VGgU8d?panelId=4&fullscreen" width="650" height="300" frameborder="0"></iframe>
|
||||
|
||||
This feature makes it easy to include interactive visualizations from your Grafana instance anywhere you want.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
set -xeo pipefail
|
||||
|
||||
. e2e/variables
|
||||
. scripts/grafana-server/variables
|
||||
|
||||
HOST=${HOST:-$DEFAULT_HOST}
|
||||
PORT=${PORT:-$DEFAULT_PORT}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
. e2e/variables
|
||||
. scripts/grafana-server/variables
|
||||
|
||||
if [ "$BASE_URL" != "" ]; then
|
||||
echo -e "BASE_URL set, skipping starting server"
|
||||
else
|
||||
# Start it in the background
|
||||
./e2e/start-server 2>&1 > e2e/server.log &
|
||||
./e2e/wait-for-grafana
|
||||
./scripts/grafana-server/start-server 2>&1 > scripts/grafana-server/server.log &
|
||||
./scripts/grafana-server/wait-for-grafana
|
||||
fi
|
||||
|
||||
./e2e/run-suite "$@"
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
. e2e/variables
|
||||
|
||||
PORT=${PORT:-$DEFAULT_PORT}
|
||||
PACKAGE_FILE=${PACKAGE_FILE:-$DEFAULT_PACKAGE_FILE}
|
||||
|
||||
./e2e/kill-server
|
||||
|
||||
mkdir $RUNDIR
|
||||
|
||||
echo -e "Copying grafana backend files to temp dir..."
|
||||
|
||||
# Expand any wildcards
|
||||
pkgs=(${PACKAGE_FILE})
|
||||
pkg=${pkgs[0]}
|
||||
if [[ -f ${pkg} ]]; then
|
||||
echo "Found package tar file ${pkg}, extracting..."
|
||||
tar zxf ${pkg} -C $RUNDIR
|
||||
mv $RUNDIR/grafana-*/* $RUNDIR
|
||||
else
|
||||
echo "Couldn't find package ${PACKAGE_FILE} - copying local dev files"
|
||||
|
||||
if [[ ! -f bin/grafana-server ]]; then
|
||||
echo bin/grafana-server missing
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp -r ./bin $RUNDIR
|
||||
cp -r ./public $RUNDIR
|
||||
cp -r ./tools $RUNDIR
|
||||
|
||||
mkdir $RUNDIR/conf
|
||||
mkdir $PROV_DIR
|
||||
mkdir $PROV_DIR/datasources
|
||||
mkdir $PROV_DIR/dashboards
|
||||
|
||||
cp ./conf/defaults.ini $RUNDIR/conf/defaults.ini
|
||||
fi
|
||||
|
||||
echo -e "Copy provisioning setup from devenv"
|
||||
|
||||
cp devenv/datasources.yaml $PROV_DIR/datasources
|
||||
cp devenv/dashboards.yaml $PROV_DIR/dashboards
|
||||
|
||||
cp -r devenv $RUNDIR
|
||||
|
||||
echo -e "Starting Grafana Server port $PORT"
|
||||
|
||||
$RUNDIR/bin/grafana-server \
|
||||
--homepath=$HOME_PATH \
|
||||
--pidfile=$RUNDIR/pid \
|
||||
cfg:server.http_port=$PORT \
|
||||
cfg:server.router_logging=1 \
|
||||
cfg:app_mode=development
|
||||
|
||||
# 2>&1 > $RUNDIR/output.log &
|
||||
# cfg:log.level=debug \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
. e2e/variables
|
||||
. scripts/grafana-server/variables
|
||||
|
||||
./e2e/run-suite verify/specs
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<!-- The template body needs to be wrapped in div to prevent Premailer from adding </tr></td> tags which breaks the HTML structure -->
|
||||
<div>
|
||||
|
||||
[[Subject .Subject "[[.Title]]"]]
|
||||
|
||||
[[ define "alert" ]]
|
||||
<tr>
|
||||
<td colspan="2" class="value">
|
||||
<span class="value-heading">Value:</span> <span class="value-value">[[ .ValueString ]]</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="value">
|
||||
<span class="value-heading">Value:</span> <span class="value-value">[[ .ValueString ]]</span>
|
||||
</td>
|
||||
</tr>
|
||||
[[ if gt (len .Annotations.SortedPairs) 0 ]]
|
||||
<tr>
|
||||
<td colspan="2" class="annotations">
|
||||
@@ -187,62 +190,63 @@
|
||||
<tr>
|
||||
<td class="twelve">
|
||||
<table>
|
||||
[[ if gt (len .Alerts.Firing) 0 ]]
|
||||
<tr>
|
||||
<td colspan="2" class="section-heading">
|
||||
Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
|
||||
[[ range .GroupLabels.SortedPairs ]]
|
||||
[[ .Name ]]=[[ .Value ]]
|
||||
[[ end ]][[ end ]]
|
||||
</td>
|
||||
</tr>
|
||||
[[ range .Alerts.Firing ]]
|
||||
[[ if gt (len .Alerts.Firing) 0 ]]
|
||||
<tr>
|
||||
<td colspan="2" class="section-heading">
|
||||
Firing: [[ .Alerts.Firing | len ]] alert[[ if gt (len .Alerts.Firing) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
|
||||
[[ range .GroupLabels.SortedPairs ]]
|
||||
[[ .Name ]]=[[ .Value ]]
|
||||
[[ end ]][[ end ]]
|
||||
</td>
|
||||
</tr>
|
||||
[[ range .Alerts.Firing ]]
|
||||
<tr>
|
||||
<td
|
||||
class="status-tag status-firing"
|
||||
width="68"
|
||||
>
|
||||
Firing
|
||||
</td>
|
||||
<td class="alert-label">
|
||||
[[ .Labels.alertname ]]
|
||||
</td>
|
||||
</tr>
|
||||
[[ template "alert" . ]]
|
||||
[[ end ]]
|
||||
[[ end ]]
|
||||
[[ if gt (len .Alerts.Resolved) 0 ]]
|
||||
<tr>
|
||||
<td colspan="2" class="section-heading">
|
||||
Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
|
||||
[[ range .GroupLabels.SortedPairs ]]
|
||||
[[ .Name ]]=[[ .Value ]]
|
||||
[[ end ]][[ end ]]
|
||||
</td>
|
||||
</tr>
|
||||
[[ range .Alerts.Resolved ]]
|
||||
<tr>
|
||||
<td
|
||||
class="status-tag status-resolved"
|
||||
width="68"
|
||||
>
|
||||
Resolved
|
||||
</td>
|
||||
<td class="alert-label">
|
||||
[[ .Labels.alertname ]]
|
||||
</td>
|
||||
</tr>
|
||||
[[ template "alert" . ]]
|
||||
[[ end ]]
|
||||
[[ end ]]
|
||||
<tr>
|
||||
<td
|
||||
class="status-tag status-firing"
|
||||
width="68"
|
||||
>
|
||||
Firing
|
||||
</td>
|
||||
<td class="alert-label">
|
||||
[[ .Labels.alertname ]]
|
||||
<td colspan="2">
|
||||
<a href="[[ .AlertPageUrl ]]" class="button">Go to alerts page</a>
|
||||
</td>
|
||||
</tr>
|
||||
[[ template "alert" . ]]
|
||||
[[ end ]]
|
||||
[[ end ]]
|
||||
[[ if gt (len .Alerts.Resolved) 0 ]]
|
||||
<tr>
|
||||
<td colspan="2" class="section-heading">
|
||||
Resolved: [[ .Alerts.Resolved | len ]] alert[[ if gt (len .Alerts.Resolved) 1 ]]s[[ end ]][[ if gt (len .GroupLabels.SortedPairs) 1 ]] for
|
||||
[[ range .GroupLabels.SortedPairs ]]
|
||||
[[ .Name ]]=[[ .Value ]]
|
||||
[[ end ]][[ end ]]
|
||||
</td>
|
||||
</tr>
|
||||
[[ range .Alerts.Resolved ]]
|
||||
<tr>
|
||||
<td
|
||||
class="status-tag status-resolved"
|
||||
width="68"
|
||||
>
|
||||
Resolved
|
||||
</td>
|
||||
<td class="alert-label">
|
||||
[[ .Labels.alertname ]]
|
||||
</td>
|
||||
</tr>
|
||||
[[ template "alert" . ]]
|
||||
[[ end ]]
|
||||
[[ end ]]
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="[[ .AlertPageUrl ]]" class="button">Go to alerts page</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
[[ end ]]
|
||||
|
||||
</div>
|
||||
|
||||
4
go.mod
4
go.mod
@@ -62,7 +62,7 @@ require (
|
||||
github.com/hashicorp/go-plugin v1.4.3
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/inconshreveable/log15 v0.0.0-20180818164646-67afb5ed74ec
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.3.1-0.20210518120617-5d1fff431040
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.6.0
|
||||
github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097
|
||||
github.com/jmespath/go-jmespath v0.4.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
@@ -157,7 +157,7 @@ require (
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/cockroachdb/apd/v2 v2.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.6.0 // indirect
|
||||
github.com/deepmap/oapi-codegen v1.8.2 // indirect
|
||||
github.com/dennwc/varint v1.0.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -603,8 +603,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4=
|
||||
github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0=
|
||||
github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
|
||||
github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU=
|
||||
github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw=
|
||||
github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE=
|
||||
github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA=
|
||||
github.com/denverdino/aliyungo v0.0.0-20170926055100-d3308649c661/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
@@ -767,6 +768,7 @@ github.com/gchaincl/sqlhooks v1.3.0/go.mod h1:9BypXnereMT0+Ys8WGWHqzgkkOfHIhyeUC
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
|
||||
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
|
||||
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
|
||||
github.com/getsentry/sentry-go v0.10.0 h1:6gwY+66NHKqyZrdi6O2jGdo7wGdo9b3B69E01NFgT5g=
|
||||
github.com/getsentry/sentry-go v0.10.0/go.mod h1:kELm/9iCblqUYh+ZRML7PNdCvEuw24wBvJPYyi86cws=
|
||||
@@ -1437,8 +1439,9 @@ github.com/influxdata/influxdb v1.8.4/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbf
|
||||
github.com/influxdata/influxdb v1.8.5/go.mod h1:oFH+pbEyDln/1TKwa98oJzVrkZwdjrJOwIDGYZj7Ma0=
|
||||
github.com/influxdata/influxdb v1.9.2/go.mod h1:UEe3MeD9AaP5rlPIes102IhYua3FhIWZuOXNHxDjSrI=
|
||||
github.com/influxdata/influxdb v1.9.3/go.mod h1:xD4ZjAgEJQO9/bX3NhFrssKtdNPi+ki1kjrttJRDhGc=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.3.1-0.20210518120617-5d1fff431040 h1:MBLCfcSsUyFPDJp6T7EoHp/Ph3Jkrm4EuUKLD2rUWHg=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.3.1-0.20210518120617-5d1fff431040/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.6.0 h1:bIOaGTgvvv1Na2hG+nIvqyv7PK2UiU2WrJN1ck1ykyM=
|
||||
github.com/influxdata/influxdb-client-go/v2 v2.6.0/go.mod h1:Y/0W1+TZir7ypoQZYd2IrnVOKB3Tq6oegAQeSVN/+EU=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/influxdata/influxql v1.1.0/go.mod h1:KpVI7okXjK6PRi3Z5B+mtKZli+R1DnZgb3N+tzevNgo=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "8.3.2",
|
||||
"testing": "8.3.2"
|
||||
"stable": "8.3.5",
|
||||
"testing": "8.4.0-beta1"
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "8.3.3"
|
||||
"version": "8.3.6"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"repository": "github:grafana/grafana",
|
||||
"scripts": {
|
||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "5.0.2",
|
||||
"@grafana/schema": "8.3.3",
|
||||
"@grafana/schema": "8.3.6",
|
||||
"@types/d3-interpolate": "^1.4.0",
|
||||
"d3-interpolate": "1.4.0",
|
||||
"date-fns": "2.21.3",
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface FeatureToggles {
|
||||
accesscontrol: boolean;
|
||||
tempoServiceGraph: boolean;
|
||||
tempoSearch: boolean;
|
||||
tempoBackendSearch: boolean;
|
||||
recordedQueries: boolean;
|
||||
newNavigation: boolean;
|
||||
fullRangeLogsVolume: boolean;
|
||||
|
||||
@@ -20,6 +20,17 @@ export enum Interval {
|
||||
Millisecond = 'millisecond',
|
||||
}
|
||||
|
||||
const UNITS = [
|
||||
Interval.Year,
|
||||
Interval.Month,
|
||||
Interval.Week,
|
||||
Interval.Day,
|
||||
Interval.Hour,
|
||||
Interval.Minute,
|
||||
Interval.Second,
|
||||
Interval.Millisecond,
|
||||
];
|
||||
|
||||
const INTERVALS_IN_SECONDS: IntervalsInSeconds = {
|
||||
[Interval.Year]: 31536000,
|
||||
[Interval.Month]: 2592000,
|
||||
@@ -206,17 +217,6 @@ export function toDuration(size: number, decimals: DecimalCount, timeScale: Inte
|
||||
return v;
|
||||
}
|
||||
|
||||
const units = [
|
||||
{ long: Interval.Year },
|
||||
{ long: Interval.Month },
|
||||
{ long: Interval.Week },
|
||||
{ long: Interval.Day },
|
||||
{ long: Interval.Hour },
|
||||
{ long: Interval.Minute },
|
||||
{ long: Interval.Second },
|
||||
{ long: Interval.Millisecond },
|
||||
];
|
||||
|
||||
// convert $size to milliseconds
|
||||
// intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
|
||||
size *= INTERVALS_IN_SECONDS[timeScale] * 1000;
|
||||
@@ -231,13 +231,13 @@ export function toDuration(size: number, decimals: DecimalCount, timeScale: Inte
|
||||
decimalsCount = decimals as number;
|
||||
}
|
||||
|
||||
for (let i = 0; i < units.length && decimalsCount >= 0; i++) {
|
||||
const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000;
|
||||
for (let i = 0; i < UNITS.length && decimalsCount >= 0; i++) {
|
||||
const interval = INTERVALS_IN_SECONDS[UNITS[i]] * 1000;
|
||||
const value = size / interval;
|
||||
if (value >= 1 || decrementDecimals) {
|
||||
decrementDecimals = true;
|
||||
const floor = Math.floor(value);
|
||||
const unit = units[i].long + (floor !== 1 ? 's' : '');
|
||||
const unit = UNITS[i] + (floor !== 1 ? 's' : '');
|
||||
strings.push(floor + ' ' + unit);
|
||||
size = size % interval;
|
||||
decimalsCount--;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e-selectors",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"description": "Grafana End-to-End Test Selectors Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -49,7 +49,7 @@
|
||||
"@babel/core": "7.14.6",
|
||||
"@babel/preset-env": "7.14.7",
|
||||
"@cypress/webpack-preprocessor": "5.9.1",
|
||||
"@grafana/e2e-selectors": "8.3.3",
|
||||
"@grafana/e2e-selectors": "8.3.6",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"babel-loader": "8.2.2",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -23,9 +23,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.1.3",
|
||||
"@grafana/data": "8.3.3",
|
||||
"@grafana/e2e-selectors": "8.3.3",
|
||||
"@grafana/ui": "8.3.3",
|
||||
"@grafana/data": "8.3.6",
|
||||
"@grafana/e2e-selectors": "8.3.6",
|
||||
"@grafana/ui": "8.3.6",
|
||||
"@sentry/browser": "5.25.0",
|
||||
"history": "4.10.1",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -66,6 +66,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
trimDefaults: false,
|
||||
tempoServiceGraph: false,
|
||||
tempoSearch: false,
|
||||
tempoBackendSearch: false,
|
||||
recordedQueries: false,
|
||||
newNavigation: false,
|
||||
fullRangeLogsVolume: false,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/schema",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"description": "Grafana Schema Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -28,10 +28,10 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.13.14",
|
||||
"@babel/preset-env": "7.13.12",
|
||||
"@grafana/data": "8.3.3",
|
||||
"@grafana/data": "8.3.6",
|
||||
"@grafana/eslint-config": "2.5.1",
|
||||
"@grafana/tsconfig": "^1.0.0-rc1",
|
||||
"@grafana/ui": "8.3.3",
|
||||
"@grafana/ui": "8.3.6",
|
||||
"@jest/core": "26.6.3",
|
||||
"@rushstack/eslint-patch": "1.0.6",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/ui",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -33,9 +33,9 @@
|
||||
"@emotion/css": "11.1.3",
|
||||
"@emotion/react": "11.1.5",
|
||||
"@grafana/aws-sdk": "0.0.3",
|
||||
"@grafana/data": "8.3.3",
|
||||
"@grafana/e2e-selectors": "8.3.3",
|
||||
"@grafana/schema": "8.3.3",
|
||||
"@grafana/data": "8.3.6",
|
||||
"@grafana/e2e-selectors": "8.3.6",
|
||||
"@grafana/schema": "8.3.6",
|
||||
"@grafana/slate-react": "0.22.10-grafana",
|
||||
"@monaco-editor/react": "4.2.2",
|
||||
"@popperjs/core": "2.5.4",
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('GraphNG utils', () => {
|
||||
timeZone: DefaultTimeZone,
|
||||
getTimeRange: getDefaultTimeRange,
|
||||
eventBus: new EventBusSrv(),
|
||||
sync: DashboardCursorSync.Tooltip,
|
||||
sync: () => DashboardCursorSync.Tooltip,
|
||||
allFrames: [frame!],
|
||||
}).getConfig();
|
||||
expect(result).toMatchSnapshot();
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface PanelContext {
|
||||
eventBus: EventBus;
|
||||
|
||||
/** Dashboard panels sync */
|
||||
sync?: DashboardCursorSync;
|
||||
sync?: () => DashboardCursorSync;
|
||||
|
||||
/** Information on what the outer container is */
|
||||
app?: CoreApp | 'string';
|
||||
|
||||
@@ -9,30 +9,14 @@ import { EmptyCell, FooterCell } from './FooterCell';
|
||||
export interface FooterRowProps {
|
||||
totalColumnsWidth: number;
|
||||
footerGroups: HeaderGroup[];
|
||||
footerValues?: FooterItem[];
|
||||
footerValues: FooterItem[];
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const FooterRow = (props: FooterRowProps) => {
|
||||
const { totalColumnsWidth, footerGroups, footerValues } = props;
|
||||
const { totalColumnsWidth, footerGroups, height } = props;
|
||||
const e2eSelectorsTable = selectors.components.Panels.Visualization.Table;
|
||||
const tableStyles = useStyles2(getTableStyles);
|
||||
const EXTENDED_ROW_HEIGHT = 27;
|
||||
|
||||
if (!footerValues) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let length = 0;
|
||||
for (const fv of footerValues) {
|
||||
if (Array.isArray(fv) && fv.length > length) {
|
||||
length = fv.length;
|
||||
}
|
||||
}
|
||||
|
||||
let height: number | undefined;
|
||||
if (footerValues && length > 1) {
|
||||
height = EXTENDED_ROW_HEIGHT * length;
|
||||
}
|
||||
|
||||
return (
|
||||
<table
|
||||
|
||||
@@ -127,7 +127,30 @@ export const Table: FC<Props> = memo((props: Props) => {
|
||||
footerValues,
|
||||
showTypeIcons,
|
||||
} = props;
|
||||
|
||||
const tableStyles = useStyles2(getTableStyles);
|
||||
const headerHeight = noHeader ? 0 : tableStyles.cellHeight;
|
||||
|
||||
const footerHeight = useMemo(() => {
|
||||
const EXTENDED_ROW_HEIGHT = 33;
|
||||
let length = 0;
|
||||
|
||||
if (!footerValues) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const fv of footerValues) {
|
||||
if (Array.isArray(fv) && fv.length > length) {
|
||||
length = fv.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (length > 1) {
|
||||
return EXTENDED_ROW_HEIGHT * length;
|
||||
}
|
||||
|
||||
return EXTENDED_ROW_HEIGHT;
|
||||
}, [footerValues]);
|
||||
|
||||
// React table data array. This data acts just like a dummy array to let react-table know how many rows exist
|
||||
// The cells use the field to look up values
|
||||
@@ -197,7 +220,7 @@ export const Table: FC<Props> = memo((props: Props) => {
|
||||
[onCellFilterAdded, prepareRow, rows, tableStyles]
|
||||
);
|
||||
|
||||
const headerHeight = noHeader ? 0 : tableStyles.cellHeight;
|
||||
const listHeight = height - (headerHeight + footerHeight);
|
||||
|
||||
return (
|
||||
<div {...getTableProps()} className={tableStyles.table} aria-label={ariaLabel} role="table">
|
||||
@@ -206,7 +229,7 @@ export const Table: FC<Props> = memo((props: Props) => {
|
||||
{!noHeader && <HeaderRow data={data} headerGroups={headerGroups} showTypeIcons={showTypeIcons} />}
|
||||
{rows.length > 0 ? (
|
||||
<FixedSizeList
|
||||
height={height - headerHeight}
|
||||
height={listHeight}
|
||||
itemCount={rows.length}
|
||||
itemSize={tableStyles.rowHeight}
|
||||
width={'100%'}
|
||||
@@ -219,7 +242,14 @@ export const Table: FC<Props> = memo((props: Props) => {
|
||||
No data
|
||||
</div>
|
||||
)}
|
||||
<FooterRow footerValues={footerValues} footerGroups={footerGroups} totalColumnsWidth={totalColumnsWidth} />
|
||||
{footerValues && (
|
||||
<FooterRow
|
||||
height={footerHeight}
|
||||
footerValues={footerValues}
|
||||
footerGroups={footerGroups}
|
||||
totalColumnsWidth={totalColumnsWidth}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
|
||||
@@ -131,6 +131,10 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
&:hover {
|
||||
background-color: ${rowHoverBg};
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
`,
|
||||
imageCell: css`
|
||||
height: 100%;
|
||||
|
||||
@@ -18,7 +18,7 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
panelContext: PanelContext = {} as PanelContext;
|
||||
|
||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
const { eventBus, sync } = this.context;
|
||||
const { eventBus, sync } = this.context as PanelContext;
|
||||
const { theme, timeZone, legend, renderers, tweakAxis, tweakScale } = this.props;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
|
||||
@@ -36,7 +36,10 @@ const defaultConfig: GraphFieldConfig = {
|
||||
axisPlacement: AxisPlacement.Auto,
|
||||
};
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursorSync; legend?: VizLegendOptions }> = ({
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
sync?: () => DashboardCursorSync;
|
||||
legend?: VizLegendOptions;
|
||||
}> = ({
|
||||
frame,
|
||||
theme,
|
||||
timeZone,
|
||||
@@ -250,9 +253,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
series: [t, b],
|
||||
fill: undefined, // using null will have the band use fill options from `t`
|
||||
});
|
||||
}
|
||||
if (!fillOpacity) {
|
||||
fillOpacity = 35; // default from flot
|
||||
|
||||
if (!fillOpacity) {
|
||||
fillOpacity = 35; // default from flot
|
||||
}
|
||||
} else {
|
||||
fillOpacity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,7 +392,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
},
|
||||
};
|
||||
|
||||
if (sync !== DashboardCursorSync.Off) {
|
||||
if (sync && sync() !== DashboardCursorSync.Off) {
|
||||
const payload: DataHoverPayload = {
|
||||
point: {
|
||||
[xScaleKey]: null,
|
||||
@@ -399,6 +405,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
key: '__global_',
|
||||
filters: {
|
||||
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
|
||||
if (sync && sync() === DashboardCursorSync.Off) {
|
||||
return false;
|
||||
}
|
||||
|
||||
payload.rowIndex = dataIdx;
|
||||
if (x < 0 && y < 0) {
|
||||
payload.point[xScaleUnit] = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GrafanaTheme2, ThresholdsConfig, ThresholdsMode } from '@grafana/data';
|
||||
import { GrafanaTheme2, Threshold, ThresholdsConfig, ThresholdsMode } from '@grafana/data';
|
||||
import { GraphThresholdsStyleConfig, GraphTresholdsStyleMode } from '@grafana/schema';
|
||||
import { getGradientRange, scaleGradient } from './gradientFills';
|
||||
import tinycolor from 'tinycolor2';
|
||||
@@ -16,9 +16,79 @@ export interface UPlotThresholdOptions {
|
||||
}
|
||||
|
||||
export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
||||
function addLines(u: uPlot, steps: Threshold[], theme: GrafanaTheme2, xMin: number, xMax: number, yScaleKey: string) {
|
||||
let ctx = u.ctx;
|
||||
|
||||
// Thresholds below a transparent threshold is treated like "less than", and line drawn previous threshold
|
||||
let transparentIndex = 0;
|
||||
|
||||
for (let idx = 0; idx < steps.length; idx++) {
|
||||
const step = steps[idx];
|
||||
if (step.color === 'transparent') {
|
||||
transparentIndex = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.lineWidth = 2;
|
||||
|
||||
// Ignore the base -Infinity threshold by always starting on index 1
|
||||
for (let idx = 1; idx < steps.length; idx++) {
|
||||
const step = steps[idx];
|
||||
let color: tinycolor.Instance;
|
||||
|
||||
// if we are below a transparent index treat this a less then threshold, use previous thresholds color
|
||||
if (transparentIndex >= idx && idx > 0) {
|
||||
color = tinycolor(theme.visualization.getColorByName(steps[idx - 1].color));
|
||||
} else {
|
||||
color = tinycolor(theme.visualization.getColorByName(step.color));
|
||||
}
|
||||
|
||||
// Unless alpha specififed set to default value
|
||||
if (color.getAlpha() === 1) {
|
||||
color.setAlpha(0.7);
|
||||
}
|
||||
|
||||
let x0 = Math.round(u.valToPos(xMin!, 'x', true));
|
||||
let y0 = Math.round(u.valToPos(step.value, yScaleKey, true));
|
||||
let x1 = Math.round(u.valToPos(xMax!, 'x', true));
|
||||
let y1 = Math.round(u.valToPos(step.value, yScaleKey, true));
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x0, y0);
|
||||
ctx.lineTo(x1, y1);
|
||||
|
||||
ctx.strokeStyle = color.toString();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function addAreas(u: uPlot, steps: Threshold[], theme: GrafanaTheme2) {
|
||||
let ctx = u.ctx;
|
||||
|
||||
let grd = scaleGradient(
|
||||
u,
|
||||
u.series[1].scale!,
|
||||
steps.map((step) => {
|
||||
let color = tinycolor(theme.visualization.getColorByName(step.color));
|
||||
|
||||
if (color.getAlpha() === 1) {
|
||||
color.setAlpha(0.15);
|
||||
}
|
||||
|
||||
return [step.value, color.toString()];
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
ctx.fillStyle = grd;
|
||||
ctx.fillRect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||
}
|
||||
|
||||
const { scaleKey, thresholds, theme, config, hardMin, hardMax, softMin, softMax } = options;
|
||||
|
||||
return (u: uPlot) => {
|
||||
const ctx = u.ctx;
|
||||
const { scaleKey, thresholds, theme, config, hardMin, hardMax, softMin, softMax } = options;
|
||||
const { min: xMin, max: xMax } = u.scales.x;
|
||||
const { min: yMin, max: yMax } = u.scales[scaleKey];
|
||||
|
||||
@@ -38,82 +108,20 @@ export function getThresholdsDrawHook(options: UPlotThresholdOptions) {
|
||||
}));
|
||||
}
|
||||
|
||||
function addLines() {
|
||||
// Thresholds below a transparent threshold is treated like "less than", and line drawn previous threshold
|
||||
let transparentIndex = 0;
|
||||
|
||||
for (let idx = 0; idx < steps.length; idx++) {
|
||||
const step = steps[idx];
|
||||
if (step.color === 'transparent') {
|
||||
transparentIndex = idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore the base -Infinity threshold by always starting on index 1
|
||||
for (let idx = 1; idx < steps.length; idx++) {
|
||||
const step = steps[idx];
|
||||
let color: tinycolor.Instance;
|
||||
|
||||
// if we are below a transparent index treat this a less then threshold, use previous thresholds color
|
||||
if (transparentIndex >= idx && idx > 0) {
|
||||
color = tinycolor(theme.visualization.getColorByName(steps[idx - 1].color));
|
||||
} else {
|
||||
color = tinycolor(theme.visualization.getColorByName(step.color));
|
||||
}
|
||||
|
||||
// Unless alpha specififed set to default value
|
||||
if (color.getAlpha() === 1) {
|
||||
color.setAlpha(0.7);
|
||||
}
|
||||
|
||||
let x0 = Math.round(u.valToPos(xMin!, 'x', true));
|
||||
let y0 = Math.round(u.valToPos(step.value, scaleKey, true));
|
||||
let x1 = Math.round(u.valToPos(xMax!, 'x', true));
|
||||
let y1 = Math.round(u.valToPos(step.value, scaleKey, true));
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = color.toString();
|
||||
ctx.moveTo(x0, y0);
|
||||
ctx.lineTo(x1, y1);
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
function addAreas() {
|
||||
let grd = scaleGradient(
|
||||
u,
|
||||
u.series[1].scale!,
|
||||
steps.map((step) => {
|
||||
let color = tinycolor(theme.visualization.getColorByName(step.color));
|
||||
|
||||
if (color.getAlpha() === 1) {
|
||||
color.setAlpha(0.15);
|
||||
}
|
||||
|
||||
return [step.value, color.toString()];
|
||||
}),
|
||||
true
|
||||
);
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = grd;
|
||||
ctx.fillRect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
|
||||
ctx.restore();
|
||||
}
|
||||
ctx.save();
|
||||
|
||||
switch (config.mode) {
|
||||
case GraphTresholdsStyleMode.Line:
|
||||
addLines();
|
||||
addLines(u, steps, theme, xMin, xMax, scaleKey);
|
||||
break;
|
||||
case GraphTresholdsStyleMode.Area:
|
||||
addAreas();
|
||||
addAreas(u, steps, theme);
|
||||
break;
|
||||
case GraphTresholdsStyleMode.LineAndArea:
|
||||
addLines();
|
||||
addAreas();
|
||||
addAreas(u, steps, theme);
|
||||
addLines(u, steps, theme, xMin, xMax, scaleKey);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ interface TooltipPluginProps {
|
||||
data: DataFrame;
|
||||
config: UPlotConfigBuilder;
|
||||
mode?: TooltipDisplayMode;
|
||||
sync?: DashboardCursorSync;
|
||||
sync?: () => DashboardCursorSync;
|
||||
// Allows custom tooltip content rendering. Exposes aligned data frame with relevant indexes for data inspection
|
||||
// Use field.state.origin indexes from alignedData frame field to get access to original data frame and field index.
|
||||
renderTooltip?: (alignedFrame: DataFrame, seriesIdx: number | null, datapointIdx: number | null) => React.ReactNode;
|
||||
@@ -91,7 +91,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
||||
u.over.addEventListener('mouseleave', plotMouseLeave);
|
||||
u.over.addEventListener('mouseenter', plotMouseEnter);
|
||||
|
||||
if (sync === DashboardCursorSync.Crosshair) {
|
||||
if (sync && sync() === DashboardCursorSync.Crosshair) {
|
||||
u.root.classList.add('shared-crosshair');
|
||||
}
|
||||
});
|
||||
@@ -162,7 +162,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
||||
};
|
||||
}, [config, setCoords, setIsActive, setFocusedPointIdx, setFocusedPointIdxs]);
|
||||
|
||||
if (focusedPointIdx === null || (!isActive && sync === DashboardCursorSync.Crosshair)) {
|
||||
if (focusedPointIdx === null || (!isActive && sync && sync() === DashboardCursorSync.Crosshair)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jaegertracing/jaeger-ui-components",
|
||||
"version": "8.3.3",
|
||||
"version": "8.3.6",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -30,8 +30,8 @@
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.1.3",
|
||||
"@emotion/react": "11.1.5",
|
||||
"@grafana/data": "8.3.3",
|
||||
"@grafana/ui": "8.3.3",
|
||||
"@grafana/data": "8.3.6",
|
||||
"@grafana/ui": "8.3.6",
|
||||
"chance": "^1.0.10",
|
||||
"classnames": "^2.2.5",
|
||||
"combokeys": "^3.0.0",
|
||||
|
||||
@@ -29,7 +29,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
reqEditorRole := middleware.ReqEditorRole
|
||||
reqOrgAdmin := middleware.ReqOrgAdmin
|
||||
reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin
|
||||
reqCanAccessTeams := middleware.AdminOrFeatureEnabled(hs.Cfg.EditorsCanAdmin)
|
||||
reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin)
|
||||
reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg)
|
||||
redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg)
|
||||
authorize := acmiddleware.Middleware(hs.AccessControl)
|
||||
|
||||
@@ -252,8 +252,8 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"commit": commit,
|
||||
"buildstamp": buildstamp,
|
||||
"edition": hs.License.Edition(),
|
||||
"latestVersion": hs.updateChecker.LatestGrafanaVersion(),
|
||||
"hasUpdate": hs.updateChecker.GrafanaUpdateAvailable(),
|
||||
"latestVersion": hs.grafanaUpdateChecker.LatestVersion(),
|
||||
"hasUpdate": hs.grafanaUpdateChecker.UpdateAvailable(),
|
||||
"env": setting.Env,
|
||||
"isEnterprise": hs.License.HasValidLicense(),
|
||||
},
|
||||
|
||||
@@ -44,10 +44,10 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*web.Mux, *HTTPServer
|
||||
Cfg: cfg,
|
||||
RendererPluginManager: &fakeRendererManager{},
|
||||
},
|
||||
SQLStore: sqlStore,
|
||||
pluginStore: &fakePluginStore{},
|
||||
updateChecker: &updatechecker.Service{},
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
SQLStore: sqlStore,
|
||||
pluginStore: &fakePluginStore{},
|
||||
grafanaUpdateChecker: &updatechecker.GrafanaService{},
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
}
|
||||
|
||||
m := web.New()
|
||||
|
||||
@@ -114,7 +114,8 @@ type HTTPServer struct {
|
||||
cleanUpService *cleanup.CleanUpService
|
||||
tracingService *tracing.TracingService
|
||||
internalMetricsSvc *metrics.InternalMetricsService
|
||||
updateChecker *updatechecker.Service
|
||||
grafanaUpdateChecker *updatechecker.GrafanaService
|
||||
pluginsUpdateChecker *updatechecker.PluginsService
|
||||
searchUsersService searchusers.Service
|
||||
expressionService *expr.Service
|
||||
}
|
||||
@@ -142,7 +143,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
notificationService *notifications.NotificationService, tracingService *tracing.TracingService,
|
||||
internalMetricsSvc *metrics.InternalMetricsService, quotaService *quota.QuotaService,
|
||||
socialService social.Service, oauthTokenService oauthtoken.OAuthTokenService,
|
||||
encryptionService encryption.Internal, updateChecker *updatechecker.Service, searchUsersService searchusers.Service,
|
||||
grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService,
|
||||
encryptionService encryption.Internal, searchUsersService searchusers.Service,
|
||||
dataSourcesService *datasources.Service, secretsService secrets.Service, expressionService *expr.Service) (*HTTPServer, error) {
|
||||
web.Env = cfg.Env
|
||||
m := web.New()
|
||||
@@ -164,7 +166,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
pluginStaticRouteResolver: pluginStaticRouteResolver,
|
||||
pluginDashboardManager: pluginDashboardManager,
|
||||
pluginErrorResolver: pluginErrorResolver,
|
||||
updateChecker: updateChecker,
|
||||
grafanaUpdateChecker: grafanaUpdateChecker,
|
||||
pluginsUpdateChecker: pluginsUpdateChecker,
|
||||
SettingsProvider: settingsProvider,
|
||||
DataSourceCache: dataSourceCache,
|
||||
AuthTokenService: userTokenService,
|
||||
@@ -419,6 +422,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
||||
}
|
||||
|
||||
m.Use(middleware.Recovery(hs.Cfg))
|
||||
m.UseMiddleware(middleware.CSRF(hs.Cfg.LoginCookieName))
|
||||
|
||||
hs.mapStatic(m, hs.Cfg.StaticRootPath, "build", "public/build")
|
||||
hs.mapStatic(m, hs.Cfg.StaticRootPath, "", "public")
|
||||
|
||||
@@ -469,7 +469,7 @@ func (hs *HTTPServer) buildCreateNavLinks(c *models.ReqContext) []*dtos.NavLink
|
||||
_, uaIsDisabledForOrg := hs.Cfg.UnifiedAlerting.DisabledOrgs[c.OrgId]
|
||||
uaVisibleForOrg := hs.Cfg.UnifiedAlerting.IsEnabled() && !uaIsDisabledForOrg
|
||||
|
||||
if setting.AlertingEnabled != nil && *setting.AlertingEnabled || uaVisibleForOrg {
|
||||
if uaVisibleForOrg {
|
||||
children = append(children, &dtos.NavLink{
|
||||
Text: "Alert rule", SubTitle: "Create an alert rule", Id: "alert",
|
||||
Icon: "bell", Url: hs.Cfg.AppSubURL + "/alerting/new",
|
||||
@@ -589,8 +589,8 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
||||
GoogleTagManagerId: setting.GoogleTagManagerId,
|
||||
BuildVersion: setting.BuildVersion,
|
||||
BuildCommit: setting.BuildCommit,
|
||||
NewGrafanaVersion: hs.updateChecker.LatestGrafanaVersion(),
|
||||
NewGrafanaVersionExists: hs.updateChecker.GrafanaUpdateAvailable(),
|
||||
NewGrafanaVersion: hs.grafanaUpdateChecker.LatestVersion(),
|
||||
NewGrafanaVersionExists: hs.grafanaUpdateChecker.UpdateAvailable(),
|
||||
AppName: setting.ApplicationName,
|
||||
AppNameBodyClass: getAppNameBodyClass(hs.License.HasValidLicense()),
|
||||
FavIcon: "public/img/fav32.png",
|
||||
|
||||
@@ -115,6 +115,7 @@ func setup() *testContext {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
sc,
|
||||
nil,
|
||||
)
|
||||
|
||||
@@ -54,6 +54,7 @@ func (t *handleResponseTransport) RoundTrip(req *http.Request) (*http.Response,
|
||||
return nil, err
|
||||
}
|
||||
res.Header.Del("Set-Cookie")
|
||||
proxyutil.SetProxyResponseHeaders(res.Header)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -663,6 +663,20 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
|
||||
assert.Equal(t, "important_cookie=important_value", proxy.ctx.Resp.Header().Get("Set-Cookie"))
|
||||
})
|
||||
|
||||
t.Run("When response should set Content-Security-Policy header", func(t *testing.T) {
|
||||
ctx, ds := setUp(t)
|
||||
var routes []*plugins.Route
|
||||
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
|
||||
dsService := datasources.ProvideService(bus.New(), nil, secretsService)
|
||||
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService)
|
||||
require.NoError(t, err)
|
||||
|
||||
proxy.HandleRequest()
|
||||
|
||||
require.NoError(t, writeErr)
|
||||
assert.Equal(t, "sandbox", proxy.ctx.Resp.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
|
||||
t.Run("Data source returns status code 401", func(t *testing.T) {
|
||||
ctx, ds := setUp(t, setUpCfg{
|
||||
writeCb: func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -83,5 +83,11 @@ func NewApiPluginProxy(ctx *models.ReqContext, proxyPath string, route *plugins.
|
||||
}
|
||||
}
|
||||
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyResponse}
|
||||
}
|
||||
|
||||
func modifyResponse(resp *http.Response) error {
|
||||
proxyutil.SetProxyResponseHeaders(resp.Header)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/macaron.v1"
|
||||
@@ -245,6 +247,41 @@ func TestPluginProxy(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `{ "url": "https://dynamic.grafana.com", "secret": "123" }`, string(content))
|
||||
})
|
||||
|
||||
t.Run("When proxying a request should set expected response headers", func(t *testing.T) {
|
||||
requestHandled := false
|
||||
backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
_, _ = w.Write([]byte("I am the backend"))
|
||||
requestHandled = true
|
||||
}))
|
||||
t.Cleanup(backendServer.Close)
|
||||
|
||||
responseWriter := web.NewResponseWriter("GET", httptest.NewRecorder())
|
||||
|
||||
route := &plugins.Route{
|
||||
Path: "/",
|
||||
URL: backendServer.URL,
|
||||
}
|
||||
|
||||
ctx := &models.ReqContext{
|
||||
SignedInUser: &models.SignedInUser{},
|
||||
Context: &web.Context{
|
||||
Req: httptest.NewRequest("GET", "/", nil),
|
||||
Resp: responseWriter,
|
||||
},
|
||||
}
|
||||
proxy := NewApiPluginProxy(ctx, "", route, "", &setting.Cfg{}, secretsService)
|
||||
proxy.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
|
||||
for {
|
||||
if requestHandled {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.Equal(t, "sandbox", ctx.Resp.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
}
|
||||
|
||||
// getPluginProxiedRequest is a helper for easier setup of tests based on global config and ReqContext.
|
||||
|
||||
@@ -69,8 +69,6 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
|
||||
Category: pluginDef.Category,
|
||||
Info: pluginDef.Info,
|
||||
Dependencies: pluginDef.Dependencies,
|
||||
LatestVersion: pluginDef.GrafanaComVersion,
|
||||
HasUpdate: pluginDef.GrafanaComHasUpdate,
|
||||
DefaultNavUrl: pluginDef.DefaultNavURL,
|
||||
State: pluginDef.State,
|
||||
Signature: pluginDef.Signature,
|
||||
@@ -78,6 +76,12 @@ func (hs *HTTPServer) GetPluginList(c *models.ReqContext) response.Response {
|
||||
SignatureOrg: pluginDef.SignatureOrg,
|
||||
}
|
||||
|
||||
update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), pluginDef.ID)
|
||||
if exists {
|
||||
listItem.LatestVersion = update
|
||||
listItem.HasUpdate = true
|
||||
}
|
||||
|
||||
if pluginSetting, exists := pluginSettingsMap[pluginDef.ID]; exists {
|
||||
listItem.Enabled = pluginSetting.Enabled
|
||||
listItem.Pinned = pluginSetting.Pinned
|
||||
@@ -122,8 +126,6 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
|
||||
BaseUrl: plugin.BaseURL,
|
||||
Module: plugin.Module,
|
||||
DefaultNavUrl: plugin.DefaultNavURL,
|
||||
LatestVersion: plugin.GrafanaComVersion,
|
||||
HasUpdate: plugin.GrafanaComHasUpdate,
|
||||
State: plugin.State,
|
||||
Signature: plugin.Signature,
|
||||
SignatureType: plugin.SignatureType,
|
||||
@@ -146,6 +148,12 @@ func (hs *HTTPServer) GetPluginSettingByID(c *models.ReqContext) response.Respon
|
||||
dto.JsonData = query.Result.JsonData
|
||||
}
|
||||
|
||||
update, exists := hs.pluginsUpdateChecker.HasUpdate(c.Req.Context(), plugin.ID)
|
||||
if exists {
|
||||
dto.LatestVersion = update
|
||||
dto.HasUpdate = true
|
||||
}
|
||||
|
||||
return response.JSON(200, dto)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
@@ -10,6 +12,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/teamguardian"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// POST /api/teams
|
||||
@@ -95,16 +98,11 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
page = 1
|
||||
}
|
||||
|
||||
var userIdFilter int64
|
||||
if hs.Cfg.EditorsCanAdmin && c.OrgRole != models.ROLE_ADMIN {
|
||||
userIdFilter = c.SignedInUser.UserId
|
||||
}
|
||||
|
||||
query := models.SearchTeamsQuery{
|
||||
OrgId: c.OrgId,
|
||||
Query: c.Query("query"),
|
||||
Name: c.Query("name"),
|
||||
UserIdFilter: userIdFilter,
|
||||
UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
SignedInUser: c.SignedInUser,
|
||||
@@ -125,13 +123,32 @@ func (hs *HTTPServer) SearchTeams(c *models.ReqContext) response.Response {
|
||||
return response.JSON(200, query.Result)
|
||||
}
|
||||
|
||||
// UserFilter returns the user ID used in a filter when querying a team
|
||||
// 1. If the user is a viewer or editor, this will return the user's ID.
|
||||
// 2. If EditorsCanAdmin is enabled and the user is an editor, this will return models.FilterIgnoreUser (0)
|
||||
// 3. If the user is an admin, this will return models.FilterIgnoreUser (0)
|
||||
func userFilter(editorsCanAdmin bool, c *models.ReqContext) int64 {
|
||||
userIdFilter := c.SignedInUser.UserId
|
||||
if (editorsCanAdmin && c.OrgRole == models.ROLE_EDITOR) || c.OrgRole == models.ROLE_ADMIN {
|
||||
userIdFilter = models.FilterIgnoreUser
|
||||
}
|
||||
|
||||
return userIdFilter
|
||||
}
|
||||
|
||||
// GET /api/teams/:teamId
|
||||
func (hs *HTTPServer) GetTeamByID(c *models.ReqContext) response.Response {
|
||||
teamId, err := strconv.ParseInt(web.Params(c.Req)[":teamId"], 10, 64)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
|
||||
}
|
||||
|
||||
query := models.GetTeamByIdQuery{
|
||||
OrgId: c.OrgId,
|
||||
Id: c.ParamsInt64(":teamId"),
|
||||
Id: teamId,
|
||||
SignedInUser: c.SignedInUser,
|
||||
HiddenUsers: hs.Cfg.HiddenUsers,
|
||||
UserIdFilter: userFilter(hs.Cfg.EditorsCanAdmin, c),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil {
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
func (hs *HTTPServer) GetTeamMembers(c *models.ReqContext) response.Response {
|
||||
query := models.GetTeamMembersQuery{OrgId: c.OrgId, TeamId: c.ParamsInt64(":teamId")}
|
||||
|
||||
if err := teamguardian.CanAdmin(hs.Bus, query.OrgId, query.TeamId, c.SignedInUser); err != nil {
|
||||
return response.Error(403, "Not allowed to list team members", err)
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil {
|
||||
return response.Error(500, "Failed to get Team Members", err)
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package macaron
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
@@ -11,9 +13,16 @@ import (
|
||||
// Bind deserializes JSON payload from the request
|
||||
func Bind(req *http.Request, v interface{}) error {
|
||||
if req.Body != nil {
|
||||
defer req.Body.Close()
|
||||
err := json.NewDecoder(req.Body).Decode(v)
|
||||
if err != nil && err != io.EOF {
|
||||
m, _, err := mime.ParseMediaType(req.Header.Get("Content-type"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m != "application/json" {
|
||||
return errors.New("bad content type")
|
||||
}
|
||||
defer func() { _ = req.Body.Close() }()
|
||||
err = json.NewDecoder(req.Body).Decode(v)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -29,7 +38,7 @@ func validate(obj interface{}) error {
|
||||
if validator, ok := obj.(Validator); ok {
|
||||
return validator.Validate()
|
||||
}
|
||||
// Otherwise, use relfection to match `binding:"Required"` struct field tags.
|
||||
// Otherwise, use reflection to match `binding:"Required"` struct field tags.
|
||||
// Resolve all pointers and interfaces, until we get a concrete type.
|
||||
t := reflect.TypeOf(obj)
|
||||
v := reflect.ValueOf(obj)
|
||||
@@ -69,6 +78,7 @@ func validate(obj interface{}) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default: // ignore
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -77,13 +77,17 @@ func (ctx *Context) run() {
|
||||
// RemoteAddr returns more real IP address.
|
||||
func (ctx *Context) RemoteAddr() string {
|
||||
addr := ctx.Req.Header.Get("X-Real-IP")
|
||||
|
||||
if len(addr) == 0 {
|
||||
addr = ctx.Req.Header.Get("X-Forwarded-For")
|
||||
if addr == "" {
|
||||
addr = ctx.Req.RemoteAddr
|
||||
if i := strings.LastIndex(addr, ":"); i > -1 {
|
||||
addr = addr[:i]
|
||||
}
|
||||
// X-Forwarded-For may contain multiple IP addresses, separated by
|
||||
// commas.
|
||||
addr = strings.TrimSpace(strings.Split(ctx.Req.Header.Get("X-Forwarded-For"), ",")[0])
|
||||
}
|
||||
|
||||
if len(addr) == 0 {
|
||||
addr = ctx.Req.RemoteAddr
|
||||
if i := strings.LastIndex(addr, ":"); i > -1 {
|
||||
addr = addr[:i]
|
||||
}
|
||||
}
|
||||
return addr
|
||||
|
||||
@@ -128,20 +128,22 @@ func Auth(options *AuthOptions) web.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
// AdminOrFeatureEnabled creates a middleware that allows access
|
||||
// if the signed in user is either an Org Admin or if the
|
||||
// feature flag is enabled.
|
||||
// AdminOrEditorAndFeatureEnabled creates a middleware that allows
|
||||
// access if the signed in user is either an Org Admin or if they
|
||||
// are an Org Editor and the feature flag is enabled.
|
||||
// Intended for when feature flags open up access to APIs that
|
||||
// are otherwise only available to admins.
|
||||
func AdminOrFeatureEnabled(enabled bool) web.Handler {
|
||||
func AdminOrEditorAndFeatureEnabled(enabled bool) web.Handler {
|
||||
return func(c *models.ReqContext) {
|
||||
if c.OrgRole == models.ROLE_ADMIN {
|
||||
return
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
accessForbidden(c)
|
||||
if c.OrgRole == models.ROLE_EDITOR && enabled {
|
||||
return
|
||||
}
|
||||
|
||||
accessForbidden(c)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
39
pkg/middleware/csrf.go
Normal file
39
pkg/middleware/csrf.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CSRF(loginCookieName string) func(http.Handler) http.Handler {
|
||||
// As per RFC 7231/4.2.2 these methods are idempotent:
|
||||
// (GET is excluded because it may have side effects in some APIs)
|
||||
safeMethods := []string{"HEAD", "OPTIONS", "TRACE"}
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// If request has no login cookie - skip CSRF checks
|
||||
if _, err := r.Cookie(loginCookieName); errors.Is(err, http.ErrNoCookie) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// Skip CSRF checks for "safe" methods
|
||||
for _, method := range safeMethods {
|
||||
if r.Method == method {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Otherwise - verify that Origin matches the server origin
|
||||
host := strings.Split(r.Host, ":")[0]
|
||||
origin, err := url.Parse(r.Header.Get("Origin"))
|
||||
if err != nil || (origin.String() != "" && origin.Hostname() != host) {
|
||||
http.Error(w, "origin not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -55,8 +55,12 @@ type GetTeamByIdQuery struct {
|
||||
SignedInUser *SignedInUser
|
||||
HiddenUsers map[string]struct{}
|
||||
Result *TeamDTO
|
||||
UserIdFilter int64
|
||||
}
|
||||
|
||||
// FilterIgnoreUser is used in a get / search teams query when the caller does not want to filter teams by user ID / membership
|
||||
const FilterIgnoreUser int64 = 0
|
||||
|
||||
type GetTeamsByUserQuery struct {
|
||||
OrgId int64
|
||||
UserId int64 `json:"userId"`
|
||||
|
||||
@@ -118,24 +118,6 @@ func (m *PluginManager) init() error {
|
||||
}
|
||||
|
||||
func (m *PluginManager) Run(ctx context.Context) error {
|
||||
if m.cfg.CheckForUpdates {
|
||||
go func() {
|
||||
m.checkForUpdates()
|
||||
|
||||
ticker := time.NewTicker(time.Minute * 10)
|
||||
run := true
|
||||
|
||||
for run {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
m.checkForUpdates()
|
||||
case <-ctx.Done():
|
||||
run = false
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
m.shutdown(ctx)
|
||||
return ctx.Err()
|
||||
@@ -392,6 +374,8 @@ func flushStream(plugin backendplugin.Plugin, stream callResourceClientResponseS
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
proxyutil.SetProxyResponseHeaders(w.Header())
|
||||
w.WriteHeader(resp.Status)
|
||||
}
|
||||
|
||||
|
||||
@@ -447,7 +447,8 @@ func TestPluginManager_lifecycle_managed(t *testing.T) {
|
||||
ctx.pluginClient.CallResourceHandlerFunc = func(ctx context.Context,
|
||||
req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
||||
return sender.Send(&backend.CallResourceResponse{
|
||||
Status: http.StatusOK,
|
||||
Status: http.StatusOK,
|
||||
Headers: map[string][]string{},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -456,7 +457,13 @@ func TestPluginManager_lifecycle_managed(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
err = ctx.manager.callResourceInternal(w, req, backend.PluginContext{PluginID: testPluginID})
|
||||
require.NoError(t, err)
|
||||
for {
|
||||
if w.Flushed {
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
require.Equal(t, "sandbox", w.Header().Get("Content-Security-Policy"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
var (
|
||||
httpClient = http.Client{Timeout: 10 * time.Second}
|
||||
)
|
||||
|
||||
type gcomPlugin struct {
|
||||
Slug string `json:"slug"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func (m *PluginManager) checkForUpdates() {
|
||||
if !m.cfg.CheckForUpdates {
|
||||
return
|
||||
}
|
||||
|
||||
m.log.Debug("Checking for updates")
|
||||
|
||||
pluginIDs := m.pluginsEligibleForVersionCheck()
|
||||
resp, err := httpClient.Get("https://grafana.com/api/plugins/versioncheck?slugIn=" + strings.Join(pluginIDs, ",") + "&grafanaVersion=" + m.cfg.BuildVersion)
|
||||
if err != nil {
|
||||
m.log.Debug("Failed to get plugins repo from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
m.log.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
m.log.Debug("Update check failed, reading response from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var gcomPlugins []gcomPlugin
|
||||
err = json.Unmarshal(body, &gcomPlugins)
|
||||
if err != nil {
|
||||
m.log.Debug("Failed to unmarshal plugin repo, reading response from grafana.com", "error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, localP := range m.Plugins(context.TODO()) {
|
||||
for _, gcomP := range gcomPlugins {
|
||||
if gcomP.Slug == localP.ID {
|
||||
localP.GrafanaComVersion = gcomP.Version
|
||||
|
||||
plugVersion, err1 := version.NewVersion(localP.Info.Version)
|
||||
gplugVersion, err2 := version.NewVersion(gcomP.Version)
|
||||
|
||||
if err1 != nil || err2 != nil {
|
||||
localP.GrafanaComHasUpdate = localP.Info.Version != localP.GrafanaComVersion
|
||||
} else {
|
||||
localP.GrafanaComHasUpdate = plugVersion.LessThan(gplugVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PluginManager) pluginsEligibleForVersionCheck() []string {
|
||||
var result []string
|
||||
for _, p := range m.plugins() {
|
||||
if p.IsCorePlugin() {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, p.ID)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -32,10 +32,6 @@ type Plugin struct {
|
||||
SignedFiles PluginFiles
|
||||
SignatureError *SignatureError
|
||||
|
||||
// GCOM update checker fields
|
||||
GrafanaComVersion string
|
||||
GrafanaComHasUpdate bool
|
||||
|
||||
// SystemJS fields
|
||||
Module string
|
||||
BaseURL string
|
||||
@@ -63,10 +59,6 @@ type PluginDTO struct {
|
||||
SignedFiles PluginFiles
|
||||
SignatureError *SignatureError
|
||||
|
||||
// GCOM update checker fields
|
||||
GrafanaComVersion string
|
||||
GrafanaComHasUpdate bool
|
||||
|
||||
// SystemJS fields
|
||||
Module string
|
||||
BaseURL string
|
||||
@@ -315,22 +307,20 @@ func (p *Plugin) ToDTO() PluginDTO {
|
||||
c, _ := p.Client()
|
||||
|
||||
return PluginDTO{
|
||||
JSONData: p.JSONData,
|
||||
PluginDir: p.PluginDir,
|
||||
Class: p.Class,
|
||||
IncludedInAppID: p.IncludedInAppID,
|
||||
DefaultNavURL: p.DefaultNavURL,
|
||||
Pinned: p.Pinned,
|
||||
Signature: p.Signature,
|
||||
SignatureType: p.SignatureType,
|
||||
SignatureOrg: p.SignatureOrg,
|
||||
SignedFiles: p.SignedFiles,
|
||||
SignatureError: p.SignatureError,
|
||||
GrafanaComVersion: p.GrafanaComVersion,
|
||||
GrafanaComHasUpdate: p.GrafanaComHasUpdate,
|
||||
Module: p.Module,
|
||||
BaseURL: p.BaseURL,
|
||||
StreamHandler: c,
|
||||
JSONData: p.JSONData,
|
||||
PluginDir: p.PluginDir,
|
||||
Class: p.Class,
|
||||
IncludedInAppID: p.IncludedInAppID,
|
||||
DefaultNavURL: p.DefaultNavURL,
|
||||
Pinned: p.Pinned,
|
||||
Signature: p.Signature,
|
||||
SignatureType: p.SignatureType,
|
||||
SignatureOrg: p.SignatureOrg,
|
||||
SignedFiles: p.SignedFiles,
|
||||
SignatureError: p.SignatureError,
|
||||
Module: p.Module,
|
||||
BaseURL: p.BaseURL,
|
||||
StreamHandler: c,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ func ProvideBackgroundServiceRegistry(
|
||||
live *live.GrafanaLive, pushGateway *pushhttp.Gateway, notifications *notifications.NotificationService,
|
||||
rendering *rendering.RenderingService, tokenService models.UserTokenBackgroundService,
|
||||
provisioning *provisioning.ProvisioningServiceImpl, alerting *alerting.AlertEngine, pm *manager.PluginManager,
|
||||
metrics *metrics.InternalMetricsService, usageStats *uss.UsageStats, updateChecker *updatechecker.Service,
|
||||
metrics *metrics.InternalMetricsService, usageStats *uss.UsageStats,
|
||||
tracing *tracing.TracingService, remoteCache *remotecache.RemoteCache, secretsService *secretsManager.SecretsService,
|
||||
grafanaUpdateChecker *updatechecker.GrafanaService, pluginsUpdateChecker *updatechecker.PluginsService,
|
||||
// Need to make sure these are initialized, is there a better place to put them?
|
||||
_ *azuremonitor.Service, _ *cloudwatch.CloudWatchService, _ *elasticsearch.Service, _ *graphite.Service,
|
||||
_ *influxdb.Service, _ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service,
|
||||
@@ -66,7 +67,8 @@ func ProvideBackgroundServiceRegistry(
|
||||
provisioning,
|
||||
alerting,
|
||||
pm,
|
||||
updateChecker,
|
||||
grafanaUpdateChecker,
|
||||
pluginsUpdateChecker,
|
||||
metrics,
|
||||
usageStats,
|
||||
tracing,
|
||||
|
||||
@@ -96,7 +96,8 @@ var wireBasicSet = wire.NewSet(
|
||||
hooks.ProvideService,
|
||||
kvstore.ProvideService,
|
||||
localcache.ProvideService,
|
||||
updatechecker.ProvideService,
|
||||
updatechecker.ProvideGrafanaService,
|
||||
updatechecker.ProvidePluginsService,
|
||||
uss.ProvideService,
|
||||
wire.Bind(new(usagestats.Service), new(*uss.UsageStats)),
|
||||
manager.ProvideService,
|
||||
|
||||
@@ -128,14 +128,14 @@ func (s *AccessControlStore) setResourcePermissions(
|
||||
var permissions []accesscontrol.ResourcePermission
|
||||
|
||||
for action := range missing {
|
||||
p, err := createResourcePermission(sess, role.ID, action, cmd.Resource, cmd.ResourceID)
|
||||
p, err := s.createResourcePermission(sess, role.ID, action, cmd.Resource, cmd.ResourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
permissions = append(permissions, *p)
|
||||
}
|
||||
|
||||
keptPermissions, err := getManagedPermissions(sess, cmd.ResourceID, keep)
|
||||
keptPermissions, err := s.getManagedPermissions(sess, cmd.ResourceID, keep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -187,14 +187,14 @@ func (s *AccessControlStore) GetResourcesPermissions(ctx context.Context, orgID
|
||||
|
||||
err := s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var err error
|
||||
result, err = getResourcesPermissions(sess, orgID, query, false)
|
||||
result, err = s.getResourcesPermissions(sess, orgID, query, false)
|
||||
return err
|
||||
})
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource string, resourceID string) (*accesscontrol.ResourcePermission, error) {
|
||||
func (s *AccessControlStore) createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource string, resourceID string) (*accesscontrol.ResourcePermission, error) {
|
||||
permission := managedPermission(action, resource, resourceID)
|
||||
permission.RoleID = roleID
|
||||
permission.Created = time.Now()
|
||||
@@ -220,7 +220,7 @@ func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, re
|
||||
LEFT JOIN team_role tr ON r.id = tr.role_id
|
||||
LEFT JOIN team t ON tr.team_id = t.id
|
||||
LEFT JOIN user_role ur ON r.id = ur.role_id
|
||||
LEFT JOIN user u ON ur.user_id = u.id
|
||||
LEFT JOIN ` + s.sql.Dialect.Quote("user") + ` u ON ur.user_id = u.id
|
||||
WHERE p.id = ?
|
||||
`
|
||||
|
||||
@@ -232,7 +232,7 @@ func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, re
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query accesscontrol.GetResourcesPermissionsQuery, managed bool) ([]accesscontrol.ResourcePermission, error) {
|
||||
func (s *AccessControlStore) getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query accesscontrol.GetResourcesPermissionsQuery, managed bool) ([]accesscontrol.ResourcePermission, error) {
|
||||
result := make([]accesscontrol.ResourcePermission, 0)
|
||||
|
||||
if len(query.Actions) == 0 {
|
||||
@@ -284,16 +284,16 @@ func getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query access
|
||||
INNER JOIN role r ON p.role_id = r.id
|
||||
`
|
||||
userFrom := rawFrom + `
|
||||
INNER JOIN user_role ur ON r.id = ur.role_id
|
||||
INNER JOIN user u ON ur.user_id = u.id
|
||||
INNER JOIN user_role ur ON r.id = ur.role_id AND (ur.org_id = 0 OR ur.org_id = ?)
|
||||
INNER JOIN ` + s.sql.Dialect.Quote("user") + ` u ON ur.user_id = u.id
|
||||
`
|
||||
teamFrom := rawFrom + `
|
||||
INNER JOIN team_role tr ON r.id = tr.role_id
|
||||
INNER JOIN team_role tr ON r.id = tr.role_id AND (tr.org_id = 0 OR tr.org_id = ?)
|
||||
INNER JOIN team t ON tr.team_id = t.id
|
||||
`
|
||||
|
||||
builtinFrom := rawFrom + `
|
||||
INNER JOIN builtin_role br ON r.id = br.role_id
|
||||
INNER JOIN builtin_role br ON r.id = br.role_id AND (br.org_id = 0 OR br.org_id = ?)
|
||||
`
|
||||
where := `
|
||||
WHERE (r.org_id = ? OR r.org_id = 0)
|
||||
@@ -306,6 +306,7 @@ func getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query access
|
||||
}
|
||||
|
||||
args := []interface{}{
|
||||
orgID,
|
||||
orgID,
|
||||
getResourceAllScope(query.Resource),
|
||||
getResourceAllIDScope(query.Resource),
|
||||
@@ -448,12 +449,12 @@ func (s *AccessControlStore) getOrCreateManagedRole(sess *sqlstore.DBSession, or
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func getManagedPermissions(sess *sqlstore.DBSession, resourceID string, ids []int64) ([]accesscontrol.ResourcePermission, error) {
|
||||
func (s *AccessControlStore) getManagedPermissions(sess *sqlstore.DBSession, resourceID string, ids []int64) ([]accesscontrol.ResourcePermission, error) {
|
||||
var result []accesscontrol.ResourcePermission
|
||||
|
||||
if len(ids) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rawSql := `
|
||||
SELECT
|
||||
p.*,
|
||||
@@ -470,7 +471,7 @@ func getManagedPermissions(sess *sqlstore.DBSession, resourceID string, ids []in
|
||||
LEFT JOIN team_role tr ON r.id = tr.role_id
|
||||
LEFT JOIN team t ON tr.team_id = t.id
|
||||
LEFT JOIN user_role ur ON r.id = ur.role_id
|
||||
LEFT JOIN user u ON ur.user_id = u.id
|
||||
LEFT JOIN ` + s.sql.Dialect.Quote("user") + ` u ON ur.user_id = u.id
|
||||
WHERE p.id IN (?` + strings.Repeat(",?", len(ids)-1) + `)
|
||||
`
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ func (s *Implementation) GetExternalUserInfoByLogin(ctx context.Context, query *
|
||||
}
|
||||
|
||||
func (s *Implementation) GetAuthInfo(query *models.GetAuthInfoQuery) error {
|
||||
if query.UserId == 0 && query.AuthId == "" {
|
||||
return models.ErrUserNotFound
|
||||
}
|
||||
|
||||
userAuth := &models.UserAuth{
|
||||
UserId: query.UserId,
|
||||
AuthModule: query.AuthModule,
|
||||
|
||||
@@ -258,7 +258,7 @@ func ErrResp(status int, err error, msg string, args ...interface{}) *response.N
|
||||
if msg != "" {
|
||||
err = errors.WithMessagef(err, msg, args...)
|
||||
}
|
||||
return response.Error(status, "API error", err)
|
||||
return response.Error(status, err.Error(), err)
|
||||
}
|
||||
|
||||
// accessForbiddenResp creates a response of forbidden access.
|
||||
|
||||
@@ -45,10 +45,13 @@ type NGAlert struct {
|
||||
}
|
||||
|
||||
type Scheduler struct {
|
||||
Registerer prometheus.Registerer
|
||||
EvalTotal *prometheus.CounterVec
|
||||
EvalFailures *prometheus.CounterVec
|
||||
EvalDuration *prometheus.SummaryVec
|
||||
Registerer prometheus.Registerer
|
||||
BehindSeconds prometheus.Gauge
|
||||
EvalTotal *prometheus.CounterVec
|
||||
EvalFailures *prometheus.CounterVec
|
||||
EvalDuration *prometheus.SummaryVec
|
||||
GetAlertRulesDuration prometheus.Histogram
|
||||
SchedulePeriodicDuration prometheus.Histogram
|
||||
}
|
||||
|
||||
type MultiOrgAlertmanager struct {
|
||||
@@ -120,6 +123,12 @@ func (moa *MultiOrgAlertmanager) GetOrCreateOrgRegistry(id int64) prometheus.Reg
|
||||
func newSchedulerMetrics(r prometheus.Registerer) *Scheduler {
|
||||
return &Scheduler{
|
||||
Registerer: r,
|
||||
BehindSeconds: promauto.With(r).NewGauge(prometheus.GaugeOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "scheduler_behind_seconds",
|
||||
Help: "The total number of seconds the scheduler is behind.",
|
||||
}),
|
||||
// TODO: once rule groups support multiple rules, consider partitioning
|
||||
// on rule group as well as tenant, similar to loki|cortex.
|
||||
EvalTotal: promauto.With(r).NewCounterVec(
|
||||
@@ -152,6 +161,24 @@ func newSchedulerMetrics(r prometheus.Registerer) *Scheduler {
|
||||
},
|
||||
[]string{"org"},
|
||||
),
|
||||
GetAlertRulesDuration: promauto.With(r).NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "get_alert_rules_duration_seconds",
|
||||
Help: "The time taken to get all alert rules.",
|
||||
Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10},
|
||||
},
|
||||
),
|
||||
SchedulePeriodicDuration: promauto.With(r).NewHistogram(
|
||||
prometheus.HistogramOpts{
|
||||
Namespace: Namespace,
|
||||
Subsystem: Subsystem,
|
||||
Name: "schedule_periodic_duration_seconds",
|
||||
Help: "The time taken to run the scheduler.",
|
||||
Buckets: []float64{0.1, 0.25, 0.5, 1, 2, 5, 10},
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ func (d DiscordNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool,
|
||||
u := tmpl(d.WebhookURL)
|
||||
if tmplErr != nil {
|
||||
d.log.Warn("failed to template Discord message", "err", tmplErr.Error())
|
||||
return false, tmplErr
|
||||
}
|
||||
|
||||
body, err := json.Marshal(bodyJSON)
|
||||
|
||||
@@ -3,6 +3,7 @@ package channels
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
@@ -100,6 +101,14 @@ func TestDiscordNotifier(t *testing.T) {
|
||||
settings: `{}`,
|
||||
expInitError: `failed to validate receiver "discord_testing" of type "discord": could not find webhook url property in settings`,
|
||||
},
|
||||
{
|
||||
name: "Invalid template returns error",
|
||||
settings: `{
|
||||
"url": "http://localhost",
|
||||
"message": "{{ template \"invalid.template\" }}"
|
||||
}`,
|
||||
expMsgError: errors.New("template: :1:12: executing \"\" at <{{template \"invalid.template\"}}>: template \"invalid.template\" not defined"),
|
||||
},
|
||||
{
|
||||
name: "Default config with one alert, use default discord username",
|
||||
settings: `{
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
)
|
||||
|
||||
func (sch *schedule) fetchAllDetails(disabledOrgs []int64) []*models.AlertRule {
|
||||
func (sch *schedule) getAlertRules(disabledOrgs []int64) []*models.AlertRule {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
sch.metrics.GetAlertRulesDuration.Observe(time.Since(start).Seconds())
|
||||
}()
|
||||
|
||||
q := models.ListAlertRulesQuery{
|
||||
ExcludeOrgs: disabledOrgs,
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ func (sch *schedule) Run(ctx context.Context) error {
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := sch.ruleEvaluationLoop(ctx); err != nil {
|
||||
if err := sch.schedulePeriodic(ctx); err != nil {
|
||||
sch.log.Error("failure while running the rule evaluation loop", "err", err)
|
||||
}
|
||||
}()
|
||||
@@ -319,17 +319,21 @@ func (sch *schedule) adminConfigSync(ctx context.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (sch *schedule) ruleEvaluationLoop(ctx context.Context) error {
|
||||
func (sch *schedule) schedulePeriodic(ctx context.Context) error {
|
||||
dispatcherGroup, ctx := errgroup.WithContext(ctx)
|
||||
for {
|
||||
select {
|
||||
case tick := <-sch.heartbeat.C:
|
||||
start := time.Now()
|
||||
sch.metrics.BehindSeconds.Set(start.Sub(tick).Seconds())
|
||||
|
||||
tickNum := tick.Unix() / int64(sch.baseInterval.Seconds())
|
||||
disabledOrgs := make([]int64, 0, len(sch.disabledOrgs))
|
||||
for disabledOrg := range sch.disabledOrgs {
|
||||
disabledOrgs = append(disabledOrgs, disabledOrg)
|
||||
}
|
||||
alertRules := sch.fetchAllDetails(disabledOrgs)
|
||||
|
||||
alertRules := sch.getAlertRules(disabledOrgs)
|
||||
sch.log.Debug("alert rules fetched", "count", len(alertRules), "disabled_orgs", disabledOrgs)
|
||||
|
||||
// registeredDefinitions is a map used for finding deleted alert rules
|
||||
@@ -405,6 +409,8 @@ func (sch *schedule) ruleEvaluationLoop(ctx context.Context) error {
|
||||
}
|
||||
ruleInfo.stop()
|
||||
}
|
||||
|
||||
sch.metrics.SchedulePeriodicDuration.Observe(time.Since(start).Seconds())
|
||||
case <-ctx.Done():
|
||||
waitErr := dispatcherGroup.Wait()
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
LastEvaluationTime: evaluationTime,
|
||||
@@ -133,7 +133,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
LastEvaluationTime: evaluationTime,
|
||||
@@ -156,7 +156,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime,
|
||||
@@ -213,12 +213,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(1 * time.Minute),
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
LastEvaluationTime: evaluationTime.Add(1 * time.Minute),
|
||||
@@ -274,12 +274,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(1 * time.Minute),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(1 * time.Minute),
|
||||
@@ -346,17 +346,17 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(80 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(80 * time.Second),
|
||||
@@ -440,22 +440,22 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(20 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(30 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(40 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(30 * time.Second),
|
||||
@@ -531,22 +531,22 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(20 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(30 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime,
|
||||
@@ -605,12 +605,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -669,12 +669,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime,
|
||||
@@ -733,12 +733,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -797,12 +797,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -860,7 +860,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -924,7 +924,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -991,7 +991,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(20 * time.Second),
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: time.Time{},
|
||||
@@ -1049,12 +1049,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -1114,12 +1114,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -1179,12 +1179,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.Error,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -1258,12 +1258,12 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(10 * time.Second),
|
||||
EvaluationState: eval.Error,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(10 * time.Second),
|
||||
@@ -1339,22 +1339,22 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(30 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(40 * time.Second),
|
||||
EvaluationState: eval.Error,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(70 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(70 * time.Second),
|
||||
@@ -1431,22 +1431,22 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(30 * time.Second),
|
||||
EvaluationState: eval.Alerting,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(40 * time.Second),
|
||||
EvaluationState: eval.Error,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(50 * time.Second),
|
||||
EvaluationState: eval.NoData,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
StartsAt: evaluationTime.Add(30 * time.Second),
|
||||
@@ -1498,7 +1498,7 @@ func TestProcessEvalResults(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime,
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
LastEvaluationTime: evaluationTime,
|
||||
@@ -1603,7 +1603,7 @@ func TestStaleResultsHandler(t *testing.T) {
|
||||
{
|
||||
EvaluationTime: evaluationTime.Add(3 * time.Minute),
|
||||
EvaluationState: eval.Normal,
|
||||
Values: make(map[string]state.EvaluationValue),
|
||||
Values: make(map[string]*float64),
|
||||
},
|
||||
},
|
||||
LastEvaluationTime: evaluationTime.Add(3 * time.Minute),
|
||||
|
||||
@@ -35,23 +35,14 @@ type Evaluation struct {
|
||||
// Values contains the RefID and value of reduce and math expressions.
|
||||
// It does not contain values for classic conditions as the values
|
||||
// in classic conditions do not have a RefID.
|
||||
Values map[string]EvaluationValue
|
||||
}
|
||||
|
||||
// EvaluationValue contains the labels and value for a RefID in an evaluation.
|
||||
type EvaluationValue struct {
|
||||
Labels data.Labels
|
||||
Value *float64
|
||||
Values map[string]*float64
|
||||
}
|
||||
|
||||
// NewEvaluationValues returns the labels and values for each RefID in the capture.
|
||||
func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]EvaluationValue {
|
||||
result := make(map[string]EvaluationValue, len(m))
|
||||
func NewEvaluationValues(m map[string]eval.NumberValueCapture) map[string]*float64 {
|
||||
result := make(map[string]*float64, len(m))
|
||||
for k, v := range m {
|
||||
result[k] = EvaluationValue{
|
||||
Labels: v.Labels,
|
||||
Value: v.Value,
|
||||
}
|
||||
result[k] = v.Value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -89,10 +89,6 @@ func addMigrationInfo(da *dashAlert) (map[string]string, map[string]string) {
|
||||
return lbls, annotations
|
||||
}
|
||||
|
||||
func getMigrationString(da dashAlert) string {
|
||||
return fmt.Sprintf(`{"dashboardUid": "%v", "panelId": %v, "alertId": %v}`, da.DashboardUID, da.PanelId, da.Id)
|
||||
}
|
||||
|
||||
func (m *migration) makeAlertRule(cond condition, da dashAlert, folderUID string) (*alertRule, error) {
|
||||
lbls, annotations := addMigrationInfo(&da)
|
||||
lbls["alertname"] = da.Name
|
||||
|
||||
@@ -22,7 +22,10 @@ import (
|
||||
)
|
||||
|
||||
const GENERAL_FOLDER = "General Alerting"
|
||||
const DASHBOARD_FOLDER = "Migrated %s"
|
||||
const DASHBOARD_FOLDER = "%s Alerts - %s"
|
||||
|
||||
// MaxFolderName is the maximum length of the folder name generated using DASHBOARD_FOLDER format
|
||||
const MaxFolderName = 255
|
||||
|
||||
// FOLDER_CREATED_BY us used to track folders created by this migration
|
||||
// during alert migration cleanup.
|
||||
@@ -231,6 +234,7 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mg.Logger.Info("alerts found to migrate", "alerts", len(dashAlerts))
|
||||
|
||||
// [orgID, dataSourceId] -> UID
|
||||
dsIDMap, err := m.slurpDSIDs()
|
||||
@@ -256,6 +260,9 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// cache for folders created for dashboards that have custom permissions
|
||||
folderCache := make(map[string]*dashboard)
|
||||
|
||||
for _, da := range dashAlerts {
|
||||
newCond, err := transConditions(*da.ParsedSettings, da.OrgId, dsIDMap)
|
||||
if err != nil {
|
||||
@@ -280,55 +287,65 @@ func (m *migration) Exec(sess *xorm.Session, mg *migrator.Migrator) error {
|
||||
}
|
||||
}
|
||||
|
||||
// get folder if exists
|
||||
folder, err := m.getFolder(dash, da)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: err,
|
||||
AlertId: da.Id,
|
||||
}
|
||||
}
|
||||
|
||||
var folder *dashboard
|
||||
switch {
|
||||
case dash.HasAcl:
|
||||
// create folder and assign the permissions of the dashboard (included default and inherited)
|
||||
ptr, err := m.createFolder(dash.OrgId, fmt.Sprintf(DASHBOARD_FOLDER, getMigrationString(da)))
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to create folder: %w", err),
|
||||
AlertId: da.Id,
|
||||
folderName := getAlertFolderNameFromDashboard(&dash)
|
||||
f, ok := folderCache[folderName]
|
||||
if !ok {
|
||||
mg.Logger.Info("create a new folder for alerts that belongs to dashboard because it has custom permissions", "org", dash.OrgId, "dashboard_uid", dash.Uid, "folder", folderName)
|
||||
// create folder and assign the permissions of the dashboard (included default and inherited)
|
||||
f, err = m.createFolder(dash.OrgId, folderName)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to create folder: %w", err),
|
||||
AlertId: da.Id,
|
||||
}
|
||||
}
|
||||
}
|
||||
folder = *ptr
|
||||
permissions, err := m.getACL(dash.OrgId, dash.Id)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to get dashboard %d under organisation %d permissions: %w", dash.Id, dash.OrgId, err),
|
||||
AlertId: da.Id,
|
||||
permissions, err := m.getACL(dash.OrgId, dash.Id)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to get dashboard %d under organisation %d permissions: %w", dash.Id, dash.OrgId, err),
|
||||
AlertId: da.Id,
|
||||
}
|
||||
}
|
||||
}
|
||||
err = m.setACL(folder.OrgId, folder.Id, permissions)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to set folder %d under organisation %d permissions: %w", folder.Id, folder.OrgId, err),
|
||||
AlertId: da.Id,
|
||||
err = m.setACL(f.OrgId, f.Id, permissions)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to set folder %d under organisation %d permissions: %w", folder.Id, folder.OrgId, err),
|
||||
AlertId: da.Id,
|
||||
}
|
||||
}
|
||||
folderCache[folderName] = f
|
||||
}
|
||||
folder = f
|
||||
case dash.FolderId > 0:
|
||||
// link the new rule to the existing folder
|
||||
default:
|
||||
// get or create general folder
|
||||
ptr, err := m.getOrCreateGeneralFolder(dash.OrgId)
|
||||
// get folder if exists
|
||||
f, err := m.getFolder(dash, da)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to get or create general folder under organisation %d: %w", dash.OrgId, err),
|
||||
Err: err,
|
||||
AlertId: da.Id,
|
||||
}
|
||||
}
|
||||
folder = &f
|
||||
default:
|
||||
f, ok := folderCache[GENERAL_FOLDER]
|
||||
if !ok {
|
||||
// get or create general folder
|
||||
f, err = m.getOrCreateGeneralFolder(dash.OrgId)
|
||||
if err != nil {
|
||||
return MigrationError{
|
||||
Err: fmt.Errorf("failed to get or create general folder under organisation %d: %w", dash.OrgId, err),
|
||||
AlertId: da.Id,
|
||||
}
|
||||
}
|
||||
folderCache[GENERAL_FOLDER] = f
|
||||
}
|
||||
// No need to assign default permissions to general folder
|
||||
// because they are included to the query result if it's a folder with no permissions
|
||||
// https://github.com/grafana/grafana/blob/076e2ce06a6ecf15804423fcc8dca1b620a321e5/pkg/services/sqlstore/dashboard_acl.go#L109
|
||||
folder = *ptr
|
||||
folder = f
|
||||
}
|
||||
|
||||
if folder.Uid == "" {
|
||||
@@ -780,3 +797,14 @@ func CheckUnifiedAlertingEnabledByDefault(migrator *migrator.Migrator) error {
|
||||
migrator.Logger.Debug(fmt.Sprintf("Found %d legacy alerts in the database. Unified alerting enabled is %v", resp.Count, ualertEnabled))
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAlertFolderNameFromDashboard generates a folder name for alerts that belong to a dashboard. Formats the string according to DASHBOARD_FOLDER format.
|
||||
// If the resulting string exceeds the migrations.MaxTitleLength, the dashboard title is stripped to be at the maximum length
|
||||
func getAlertFolderNameFromDashboard(dash *dashboard) string {
|
||||
maxLen := MaxFolderName - len(fmt.Sprintf(DASHBOARD_FOLDER, "", dash.Uid))
|
||||
title := dash.Title
|
||||
if len(title) > maxLen {
|
||||
title = title[:maxLen]
|
||||
}
|
||||
return fmt.Sprintf(DASHBOARD_FOLDER, title, dash.Uid) // include UID to the name to avoid collision
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user