Compare commits
139 Commits
provisioni
...
v8.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6134e3cf35 | ||
|
|
c7bc0bd29d | ||
|
|
ad35db8636 | ||
|
|
c75be3bb3e | ||
|
|
f368cac796 | ||
|
|
a228985d97 | ||
|
|
feaf32f04c | ||
|
|
447752c00d | ||
|
|
d300a27ee7 | ||
|
|
301be18d86 | ||
|
|
644b23aba2 | ||
|
|
da335ce32e | ||
|
|
7065ee20ee | ||
|
|
5c64c3559d | ||
|
|
e9c13f824c | ||
|
|
c0e157300b | ||
|
|
9c1c446b44 | ||
|
|
d2feeb8455 | ||
|
|
f08c5a796c | ||
|
|
690c08b80d | ||
|
|
1bd1838363 | ||
|
|
b37047eadb | ||
|
|
9841f1d488 | ||
|
|
7b7f7facca | ||
|
|
52fe9ba3fe | ||
|
|
f7683af8b5 | ||
|
|
0508656182 | ||
|
|
0a4b6dccfc | ||
|
|
f682ad8c60 | ||
|
|
4ea9193696 | ||
|
|
ffab9f6587 | ||
|
|
37b762fe9c | ||
|
|
45d8bbba3a | ||
|
|
78659b6814 | ||
|
|
e59f21a22e | ||
|
|
f62e76b5d5 | ||
|
|
5ba2aafa88 | ||
|
|
b53f5675f4 | ||
|
|
5de766a202 | ||
|
|
8888dac836 | ||
|
|
17c51dcd32 | ||
|
|
482ca1b0f7 | ||
|
|
cd66d6cdc4 | ||
|
|
548145691b | ||
|
|
aa5bc10bcf | ||
|
|
39cab9c066 | ||
|
|
bc9621699b | ||
|
|
2dbe8dfcbc | ||
|
|
73a0da54e4 | ||
|
|
c724639f56 | ||
|
|
1bffc1f4ad | ||
|
|
7f4a77fb75 | ||
|
|
8bb7c88e07 | ||
|
|
190766fed1 | ||
|
|
8dd16fb871 | ||
|
|
2250c245ef | ||
|
|
ee066425ee | ||
|
|
c8987a040c | ||
|
|
c8aeb049ab | ||
|
|
c2a6188334 | ||
|
|
00e572dc3b | ||
|
|
c8327d04a8 | ||
|
|
a0ff246fcb | ||
|
|
05fb17c3f3 | ||
|
|
2a34264cdd | ||
|
|
91f7cd2307 | ||
|
|
ff6a7b0a7f | ||
|
|
5c41d84f88 | ||
|
|
37e5b0fbc7 | ||
|
|
0e8ba02479 | ||
|
|
21404f4169 | ||
|
|
b8039a80b7 | ||
|
|
e8e03d7b1f | ||
|
|
f01f1073b8 | ||
|
|
5332a2db05 | ||
|
|
67d42fc51c | ||
|
|
a8bfaafeb5 | ||
|
|
7366fa2530 | ||
|
|
6aa95a6b86 | ||
|
|
9534cebdc1 | ||
|
|
e8517ecf15 | ||
|
|
59886859c0 | ||
|
|
5379941bf9 | ||
|
|
ba4af4a7f6 | ||
|
|
7729b14da3 | ||
|
|
691e94ebe4 | ||
|
|
5721ff9689 | ||
|
|
d6b166223f | ||
|
|
c7696261fb | ||
|
|
0cb55ee544 | ||
|
|
73f5ca876f | ||
|
|
09d6461ce5 | ||
|
|
ef592f1a66 | ||
|
|
31f4d88206 | ||
|
|
ee9807d631 | ||
|
|
9af9cd39cd | ||
|
|
cce492b246 | ||
|
|
be0d1a8d57 | ||
|
|
f438ffacf6 | ||
|
|
aa412e9678 | ||
|
|
b477c7eba9 | ||
|
|
fb9a67f34d | ||
|
|
556178c714 | ||
|
|
68c6e7514a | ||
|
|
bd76bd0d0e | ||
|
|
be54ab63cb | ||
|
|
abc9678589 | ||
|
|
fd703d9d87 | ||
|
|
cb0f520a69 | ||
|
|
f5dd8de077 | ||
|
|
b31aaf444f | ||
|
|
c3f6e1e224 | ||
|
|
9597cc68a7 | ||
|
|
7a1e09631f | ||
|
|
9c5e459f0b | ||
|
|
dd2663352c | ||
|
|
58f27b980d | ||
|
|
283b93bc15 | ||
|
|
cb0093d27f | ||
|
|
056ff9cf32 | ||
|
|
df94aa979c | ||
|
|
776d2cc03e | ||
|
|
3fa4ae500a | ||
|
|
a8ab8a570b | ||
|
|
99dbf0502a | ||
|
|
c7877e09d0 | ||
|
|
993ed70e34 | ||
|
|
0f27624e37 | ||
|
|
ec1ae002ea | ||
|
|
5965a7e479 | ||
|
|
b7b2149b76 | ||
|
|
b4a0436af8 | ||
|
|
1a4ecfde4e | ||
|
|
52b8e4991c | ||
|
|
f3a704de5f | ||
|
|
4dcf1a8464 | ||
|
|
f497164472 | ||
|
|
8280aa5636 | ||
|
|
50712b5782 |
@@ -302,7 +302,7 @@ exports[`no enzyme tests`] = {
|
||||
"public/app/plugins/datasource/cloudwatch/components/ConfigEditor.test.tsx:2974837543": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:132770839": [
|
||||
"public/app/plugins/datasource/cloudwatch/components/LogsQueryField.test.tsx:3888529428": [
|
||||
[1, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx:1089831034": [
|
||||
|
||||
1078
.drone.yml
1078
.drone.yml
File diff suppressed because it is too large
Load Diff
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,3 +1,54 @@
|
||||
<!-- 8.5.0-beta1 START -->
|
||||
|
||||
# 8.5.0-beta1 (2022-04-06)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- Add config option to enable/disable reporting. (Enterprise)
|
||||
- **Alerting:** Accurately set value for prom-compatible APIs. [#47216](https://github.com/grafana/grafana/pull/47216), [@gotjosh](https://github.com/gotjosh)
|
||||
- **Alerting:** Provisioning API - Notification Policies. [#46755](https://github.com/grafana/grafana/pull/46755), [@alexweav](https://github.com/alexweav)
|
||||
- **Alerting:** Notification URL points to alert view page instead of alert edit page. [#47752](https://github.com/grafana/grafana/pull/47752), [@joeblubaugh](https://github.com/joeblubaugh)
|
||||
- **Analytics:** Enable grafana and plugin update checks to be operated independently. [#46352](https://github.com/grafana/grafana/pull/46352), [@wbrowne](https://github.com/wbrowne)
|
||||
- **Azure Monitor:** Add support for multiple template variables in resource picker. [#46215](https://github.com/grafana/grafana/pull/46215), [@sarahzinger](https://github.com/sarahzinger)
|
||||
- **Caching:** Add separate TTL for resources cache. (Enterprise)
|
||||
- **Caching:** add support for TLS configuration for Redis Cluster. (Enterprise)
|
||||
- **NewsPanel:** Remove Use Proxy option and update documentation with recommendations. [#47189](https://github.com/grafana/grafana/pull/47189), [@joshhunt](https://github.com/joshhunt)
|
||||
- **OAuth:** Sync GitHub OAuth user name to Grafana if it's set. [#45438](https://github.com/grafana/grafana/pull/45438), [@pallxk](https://github.com/pallxk)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Plugins:** Fix Default Nav URL for dashboard includes. [#47143](https://github.com/grafana/grafana/pull/47143), [@wbrowne](https://github.com/wbrowne)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
When user is using Github OAuth, GitHub login is showed as both Grafana login and name. Now the GitHub name is showed as Grafana name, and GitHub login is showed as Grafana Login. Issue [#45438](https://github.com/grafana/grafana/issues/45438)
|
||||
|
||||
The meaning of the default data source has now changed from being a persisted property in a panel. Before when you selected the default data source for a panel and later changed the default data source to another data source it would change all panels who were configured to use the default data source. From now on the default data source is just the default for new panels and changing the default will not impact any currently saved dashboards. Issue [#45132](https://github.com/grafana/grafana/issues/45132)
|
||||
|
||||
<!-- 8.4.7 START -->
|
||||
|
||||
# 8.4.7 (2022-04-19)
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **CloudWatch:** Added missing MemoryDB Namespace metrics. [#47290](https://github.com/grafana/grafana/pull/47290), [@james-deee](https://github.com/james-deee)
|
||||
- **Histogram Panel:** Take decimal into consideration. [#47330](https://github.com/grafana/grafana/pull/47330), [@mdvictor](https://github.com/mdvictor)
|
||||
- **TimeSeries:** Sort tooltip values based on raw values. [#46738](https://github.com/grafana/grafana/pull/46738), [@dprokop](https://github.com/dprokop)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **API:** Include userId, orgId, uname in request logging middleware. [#47183](https://github.com/grafana/grafana/pull/47183), [@marefr](https://github.com/marefr)
|
||||
- **Elasticsearch:** Respect maxConcurrentShardRequests datasource setting. [#47120](https://github.com/grafana/grafana/pull/47120), [@alexandrst88](https://github.com/alexandrst88)
|
||||
|
||||
<!-- 8.4.7 END -->
|
||||
<!-- 8.5.0-beta1 END -->
|
||||
<!-- 8.4.6 START -->
|
||||
|
||||
# 8.4.6 (2022-04-12)
|
||||
|
||||
- **Security:** Fixes CVE-2022-24812. For more information, see our [blog](https://grafana.com/blog/2022/04/12/grafana-enterprise-8.4.6-released-with-high-severity-security-fix/)
|
||||
|
||||
<!-- 8.4.6 END -->
|
||||
<!-- 8.4.5 START -->
|
||||
|
||||
# 8.4.5 (2022-03-31)
|
||||
|
||||
@@ -20,7 +20,7 @@ COPY emails emails
|
||||
ENV NODE_ENV production
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:1.17.8-alpine3.15 as go-builder
|
||||
FROM golang:1.17.9-alpine3.15 as go-builder
|
||||
|
||||
RUN apk add --no-cache gcc g++ make
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ COPY emails emails
|
||||
ENV NODE_ENV production
|
||||
RUN yarn build
|
||||
|
||||
FROM golang:1.17.8 AS go-builder
|
||||
FROM golang:1.17.9 AS go-builder
|
||||
|
||||
WORKDIR /src/grafana
|
||||
|
||||
|
||||
@@ -230,6 +230,9 @@ application_insights_connection_string =
|
||||
# Optional. Specifies an Application Insights endpoint URL where the endpoint string is wrapped in backticks ``.
|
||||
application_insights_endpoint_url =
|
||||
|
||||
# Controls if the UI contains any links to user feedback forms
|
||||
feedback_links_enabled = true
|
||||
|
||||
#################################### Security ############################
|
||||
[security]
|
||||
# disable creation of admin user on first start of grafana
|
||||
@@ -1131,9 +1134,12 @@ license_path =
|
||||
# enable = feature1,feature2
|
||||
enable =
|
||||
|
||||
# The new prometheus visual query builder
|
||||
# The new prometheus visual query builder
|
||||
promQueryBuilder = true
|
||||
|
||||
# Experimental Explore to Dashboard workflow
|
||||
explore2Dashboard = true
|
||||
|
||||
# feature1 = true
|
||||
# feature2 = false
|
||||
|
||||
|
||||
@@ -230,6 +230,9 @@
|
||||
# Rudderstack Config url, optional, used by Rudderstack SDK to fetch source config
|
||||
;rudderstack_config_url =
|
||||
|
||||
# Controls if the UI contains any links to user feedback forms
|
||||
;feedback_links_enabled = true
|
||||
|
||||
#################################### Security ####################################
|
||||
[security]
|
||||
# disable creation of admin user on first start of grafana
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"iteration": 1646409057541,
|
||||
"iteration": 1601526910610,
|
||||
"links": [
|
||||
{
|
||||
"icon": "external link",
|
||||
@@ -73,61 +73,6 @@
|
||||
"timeShift": null,
|
||||
"title": "${custom.text}",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 9,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"type": "stat",
|
||||
"title": "Panel Title",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"title": "Var Link",
|
||||
"url": "/d/vmie2cmWz/bar-gauge-demo?var-custom=$custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"reduceOptions": {
|
||||
"values": false,
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": ""
|
||||
},
|
||||
"orientation": "auto",
|
||||
"textMode": "auto",
|
||||
"colorMode": "value",
|
||||
"graphMode": "area",
|
||||
"justifyMode": "auto"
|
||||
},
|
||||
"pluginVersion": "8.5.0-pre",
|
||||
"datasource": null
|
||||
}
|
||||
],
|
||||
"schemaVersion": 26,
|
||||
@@ -167,11 +112,6 @@
|
||||
"selected": false,
|
||||
"text": "p3",
|
||||
"value": "p3"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "p4",
|
||||
"value": "test%25value"
|
||||
}
|
||||
],
|
||||
"query": "p1,p2,p3",
|
||||
|
||||
2957
devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json
Normal file
2957
devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json
Normal file
File diff suppressed because it is too large
Load Diff
2683
devenv/dev-dashboards/panel-table/table_pagination.json
Normal file
2683
devenv/dev-dashboards/panel-table/table_pagination.json
Normal file
File diff suppressed because one or more lines are too long
@@ -12,3 +12,4 @@ This section includes information for Grafana administrators, team administrator
|
||||
- [Configuration]({{< relref "configuration" >}})
|
||||
- [Configure Docker image]({{< relref "configure-docker" >}})
|
||||
- [Security]({{< relref "security" >}})
|
||||
- [Service accounts]({{< relref "service-accounts" >}})
|
||||
|
||||
17
docs/sources/administration/api-keys/_index.md
Normal file
17
docs/sources/administration/api-keys/_index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: 'API keys in Grafana'
|
||||
menuTitle: 'API keys'
|
||||
description: 'This section contains information about API keys in Grafana'
|
||||
weight: 300
|
||||
keywords:
|
||||
- API keys
|
||||
- Service accounts
|
||||
---
|
||||
|
||||
# API keys in Grafana
|
||||
|
||||
API Keys can be used to interact with Grafana HTTP APIs.
|
||||
|
||||
We recommend using service accounts instead of API keys if you are on Grafana 8.5+, for more information refer to [About service accounts]({{< relref "../service-accounts/about-service-accounts.md#">}}).
|
||||
|
||||
{{< section >}}
|
||||
12
docs/sources/administration/api-keys/about-api-keys.md
Normal file
12
docs/sources/administration/api-keys/about-api-keys.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: About API keys in Grafana
|
||||
menuTitle: About API keys
|
||||
description: 'Learn about using API keys in Grafana'
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# About API keys in Grafana
|
||||
|
||||
An API key is a randomly generated string that external systems use to interact with Grafana HTTP APIs.
|
||||
|
||||
When you create an API key, you specify a **Role** that determines the permissions associated with the API key. Role permissions control that actions the API key can perform on Grafana resources. For more information about creating API keys, refer to [Create an API key]({{< relref "./create-api-key.md#">}}).
|
||||
34
docs/sources/administration/api-keys/create-api-key.md
Normal file
34
docs/sources/administration/api-keys/create-api-key.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Create an API key in Grafana
|
||||
menuTitle: Create an API key
|
||||
description: 'How to create an API key in Grafana'
|
||||
weight: 50
|
||||
keywords:
|
||||
- API keys
|
||||
- Service accounts
|
||||
---
|
||||
|
||||
# Create an API key in Grafana
|
||||
|
||||
Create an API key when you want to manage your computed workload with a user.
|
||||
|
||||
For more information about API keys, refer to [About API keys in Grafana]({{< relref "./about-api-keys.md">}}).
|
||||
|
||||
This topic shows you how to create an API key using the Grafana UI. You can also create an API key using the Grafana HTTP API. For more information about creating API keys via the API, refer to [Create API key via API]({{< relref "../../http_api/create-api-tokens-for-org.md#how-to-create-a-new-organization-and-an-api-token">}}).
|
||||
|
||||
## Before you begin:
|
||||
|
||||
- Ensure you have permission to create and edit API keys. For more information about permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#">}}).
|
||||
|
||||
**To create an API key:**
|
||||
|
||||
1. Sign in to Grafana, hover your cursor over **Configuration** (the gear icon), and click **API Keys**.
|
||||
1. Click **New API key**.
|
||||
1. Enter a unique name for the key.
|
||||
1. In the **Role** field, select one of the following access levels you want to assign to the key.
|
||||
- **Admin**: Enables a user to use APIs at the broadest, most powerful administrative level.
|
||||
- **Editor** or **Viewer** to limit the key's users to those levels of power.
|
||||
1. In the **Time to live** field, specify how long you want the key to be valid.
|
||||
- The maximum length of time is 30 days (one month). You enter a number and a letter. Valid letters include `s` for seconds,`m` for minutes, `h` for hours, `d `for days, `w` for weeks, and `M `for month. For example, `12h` is 12 hours and `1M` is 1 month (30 days).
|
||||
- If you are unsure about how long an API key should be valid, we recommend that you choose a short duration, such as a few hours. This approach limits the risk of having API keys that are valid for a long time.
|
||||
1. Click **Add**.
|
||||
@@ -509,6 +509,10 @@ If you want to track Grafana usage via Azure Application Insights, then specify
|
||||
|
||||
<hr />
|
||||
|
||||
### enable_feedback_links
|
||||
|
||||
If set to false will remove all feedback links from the UI. Defaults to true.
|
||||
|
||||
## [security]
|
||||
|
||||
### disable_initial_admin_creation
|
||||
|
||||
15
docs/sources/administration/service-accounts/_index.md
Normal file
15
docs/sources/administration/service-accounts/_index.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: 'Service accounts in Grafana'
|
||||
menuTitle: 'Service accounts'
|
||||
description: 'This page contains information about service accounts in Grafana'
|
||||
weight: 300
|
||||
keywords:
|
||||
- API keys
|
||||
- Service accounts
|
||||
---
|
||||
|
||||
# Service accounts in Grafana
|
||||
|
||||
You can use service accounts to run automated or compute workloads.
|
||||
|
||||
{{< section >}}
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: About service accounts
|
||||
menuTitle: About service accounts
|
||||
description: 'This page contains detailed information about service accounts in Grafana'
|
||||
weight: 30
|
||||
---
|
||||
|
||||
# About service accounts in Grafana
|
||||
|
||||
A service account can be used to run automated or compute workloads. Applications use service account tokens to authorize themselves as a service account.
|
||||
|
||||
> **Note:** Service accounts are available in Grafana 8.5+ as a beta feature, to enable service accounts refer to [Enable service accounts]({{< relref "./enable-service-accounts.md#">}}) section.
|
||||
|
||||
A common use case for creating a service account is to perform operations on automated or triggered tasks. You can use service accounts to:
|
||||
|
||||
- Schedule reports for specific dashboards to be delivered on a daily/weekly/monthly basis
|
||||
- Define alerts in your system to be used in Grafana
|
||||
- Set up an external authentication provider to manage users and permissions across an organization
|
||||
- Establish machine-to-machine communication
|
||||
- Interact with Grafana without logging in as a user
|
||||
|
||||
You can also use service accounts in combination with fine-grained access control to grant users specific scopes.
|
||||
|
||||
You can associate a service account with multiple tokens. This is because a service account:
|
||||
|
||||
- can be used by multiple team members and therefore can generate their own token each
|
||||
- can be used across multiple tenants and each tenant can have its own token
|
||||
|
||||
We recommend the you begin by creating one service account for each use case.
|
||||
|
||||
> **Note:** Service accounts can only act in the organization they are created for. If you have the same task that is needed for multiple organizations, we recommend creating service accounts in each organization.
|
||||
|
||||
---
|
||||
|
||||
## Service account tokens
|
||||
|
||||
A service account token is a generated random string that are an alternative to using passwords for authentication with Grafana, to interact with the Grafana HTTP APIs.
|
||||
|
||||
When you create a service account, you can associate one or more access tokens with it. You can use service access tokens the same way as API Keys, for example to access Grafana HTTP API programmatically.
|
||||
|
||||
Service account access tokens inherit permissions from service account directly.
|
||||
|
||||
### Service accounts benefits
|
||||
|
||||
The added benefits of service accounts to API keys include:
|
||||
|
||||
- Service accounts resemble Grafana users and can be enabled/disabled, granted specific permissions, and remain active until they are deleted or disabled. API keys are only valid until their expiry date.
|
||||
- Service accounts can be associated with multiple tokens.
|
||||
- Unlike API keys, service account tokens are not associated with a specific user, which means that applications can be authenticated even if a Grafana user is deleted.
|
||||
- You can grant granular permissions to service accounts by leveraging [fine-grained access control]({{< relref "../../enterprise/access-control">}}). For more information about permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#">}}).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: 'Add a token to a service account in Grafana'
|
||||
menuTitle: 'Add a token to a service account'
|
||||
description: 'This topic shows you how to add a token to a service account'
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# Add a token to a service account in Grafana
|
||||
|
||||
A service account token is a randomly generated string that external system use to authenticate into Grafana, and include specific permissions to interact with the Grafana HTTP APIs.
|
||||
For more information about service accounts, refer to [About service accounts in Grafana]({{< relref "./about-service-accounts.md">}}).
|
||||
|
||||
You can create a service account token using the Grafana UI or via the API. For more information about creating a service account token via the API, refer to [HTTP API Create service account token]({{< relref "../../http_api/serviceaccount.md#create-service-account-tokens">}}).
|
||||
|
||||
## Before you begin
|
||||
|
||||
- Ensure you have added the `serviceAccounts` feature toggle to Grafana. For more information about adding the feature toggle, refer to [Enable service accounts]({{< relref "./enable-service-accounts.md#">}}).
|
||||
- Ensure you have permission to create and edit service accounts. For more information about user roles, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#">}}).
|
||||
- [Create a service account in Grafana]({{< relref "./create-service-account.md#">}}).
|
||||
|
||||
**To add a token to a service account:**
|
||||
|
||||
1. Sign in to Grafana and hover your cursor over the organization icon in the sidebar.
|
||||
1. Click **Service accounts**.
|
||||
1. Click the service account to which you want to add a token.
|
||||
1. Click **Add token**.
|
||||
1. Enter a name for the token.
|
||||
1. (recommended) Enter an expiry date and expiry date for the token or leave it on no expiry date option.
|
||||
- The expiry date specifies how long you want the key to be valid.
|
||||
- If you are unsure of an expiration date, we recommend that you set the token to expire after a short time, such as a few hours or less. This limits the risk associated with a token that is valid for a long time.
|
||||
1. Click **Generate service account token**.
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
title: Create a service account in Grafana
|
||||
menuTitle: Create a service account
|
||||
description: 'How to create a service account in Grafana'
|
||||
weight: 50
|
||||
keywords:
|
||||
- Service accounts
|
||||
---
|
||||
|
||||
# Create a service account in Grafana
|
||||
|
||||
A service account is a user account that you can use to run automated or compute workloads. For more information about how you can use service accounts, refer to [About service accounts]({{< relref "../service-accounts/about-service-accounts.md#">}}).
|
||||
|
||||
For more information about creating service accounts via the API, refer to [Create service account via API]({{< relref "../../http_api/serviceaccount.md#create-service-account">}}).
|
||||
|
||||
## Before you begin
|
||||
|
||||
- Ensure you have added the feature toggle for service accounts `serviceAccounts`. For more information about adding the feature toggle, refer to [Enable service accounts]({{< relref "./enable-service-accounts.md#">}}).
|
||||
- Ensure you have permission to create and edit service accounts. For more information about user permissions, refer to [About users and permissions]({{< relref "../manage-users-and-permissions/about-users-and-permissions.md#">}}).
|
||||
|
||||
**To create a service account:**
|
||||
|
||||
1. Sign in to Grafana and hover your cursor over the organization icon in the sidebar.
|
||||
1. Click **Service accounts**.
|
||||
1. Click **New service account**.
|
||||
1. Enter a **Display name**.
|
||||
1. The display name must be unique as it determines the ID associated with the service account.
|
||||
- We recommend that you use a consistent naming convention when you name service accounts. A consistent naming convention can help you scale and maintain service accounts in the future.
|
||||
- You can change the display name at any time.
|
||||
1. Click **Create service account**.
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
title: 'Enable service accounts in Grafana'
|
||||
menuTitle: 'Enable service accounts'
|
||||
description: 'This topic shows you how to to enable the service accounts feature in Grafana'
|
||||
weight: 40
|
||||
keywords:
|
||||
- Feature toggle
|
||||
- Service accounts
|
||||
---
|
||||
|
||||
# Enable service accounts in Grafana
|
||||
|
||||
Service accounts are available behind the `service-accounts` feature toggle available in Grafana 9.0+.
|
||||
|
||||
You can enable service accounts by:
|
||||
|
||||
- modifying the Grafana configuration file, or
|
||||
- configuring an environment variable
|
||||
|
||||
## Enable service accounts with configuration file
|
||||
|
||||
This topic shows you how to enable service accounts by modifying the Grafana configuration file.
|
||||
|
||||
1. Sign in to the Grafana server and locate the configuration file. For more information about finding the configuration file, refer to LINK.
|
||||
1. Open the configuration file and locate the [feature toggles] section. In your [config file]({{< relref "../../administration/configuration.md#config-file-locations" >}}), add `serviceAccounts` as a [feature_toggle]({{< relref "../../administration/configuration.md#feature_toggle" >}}).
|
||||
|
||||
```
|
||||
[feature_toggles]
|
||||
# enable features, separated by spaces
|
||||
enable = serviceAccounts
|
||||
```
|
||||
|
||||
1. Save your changes, Grafana should recognize your changes; in case of any issues we recommend restarting the Grafana server.
|
||||
|
||||
## Enable service accounts with an environment variable
|
||||
|
||||
This topic shows you how to enable service accounts by setting environment variables before starting Grafana.
|
||||
|
||||
> **Note:** Environment variables override any configuration file settings.
|
||||
|
||||
You can use `GF_FEATURE_TOGGLES_ENABLE = serviceAccounts` environment variable.
|
||||
|
||||
For more information regarding on how to setup environment variables refer to [Configuring with environment variables]({{< relref "../../administration/configuration.md#override-configuration-with-environment-variables" >}}).
|
||||
@@ -8,6 +8,8 @@ weight = 113
|
||||
|
||||
Grafana 8.0 has new and improved alerting that centralizes alerting information in a single, searchable view. It is enabled by default for all new OSS instances, and is an [opt-in]({{< relref "./opt-in.md" >}}) feature for older installations that still use legacy dashboard alerting. We encourage you to create issues in the Grafana GitHub repository for bugs found while testing Grafana alerting. See also, [What's New with Grafana alerting]({{< relref "./difference-old-new.md" >}}).
|
||||
|
||||
> Refer to [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) in Grafana Enterprise to learn more about controlling access to alerts using fine-grained permissions.
|
||||
|
||||
When Grafana alerting is enabled, you can:
|
||||
|
||||
- [Create Grafana managed alerting rules]({{< relref "alerting-rules/create-grafana-managed-rule.md" >}})
|
||||
|
||||
@@ -4,7 +4,7 @@ aliases = ["/docs/grafana/latest/features/dashboard/dashboards/"]
|
||||
weight = 80
|
||||
+++
|
||||
|
||||
# Dshboard rows
|
||||
# Dashboard rows
|
||||
|
||||
A dashboard row is a logical divider within a dashboard. It is used to group panels together.
|
||||
|
||||
|
||||
@@ -143,13 +143,13 @@ types of template variables.
|
||||
|
||||
### Query variable
|
||||
|
||||
The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string.
|
||||
The Elasticsearch data source supports two types of queries you can use in the _Query_ field of _Query_ variables. The query is written using a custom JSON string. The field should be mapped as a [keyword](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword) in the Elasticsearch index mapping. If it is [multi-field](https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html) with both a `text` and `keyword` type, then use `"field":"fieldname.keyword"`(sometimes`fieldname.raw`) to specify the keyword field in your query.
|
||||
|
||||
| Query | Description |
|
||||
| -------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
|
||||
| `{"find": "terms", "field": "@hostname", "size": 1000}` | Returns a list of values for a field using term aggregation. Query will use current dashboard time range as time range for query. |
|
||||
| `{"find": "terms", "field": "@hostname", "query": '<lucene query>'}` | Returns a list of values for a field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
|
||||
| Query | Description |
|
||||
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `{"find": "fields", "type": "keyword"}` | Returns a list of field names with the index type `keyword`. |
|
||||
| `{"find": "terms", "field": "hostname.keyword", "size": 1000}` | Returns a list of values for a keyword using term aggregation. Query will use current dashboard time range as time range query. |
|
||||
| `{"find": "terms", "field": "hostname", "query": '<lucene query>'}` | Returns a list of values for a keyword field using term aggregation and a specified lucene query filter. Query will use current dashboard time range as time range for query. |
|
||||
|
||||
There is a default size limit of 500 on terms queries. Set the size property in your query to set a custom limit.
|
||||
You can use other variables inside the query. Example query definition for a variable named `$host`.
|
||||
|
||||
@@ -16,76 +16,59 @@ Grafana includes built-in support for Prometheus. This topic explains options, v
|
||||
|
||||
To access Prometheus settings, hover your mouse over the **Configuration** (gear) icon, then click **Data Sources**, and then click the Prometheus data source.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
|
||||
| `Default` | Default data source that is pre-selected for new panels. |
|
||||
| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
|
||||
| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
|
||||
| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
|
||||
| `User` | User name for basic authentication. |
|
||||
| `Password` | Password for basic authentication. |
|
||||
| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
|
||||
| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
|
||||
| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
|
||||
| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
|
||||
| `Label name` | Add the name of the field in the label object. |
|
||||
| `URL` | If the link is external, then enter the full link URL. You can interpolate the value from the field with `${__value.raw }` macro. |
|
||||
| `URL Label` | (Optional) Set a custom display label for the link URL. The link label defaults to the full external URL or the name of datasource and is overridden by this setting. |
|
||||
| `Internal link` | Select if the link is internal or external. In the case of an internal link, a data source selector allows you to select the target data source. Supports tracing data sources only. |
|
||||
| Name | Description |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Name` | The data source name. This is how you refer to the data source in panels and queries. |
|
||||
| `Default` | Default data source that is pre-selected for new panels. |
|
||||
| `Url` | The URL of your Prometheus server, for example, `http://prometheus.example.org:9090`. |
|
||||
| `Access` | Server (default) = URL needs to be accessible from the Grafana backend/server, Browser = URL needs to be accessible from the browser. **Note**: Browser (direct) access is deprecated and will be removed in a future release. |
|
||||
| `Basic Auth` | Enable basic authentication to the Prometheus data source. |
|
||||
| `User` | User name for basic authentication. |
|
||||
| `Password` | Password for basic authentication. |
|
||||
| `Scrape interval` | Set this to the typical scrape and evaluation interval configured in Prometheus. Defaults to 15s. |
|
||||
| `HTTP method` | Use either POST or GET HTTP method to query your data source. POST is the recommended and pre-selected method as it allows bigger queries. Change this to GET if you have a Prometheus version older than 2.1 or if POST requests are restricted in your network. |
|
||||
| `Disable metrics lookup` | Checking this option will disable the metrics chooser and metric/label support in the query field's autocomplete. This helps if you have performance issues with bigger Prometheus instances. |
|
||||
| `Custom Query Parameters` | Add custom parameters to the Prometheus query URL. For example `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be concatenated together with an '&'. |
|
||||
| **Exemplars configuration** | |
|
||||
| `Internal link` | Enable this option is you have an internal link. When you enable this option, you will see a data source selector. Select the backend tracing data store for your exemplar data. |
|
||||
| `Data source` | You will see this option only if you enable `Internal link` option. Select the backend tracing data store for your exemplar data. |
|
||||
| `URL` | You will see this option only if the `Internal link` option is disabled. Enter the full URL of the external link. You can interpolate the value from the field with `${__value.raw }` macro. |
|
||||
| `URL Label` | (Optional) add a custom display label to override the value of the `Label name` field. |
|
||||
| `Label name` | Add a name for the exemplar traceID property. |
|
||||
|
||||
## Prometheus query editor
|
||||
|
||||
Below you can find information and options for Prometheus query editor in dashboard and in Explore.
|
||||
Prometheus query editor is separated into 3 distinct modes that you can switch between. See docs for each section below.
|
||||
|
||||
### Query editor in dashboards
|
||||

|
||||
|
||||
Open a graph in edit mode by clicking the title > Edit (or by pressing `e` key while hovering over panel).
|
||||
At the top of the editor there is `Run query` button that will run the query and `Explain | Builder | Code` tabs to switch between the editor modes. If the query editor is in Builder mode there are additional elements explained in the Builder section.
|
||||
|
||||
{{< figure src="/static/img/docs/v45/prometheus_query_editor_still.png"
|
||||
animated-gif="/static/img/docs/v45/prometheus_query_editor.gif" >}}
|
||||
Each mode is synchronized with the other modes, so you can switch between them without losing your work, although there are some limitations. Some more complex queries are not yet supported in the builder mode. If you try to switch from `Code` to `Builder` with such query, editor will show a popup explaining that you can lose some parts of the query, and you can decide if you still want to continue to `Builder` mode or not.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `Query expression` | Prometheus query expression. For more information, refer to the [Prometheus documentation](http://prometheus.io/docs/querying/basics/). |
|
||||
| `Legend format` | Controls the name of the time series, using name or pattern. For example, `{{hostname}}` is replaced by the label value for the label `hostname`. |
|
||||
| `Step` | Use 'Minimum' or 'Maximum' step mode to set the lower or upper bounds respectively on the interval between data points. For example, set "minimum 1h" to hint that measurements are not frequent (taken hourly). Use the 'Exact' step mode to set a precise interval between data points. `$__interval` and `$__rate_interval` are supported. |
|
||||
| `Resolution` | `1/1` sets both the `$__interval` variable and the [`step` parameter of Prometheus range queries](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) such that each pixel corresponds to one data point. For better performance, you can pick lower resolutions. `1/2` only retrieves a data point for every other pixel, and `1/10` retrieves one data point per 10 pixels. Both _Min time interval_ and _Step_ limit the final value of `$__interval` and `step`. |
|
||||
| `Metric lookup` | Search for metric names in this input field. |
|
||||
| `Format as` | You can switch between `Table` `Time series` or `Heatmap` options. The `Table` option works only in the Table panel. `Heatmap` displays metrics of the Histogram type on a Heatmap panel. Under the hood, it converts cumulative histograms to regular ones and sorts series by the bucket bound. |
|
||||
| `Instant` | Perform an "instant" query to return only the latest value that Prometheus has scraped for the requested time series. Instant queries can return results much faster than normal range queries. Use them to look up label sets. |
|
||||
| `Min time interval` | This value multiplied by the denominator from the _Resolution_ setting sets a lower limit to both the `$__interval` variable and the [`step` parameter of Prometheus range queries](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries). Defaults to _Scrape interval_ as specified in the data source options. |
|
||||
| `Exemplars` | Run and show exemplars in the graph. |
|
||||
### Code mode
|
||||
|
||||
> **Note:** Grafana modifies the request dates for queries to align them with the dynamically calculated step. This ensures consistent display of metrics data, but it can result in a small gap of data at the right edge of a graph.
|
||||

|
||||
|
||||
#### Instant queries in dashboards
|
||||
Code mode allows you to write raw queries in a textual editor. It implements advanced autocomplete features and syntax highlighting to help with writing complex queries. In addition, it also contains `Metrics browser` to further aid with writing queries (see more docs below).
|
||||
|
||||
The Prometheus data source allows you to run "instant" queries, which query only the latest value.
|
||||
You can visualize the results in a table panel to see all available labels of a timeseries.
|
||||
For more information about Prometheus query language, refer to the [Prometheus documentation](http://prometheus.io/docs/querying/basics/).
|
||||
|
||||
Instant query results are made up only of one data point per series but can be shown in the graph panel with the help of [series overrides]({{< relref "../visualizations/graph-panel.md#series-overrides" >}}).
|
||||
To show them in the graph as a latest value point, add a series override and select `Points > true`.
|
||||
To show a horizontal line across the whole graph, add a series override and select `Transform > constant`.
|
||||
#### Autocomplete
|
||||
|
||||
> Support for constant series overrides is available from Grafana v6.4
|
||||

|
||||
|
||||
### Query editor in Explore
|
||||
Autocomplete kicks automatically in appropriate times during typing. Use `ctrl/cmd + space` to trigger autocomplete manually when needed. Autocomplete can suggest both static functions, aggregations and keywords but also dynamic items like metrics and labels. Autocomplete dropdown also shows documentation for the suggested items, either static one or dynamic metric documentation where available.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `Query expression` | Prometheus query expression, check out the [Prometheus documentation](http://prometheus.io/docs/querying/basics/). |
|
||||
| `Step` | [`Step` parameter of Prometheus range queries](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries). Time units can be used here, for example: 5s, 1m, 3h, 1d, 1y. Default unit if no unit specified is `s` (seconds). |
|
||||
| `Query type` | `Range`, `Instant`, or `Both`. When running **Range query**, the result of the query is displayed in graph and table. Instant query returns only the latest value that Prometheus has scraped for the requested time series and it is displayed in the table. When **Both** is selected, both instant query and range query is run. Result of range query is displayed in graph and the result of instant query is displayed in the table. |
|
||||
| `Exemplars` | Run and show exemplars in the graph. |
|
||||
In [Explore]({{< relref "../explore/_index.md" >}}) use `shift + enter` to run the query.
|
||||
|
||||
### Metrics browser
|
||||
#### Metrics browser
|
||||
|
||||
The metrics browser allows you to quickly find metrics and select relevant labels to build basic queries.
|
||||
When you open the browser you will see all available metrics and labels.
|
||||
If supported by your Prometheus instance, each metric will show its HELP and TYPE as a tooltip.
|
||||
|
||||
{{< figure src="/static/img/docs/v8/prometheus_metrics_browser.png" class="docs-image--no-shadow" max-width="800px" caption="Screenshot of the metrics browser for Prometheus" >}}
|
||||

|
||||
|
||||
When you select a metric, the browser narrows down the available labels to show only the ones applicable to the metric.
|
||||
You can then select one or more labels for which the available label values are shown in lists in the bottom section.
|
||||
@@ -93,14 +76,77 @@ Select one or more values for each label to tighten your query scope.
|
||||
|
||||
> **Note:** If you do not remember a metric name to start with, you can also select a few labels first, to narrow down the list and then find relevant label values.
|
||||
|
||||
All lists in the metrics browser have a search field above them to quickly filter for metrics or labels that match a certain string. The values section only has one search field. Its filtering applies to all labels to help you find values across labels once they have been selected, for example, among your labels `app`, `job`, `job_name` only one might with the value you are looking for.
|
||||
All lists in the metrics browser have a search field above them to quickly filter for metrics or labels that match a certain string. The values section only has one search field. It's filtering applies to all labels to help you find values across labels once they have been selected, for example, among your labels `app`, `job`, `job_name` only one might with the value you are looking for.
|
||||
|
||||
Once you are satisfied with your query, click "Use query" to run the query. The button "Use as rate query" adds a `rate(...)[$__interval]` around your query to help write queries for counter metrics.
|
||||
The "Validate selector" button will check with Prometheus how many time series are available for that selector.
|
||||
|
||||
#### Limitations
|
||||
#### Options
|
||||
|
||||
The metrics browser has a hard limit of 10,000 labels (keys) and 50,000 label values (including metric names). If your Prometheus instance returns more results, the browser will continue functioning. However, the result sets will be cut off above those maximum limits.
|
||||

|
||||
|
||||
| Name | Description |
|
||||
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Legend` | Controls the name of the time series. Use predefined format or use custom format.<br/>`Auto` - only includes unique labels.<br/>`Verbose` - includes all labels.<br/>`Custom` - select will change to text input. Use use tamplating to select which labels will be included. For example, `{{hostname}}` is replaced by the label value for the label `hostname`. Clear the input and click outside the input to go back to select mode. |
|
||||
| `Min step` | Set the lower bounds on the interval between data points. For example, set "1h" to hint that measurements are not frequent (taken hourly). `$__interval` and `$__rate_interval` are supported. |
|
||||
| `Format` | You can switch between `Table` `Time series` or `Heatmap` options. The `Table` option works only in the Table panel. `Heatmap` displays metrics of the Histogram type on a Heatmap panel. Under the hood, it converts cumulative histograms to regular ones and sorts series by the bucket bound. |
|
||||
| `Type` | `Range` - Query returning a Range vector, a set of time series containing a range of data points over time for each time series.<br/>`Instant` - Perform an "instant" query to return only the latest value that Prometheus has scraped for the requested time series. Instant queries can return results much faster than normal range queries. Use them to look up label sets. Instant query results are made up only of one data point per series but can be shown in the graph panel in a dashboard with the help of [series overrides]({{< relref "../visualizations/graph-panel.md#series-overrides" >}}). To show them in the graph as a latest value point, add a series override and select `Points > true`. To show a horizontal line across the whole graph, add a series override and select `Transform > constant`. <br/>`Both` - Available only in Explore. Runs both range and instant query |
|
||||
| `Exemplars` | If on, run exemplars query with the regular query and show exemplars in the graph. |
|
||||
|
||||
> **Note:** Grafana modifies the request dates for queries to align them with the dynamically calculated step. This ensures consistent display of metrics data, but it can result in a small gap of data at the right edge of a graph.
|
||||
|
||||
### Builder mode
|
||||
|
||||
#### Toolbar
|
||||
|
||||
In addition to `Run query` button and mode switcher, in builder mode additional elements are available:
|
||||
|
||||
| Name | Description |
|
||||
| -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Query patterns | A list of useful operation patterns that can be used to quickly add multiple operations to your query to achieve a specific goal. |
|
||||
| Raw query | Toggle to show raw query generated by the builder that will be sent to Prometheus instance. |
|
||||
|
||||
#### Metric and labels
|
||||
|
||||

|
||||
|
||||
Select a specific metric name from the dropdown list. List of available metrics is fetched from the Prometheus server based on selected time rage. Write into the select when the dropdown is open to search and filter the list.
|
||||
|
||||
Select desired labels and their values from the dropdown list. When metric is selected, available labels and their values are fetched from the server. Use the `+` button to add more labels. Use the `x` button to remove a label.
|
||||
|
||||
#### Operations
|
||||
|
||||

|
||||
|
||||
Use the `+ Operations` button to add operation to your query. Operations are grouped into sections for easier navigation. When the operations dropdown is open, write into the search input to search and filter operations list.
|
||||
|
||||
Operations in a query are shown as boxes in the operations section. Each has a header with a name and additional action buttons. Hover over the operation header to show the action buttons. Click the `v` button to quickly replace the operation with different one of the same type. Click the `info` button to open operations' description tooltip. Click the `x` button to remove the operation.
|
||||
|
||||
Operation can have additional parameters under the operation header. See the operation description or Prometheus docs for more details about each operation.
|
||||
|
||||
Some operations make sense only in specific order, if adding an operation would result in nonsensical query, operation will be added to the correct place. To order operations manually drag operation box by the operation name and drop in appropriate place.
|
||||
|
||||
##### Hints
|
||||
|
||||

|
||||
|
||||
In same cases the query editor can detect which operations would be most appropriate for a selected metric. In such cases it will show a hint next to the `+ Operations` button. Click on the hint to add the operations to your query.
|
||||
|
||||
#### Raw query
|
||||
|
||||

|
||||
|
||||
This section is shown only if the `Raw query` switch from the query editor top toolbar is set to `on`. It shows the raw query that will be created and executed by the query editor.
|
||||
|
||||
#### Options
|
||||
|
||||
Same set of option is available as in the `Code` mode. See the [Code mode options]({{< relref "#options" >}}) for details.
|
||||
|
||||
### Explain mode
|
||||
|
||||

|
||||
|
||||
Explain mode helps with understanding the query. It shows a step by step explanation of all query parts and the operations.
|
||||
|
||||
## Templating
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ Fine-grained access control is available for the following capabilities:
|
||||
- [Provision Grafana]({{< relref "../../administration/provisioning/_index.md" >}})
|
||||
- [Manage reports]({{< relref "../reporting.md" >}})
|
||||
- [View server information]({{< relref "../../administration/view-server/_index.md" >}})
|
||||
- [Manage teams]({{< relref "../../administration/manage-users-and-permissions/manage-teams/_index.md" >}})
|
||||
- [Manage dashboards and folders]({{< relref "../../dashboards/_index.md" >}})
|
||||
- [Manage annotations]({{< relref "../../visualizations/annotations.md" >}})
|
||||
- [Alerting]({{< relref "../../alerting/unified-alerting/_index.md">}})
|
||||
|
||||
To learn about specific endpoints where you can use fine-grained access control, refer to [Permissions]({{< relref "./permissions.md" >}}) and to the relevant [API]({{< relref "../../http_api/_index.md" >}}) documentation.
|
||||
|
||||
@@ -58,8 +62,13 @@ enable = accesscontrol
|
||||
|
||||
You can use `GF_FEATURE_TOGGLES_ENABLE = accesscontrol` environment variable to override the config file configuration and enable fine-grained access control.
|
||||
|
||||
Refer to [Configuring with environment variables]({{< relref "../../administration/configuration.md#configure-with-environment-variables" >}}) for more information.
|
||||
Refer to [Configuring with environment variables]({{< relref "../../administration/configuration.md#/#override-configuration-with-environment-variables" >}}) for more information.
|
||||
|
||||
### Verify if enabled
|
||||
|
||||
You can verify if fine-grained access control is enabled or not by sending an HTTP request to the [Check endpoint]({{< relref "../../http_api/access_control.md#check-if-enabled" >}}).
|
||||
|
||||
## Caveats
|
||||
|
||||
If you have created a folder with unique identifier (uid) set to "general", you will not be able to manage its permissions with fine-grained access control.
|
||||
Any [folder permissions]({{< relref "../../administration/manage-users-and-permissions/manage-dashboard-permissions/_index.md" >}}) set for this folder will be disregarded when fine-grained access control is enabled.
|
||||
|
||||
@@ -54,11 +54,34 @@ The reference information that follows complements conceptual information about
|
||||
| `fixed:annotations.dashboard:writer` | `annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:dashboard` | Create, update and delete dashboard annotations and annotation tags. |
|
||||
| `fixed:annotations:writer` | `annotations:write` <br>`annotations.create`<br> `annotations:delete` for scope `annotations:type:*` | Create, update and delete all annotations and annotation tags. |
|
||||
|
||||
### Alerting roles
|
||||
|
||||
If you [enable]({{< relref "../../alerting/unified-alerting/opt-in.md" >}}) Grafana Alerting, you can use predefined roles to manage user access to alert rules, alert instances, and alert notification settings and create custom roles to limit user access to alert rules in a folder.
|
||||
|
||||
Access to Grafana alert rules is an intersection of many permissions:
|
||||
|
||||
- Permission to read a folder, for example, the fixed role `fixed:folders:reader` or action `folders:read` in the scope of a folder `folders:id:`
|
||||
- Permission to manage alerts. The following table contains information about alerting fixed roles.
|
||||
- Permission to query **all** data sources that the rule uses, for example, the fixed role `fixed:datasources:reader` or action `datasources:query` in the scope of `datasources:uid:`.
|
||||
|
||||
For more information about the permissions required to access alert rules, refer to [Create a custom role to access alerts in a folder]({{< relref "./usage-scenarios.md#create-a-custom-role-to-access-alerts-in-a-folder" >}}).
|
||||
|
||||
| Fixed roles | Permissions | Descriptions |
|
||||
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `fixed:alerting.rules:reader` | `alert.rule:read` for scope `folders:*` <br> `alert.rules.external:read` for scope `datasources:*` | Read all\* Grafana, Mimir, and Loki alert rules |
|
||||
| `fixed:alerting.rules:editor` | All permissions from `fixed:alerting.rules:reader` and <br> `alert.rule:create` <br> `alert.rule:update` <br> `alert.rule:delete` for scope `folders:*` <br> `alert.rules.external:write` for scope `datasources:*` | Create, update, and delete all\* Grafana, Mimir, and Loki alert rules. |
|
||||
| `fixed:alerting.instances:reader` | `alert.instances:read` for organization scope <br> `alert.instances.external:read` for scope `datasources:*` | Read all alerts and silences in the organization produced by Grafana Alerts and Mimir and Loki alerts and silences. |
|
||||
| `fixed:alerting.instances:editor` | All permissions from `fixed:alerting.instances:reader` and<br> `alert.instances:create`<br>`alert.instances:update` for organization scope <br> `alert.instances.external:write` for scope `datasources:*` | Create, update and expire all silences in the organization produced by Grafana, Mimir, and Loki. |
|
||||
| `fixed:alerting.notifications:reader` | `alert.notifications:read` for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Read all Grafana and Alertmanager contact points, templates, and notification policies. |
|
||||
| `fixed:alerting.notifications:editor` | All permissions from `fixed:alerting.notifications:reader` and<br>`alert.notifications:create`<br>`alert.notifications:update`<br>`alert.notifications:delete` for organization scope<br>`alert.notifications.external:read` for scope `datasources:*` | Create, update, and delete contact points, templates, mute timings and notification policies for Grafana and external Alertmanager. |
|
||||
| `fixed:alerting:reader` | All permissions from `fixed:alerting.rules:reader` <br>`fixed:alerting.instances:reader`<br>`fixed:alerting.notifications:reader` | Read-only permissions for all Grafana, Mimir, Loki and Alertmanager alert rules\*, alerts, contact points, and notification policies. |
|
||||
| `fixed:alerting:editor` | All permissions from `fixed:alerting.rules:editor` <br>`fixed:alerting.instances:editor`<br>`fixed:alerting.notifications:editor` | Create, update, and delete Grafana, Mimir, Loki and Alertmanager alert rules\*, silences, contact points, templates, mute timings, and notification policies. |
|
||||
|
||||
## Default built-in role assignments
|
||||
|
||||
| Built-in role | Associated role | Description |
|
||||
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Grafana Admin | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer` | Default [Grafana server administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#grafana-server-administrators" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixes:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer` | Default [Grafana organization administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
|
||||
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled | Default [Editor]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
|
||||
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer` | Default [Viewer]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
|
||||
| Built-in role | Associated role | Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Grafana Admin | `fixed:roles:reader`<br>`fixed:roles:writer`<br>`fixed:users:reader`<br>`fixed:users:writer`<br>`fixed:org.users:reader`<br>`fixed:org.users:writer`<br>`fixed:ldap:reader`<br>`fixed:ldap:writer`<br>`fixed:stats:reader`<br>`fixed:settings:reader`<br>`fixed:settings:writer`<br>`fixed:provisioning:writer`<br>`fixed:organization:reader`<br>`fixed:organization:maintainer`<br>`fixed:licensing:reader`<br>`fixed:licensing:writer` | Default [Grafana server administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#grafana-server-administrators" >}}) assignments. |
|
||||
| Admin | `fixed:reports:reader`<br>`fixed:reports:writer`<br>`fixed:datasources:reader`<br>`fixed:datasources:writer`<br>`fixed:organization:writer`<br>`fixed:datasources.permissions:reader`<br>`fixed:datasources.permissions:writer`<br>`fixed:teams:writer`<br>`fixed:dashboards:reader`<br>`fixed:dashboards:writer`<br>`fixed:dashboards.permissions:reader`<br>`fixed:dashboards.permissions:writer`<br>`fixed:folders:reader`<br>`fixes:folders:writer`<br>`fixed:folders.permissions:reader`<br>`fixed:folders.permissions:writer`<br>`fixed:alerting:editor` | Default [Grafana organization administrator]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
|
||||
| Editor | `fixed:datasources:explorer`<br>`fixed:dashboards:creator`<br>`fixed:folders:creator`<br>`fixed:annotations:writer`<br>`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled<br>`fixed:alerting:editor` | Default [Editor]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
|
||||
| Viewer | `fixed:datasources:id:reader`<br>`fixed:organization:reader`<br>`fixed:annotations:reader`<br>`fixed:annotations.dashboard:writer`<br>`fixed:alerting:reader` | Default [Viewer]({{< relref "../../administration/manage-users-and-permissions/about-users-and-permissions.md#organization-users-and-permissions" >}}) assignments. |
|
||||
|
||||
@@ -23,100 +23,117 @@ scope
|
||||
|
||||
The following list contains fine-grained access control actions.
|
||||
|
||||
| Action | Applicable scope | Description |
|
||||
| ------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `roles:list` | `roles:*` | List available roles without permissions. |
|
||||
| `roles:read` | `roles:*` <br> `roles:uid:*` | Read a specific role with its permissions. |
|
||||
| `roles:write` | `permissions:delegate` | Create or update a custom role. |
|
||||
| `roles:delete` | `permissions:delegate` | Delete a custom role. |
|
||||
| `roles.builtin:list` | `roles:*` | List built-in role assignments. |
|
||||
| `roles.builtin:add` | `permissions:delegate` | Create a built-in role assignment. |
|
||||
| `roles.builtin:remove` | `permissions:delegate` | Delete a built-in role assignment. |
|
||||
| `reports.admin:create` | n/a | Create reports. |
|
||||
| `reports.admin:write` | `reports:*` <br> `reports:id:*` | Update reports. |
|
||||
| `reports:delete` | `reports:*` <br> `reports:id:*` | Delete reports. |
|
||||
| `reports:read` | `reports:*` | List all available reports or get a specific report. |
|
||||
| `reports:send` | `reports:*` | Send a report email. |
|
||||
| `reports.settings:write` | n/a | Update report settings. |
|
||||
| `reports.settings:read` | n/a | Read report settings. |
|
||||
| `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "./permissions.md#scope-definitions" >}}). |
|
||||
| `teams.roles:list` | `teams:*` | List roles assigned directly to a team. |
|
||||
| `teams.roles:add` | `permissions:delegate` | Assign a role to a team. |
|
||||
| `teams.roles:remove` | `permissions:delegate` | Unassign a role from a team. |
|
||||
| `users:read` | `global.users:*` | Read or search user profiles. |
|
||||
| `users:write` | `global.users:*` <br> `global.users:id:*` | Update a user’s profile. |
|
||||
| `users.teams:read` | `global.users:*` <br> `global.users:id:*` | Read a user’s teams. |
|
||||
| `users.authtoken:list` | `global.users:*` <br> `global.users:id:*` | List authentication tokens that are assigned to a user. |
|
||||
| `users.authtoken:update` | `global.users:*` <br> `global.users:id:*` | Update authentication tokens that are assigned to a user. |
|
||||
| `users.password:update` | `global.users:*` <br> `global.users:id:*` | Update a user’s password. |
|
||||
| `users:delete` | `global.users:*` <br> `global.users:id:*` | Delete a user. |
|
||||
| `users:create` | n/a | Create a user. |
|
||||
| `users:enable` | `globa.users:*` <br> `global.users:id:*` | Enable a user. |
|
||||
| `users:disable` | `global.users:*` <br> `global.users:id:*` | Disable a user. |
|
||||
| `users.permissions:update` | `global.users:*` <br> `global.users:id:*` | Update a user’s organization-level permissions. |
|
||||
| `users:logout` | `global.users:*` <br> `global.users:id:*` | Sign out a user. |
|
||||
| `users.quotas:list` | `global.users:*` <br> `global.users:id:*` | List a user’s quotas. |
|
||||
| `users.quotas:update` | `global.users:*` <br> `global.users:id:*` | Update a user’s quotas. |
|
||||
| `users.roles:list` | `users:*` | List roles assigned directly to a user. |
|
||||
| `users.roles:add` | `permissions:delegate` | Assign a role to a user. |
|
||||
| `users.roles:remove` | `permissions:delegate` | Unassign a role from a user. |
|
||||
| `users.permissions:list` | `users:*` | List permissions of a user. |
|
||||
| `org.users:read` | `users:*` <br> `users:id:*` | Get user profiles within an organization. |
|
||||
| `org.users:add` | `users:*` | Add a user to an organization. |
|
||||
| `org.users:remove` | `users:*` <br> `users:id:*` | Remove a user from an organization. |
|
||||
| `org.users.role:update` | `users:*` <br> `users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of an organization. |
|
||||
| `orgs:read` | `orgs:*` <br> `orgs:id:*` | Read one or more organizations. |
|
||||
| `orgs:write` | `orgs:*` <br> `orgs:id:*` | Update one or more organizations. |
|
||||
| `org:create` | n/a | Create an organization. |
|
||||
| `orgs:delete` | `orgs:*` <br> `orgs:id:*` | Delete one or more organizations. |
|
||||
| `orgs.quotas:read` | `orgs:*` <br> `orgs:id:*` | Read organization quotas. |
|
||||
| `orgs.quotas:write` | `orgs:*` <br> `orgs:id:*` | Update organization quotas. |
|
||||
| `orgs.preferences:read` | `orgs:*` <br> `orgs:id:*` | Read organization preferences. |
|
||||
| `orgs.preferences:write` | `orgs:*` <br> `orgs:id:*` | Update organization preferences. |
|
||||
| `ldap.user:read` | n/a | Read users via LDAP. |
|
||||
| `ldap.user:sync` | n/a | Sync users via LDAP. |
|
||||
| `ldap.status:read` | n/a | Verify the availability of the LDAP server or servers. |
|
||||
| `ldap.config:reload` | n/a | Reload the LDAP configuration. |
|
||||
| `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. |
|
||||
| `settings:read` | `settings:*`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../administration/configuration/_index.md" >}}) |
|
||||
| `settings:write` | `settings:*`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../../enterprise/settings-updates/_index.md" >}}). |
|
||||
| `server.stats:read` | n/a | Read Grafana instance statistics. |
|
||||
| `datasources:explore` | n/a | Enable access to the **Explore** tab. |
|
||||
| `datasources:read` | n/a<br>`datasources:*`<br>`datasources:id:*`<br>`datasources:uid:*`<br>`datasources:name:*` | List data sources. |
|
||||
| `datasources:query` | n/a<br>`datasources:*`<br>`datasources:id:*` | Query data sources. |
|
||||
| `datasources.id:read` | `datasources:*`<br>`datasources:name:*` | Read data source IDs. |
|
||||
| `datasources:create` | n/a | Create data sources. |
|
||||
| `datasources:write` | `datasources:*`<br>`datasources:id:*` | Update data sources. |
|
||||
| `datasources:delete` | `datasources:id:*`<br>`datasources:uid:*`<br>`datasources:name:*` | Delete data sources. |
|
||||
| `datasources.permissions:read` | `datasources:*`<br>`datasources:id:*` | List data source permissions. |
|
||||
| `datasources.permissions:write` | `datasources:*`<br>`datasources:id:*` | Update data source permissions. |
|
||||
| `licensing:read` | n/a | Read licensing information. |
|
||||
| `licensing:update` | n/a | Update the license token. |
|
||||
| `licensing:delete` | n/a | Delete the license token. |
|
||||
| `licensing.reports:read` | n/a | Get custom permission reports. |
|
||||
| `teams:create` | n/a | Create teams. |
|
||||
| `teams:read` | `teams:*`<br>`teams:id:*` | Read one or more teams and team preferences. |
|
||||
| `teams:write` | `teams:*`<br>`teams:id:*` | Update one or more teams and team preferences. |
|
||||
| `teams:delete` | `teams:*`<br>`teams:id:*` | Delete one or more teams. |
|
||||
| `teams.permissions:read` | `teams:*`<br>`teams:id:*` | Read members and External Group Synchronization setup for teams. |
|
||||
| `teams.permissions:write` | `teams:*`<br>`teams:id:*` | Add, remove and update members and manage External Group Synchronization setup for teams. |
|
||||
| `dashboards:read` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Read one or more dashboards. |
|
||||
| `dashboards:create` | `folders:*`<br>`folders:id:*` | Create dashboards in one or more folders. |
|
||||
| `dashboards:write` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Update one or more dashboards. |
|
||||
| `dashboards:edit` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Edit one or more dashboards (only in ui). |
|
||||
| `dashboards:delete` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Delete one or more dashboards. |
|
||||
| `dashboards.permissions:read` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Read permissions for one or more dashboards. |
|
||||
| `dashboards.permissions:write` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Update permissions for one or more dashboards. |
|
||||
| `folders:read` | `folders:*`<br>`folders:id:*` | Read one or more folders. |
|
||||
| `folders:create` | n/a | Create folders. |
|
||||
| `folders:write` | `folders:*`<br>`folders:id:*` | Update one or more folders. |
|
||||
| `folders:delete` | `folders:*`<br>`folders:id:*` | Delete one or more folders. |
|
||||
| `folers.permissions:read` | `folders:*`<br>`folders:id:*` | Read permissions for one or more folders. |
|
||||
| `folders.permissions:write` | `folders:*`<br>`folders:id:*` | Update permissions for one or more folders. |
|
||||
| `annotations.read` | `annotations:*`<br>`annotations:type:*` | Read annotations and annotation tags. |
|
||||
| `annotations.create` | `annotations:*`<br>`annotations:type:*` | Create annotations. |
|
||||
| `annotations.write` | `annotations:*`<br>`annotations:type:*` | Update annotations. |
|
||||
| `annotations.delete` | `annotations:*`<br>`annotations:type:*` | Delete annotations. |
|
||||
| Action | Applicable scope | Description |
|
||||
| ------------------------------------ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `roles:list` | `roles:*` | List available roles without permissions. |
|
||||
| `roles:read` | `roles:*` <br> `roles:uid:*` | Read a specific role with its permissions. |
|
||||
| `roles:write` | `permissions:delegate` | Create or update a custom role. |
|
||||
| `roles:delete` | `permissions:delegate` | Delete a custom role. |
|
||||
| `roles.builtin:list` | `roles:*` | List built-in role assignments. |
|
||||
| `roles.builtin:add` | `permissions:delegate` | Create a built-in role assignment. |
|
||||
| `roles.builtin:remove` | `permissions:delegate` | Delete a built-in role assignment. |
|
||||
| `reports.admin:create` | n/a | Create reports. |
|
||||
| `reports.admin:write` | `reports:*` <br> `reports:id:*` | Update reports. |
|
||||
| `reports:delete` | `reports:*` <br> `reports:id:*` | Delete reports. |
|
||||
| `reports:read` | `reports:*` | List all available reports or get a specific report. |
|
||||
| `reports:send` | `reports:*` | Send a report email. |
|
||||
| `reports.settings:write` | n/a | Update report settings. |
|
||||
| `reports.settings:read` | n/a | Read report settings. |
|
||||
| `provisioning:reload` | `provisioners:*` | Reload provisioning files. To find the exact scope for specific provisioner, see [Scope definitions]({{< relref "./permissions.md#scope-definitions" >}}). |
|
||||
| `teams.roles:list` | `teams:*` | List roles assigned directly to a team. |
|
||||
| `teams.roles:add` | `permissions:delegate` | Assign a role to a team. |
|
||||
| `teams.roles:remove` | `permissions:delegate` | Unassign a role from a team. |
|
||||
| `users:read` | `global.users:*` | Read or search user profiles. |
|
||||
| `users:write` | `global.users:*` <br> `global.users:id:*` | Update a user’s profile. |
|
||||
| `users.teams:read` | `global.users:*` <br> `global.users:id:*` | Read a user’s teams. |
|
||||
| `users.authtoken:list` | `global.users:*` <br> `global.users:id:*` | List authentication tokens that are assigned to a user. |
|
||||
| `users.authtoken:update` | `global.users:*` <br> `global.users:id:*` | Update authentication tokens that are assigned to a user. |
|
||||
| `users.password:update` | `global.users:*` <br> `global.users:id:*` | Update a user’s password. |
|
||||
| `users:delete` | `global.users:*` <br> `global.users:id:*` | Delete a user. |
|
||||
| `users:create` | n/a | Create a user. |
|
||||
| `users:enable` | `globa.users:*` <br> `global.users:id:*` | Enable a user. |
|
||||
| `users:disable` | `global.users:*` <br> `global.users:id:*` | Disable a user. |
|
||||
| `users.permissions:update` | `global.users:*` <br> `global.users:id:*` | Update a user’s organization-level permissions. |
|
||||
| `users:logout` | `global.users:*` <br> `global.users:id:*` | Sign out a user. |
|
||||
| `users.quotas:list` | `global.users:*` <br> `global.users:id:*` | List a user’s quotas. |
|
||||
| `users.quotas:update` | `global.users:*` <br> `global.users:id:*` | Update a user’s quotas. |
|
||||
| `users.roles:list` | `users:*` | List roles assigned directly to a user. |
|
||||
| `users.roles:add` | `permissions:delegate` | Assign a role to a user. |
|
||||
| `users.roles:remove` | `permissions:delegate` | Unassign a role from a user. |
|
||||
| `users.permissions:list` | `users:*` | List permissions of a user. |
|
||||
| `org.users:read` | `users:*` <br> `users:id:*` | Get user profiles within an organization. |
|
||||
| `org.users:add` | `users:*` | Add a user to an organization. |
|
||||
| `org.users:remove` | `users:*` <br> `users:id:*` | Remove a user from an organization. |
|
||||
| `org.users.role:update` | `users:*` <br> `users:id:*` | Update the organization role (`Viewer`, `Editor`, or `Admin`) of an organization. |
|
||||
| `orgs:read` | `orgs:*` <br> `orgs:id:*` | Read one or more organizations. |
|
||||
| `orgs:write` | `orgs:*` <br> `orgs:id:*` | Update one or more organizations. |
|
||||
| `org:create` | n/a | Create an organization. |
|
||||
| `orgs:delete` | `orgs:*` <br> `orgs:id:*` | Delete one or more organizations. |
|
||||
| `orgs.quotas:read` | `orgs:*` <br> `orgs:id:*` | Read organization quotas. |
|
||||
| `orgs.quotas:write` | `orgs:*` <br> `orgs:id:*` | Update organization quotas. |
|
||||
| `orgs.preferences:read` | `orgs:*` <br> `orgs:id:*` | Read organization preferences. |
|
||||
| `orgs.preferences:write` | `orgs:*` <br> `orgs:id:*` | Update organization preferences. |
|
||||
| `ldap.user:read` | n/a | Read users via LDAP. |
|
||||
| `ldap.user:sync` | n/a | Sync users via LDAP. |
|
||||
| `ldap.status:read` | n/a | Verify the availability of the LDAP server or servers. |
|
||||
| `ldap.config:reload` | n/a | Reload the LDAP configuration. |
|
||||
| `status:accesscontrol` | `services:accesscontrol` | Get access-control enabled status. |
|
||||
| `settings:read` | `settings:*`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Read the [Grafana configuration settings]({{< relref "../../administration/configuration/_index.md" >}}) |
|
||||
| `settings:write` | `settings:*`<br>`settings:auth.saml:*`<br>`settings:auth.saml:enabled` (property level) | Update any Grafana configuration settings that can be [updated at runtime]({{< relref "../../enterprise/settings-updates/_index.md" >}}). |
|
||||
| `server.stats:read` | n/a | Read Grafana instance statistics. |
|
||||
| `datasources:explore` | n/a | Enable access to the **Explore** tab. |
|
||||
| `datasources:read` | n/a<br>`datasources:*`<br>`datasources:id:*`<br>`datasources:uid:*`<br>`datasources:name:*` | List data sources. |
|
||||
| `datasources:query` | n/a<br>`datasources:*`<br>`datasources:id:*` | Query data sources. |
|
||||
| `datasources.id:read` | `datasources:*`<br>`datasources:name:*` | Read data source IDs. |
|
||||
| `datasources:create` | n/a | Create data sources. |
|
||||
| `datasources:write` | `datasources:*`<br>`datasources:id:*` | Update data sources. |
|
||||
| `datasources:delete` | `datasources:id:*`<br>`datasources:uid:*`<br>`datasources:name:*` | Delete data sources. |
|
||||
| `datasources.permissions:read` | `datasources:*`<br>`datasources:id:*` | List data source permissions. |
|
||||
| `datasources.permissions:write` | `datasources:*`<br>`datasources:id:*` | Update data source permissions. |
|
||||
| `licensing:read` | n/a | Read licensing information. |
|
||||
| `licensing:update` | n/a | Update the license token. |
|
||||
| `licensing:delete` | n/a | Delete the license token. |
|
||||
| `licensing.reports:read` | n/a | Get custom permission reports. |
|
||||
| `teams:create` | n/a | Create teams. |
|
||||
| `teams:read` | `teams:*`<br>`teams:id:*` | Read one or more teams and team preferences. |
|
||||
| `teams:write` | `teams:*`<br>`teams:id:*` | Update one or more teams and team preferences. |
|
||||
| `teams:delete` | `teams:*`<br>`teams:id:*` | Delete one or more teams. |
|
||||
| `teams.permissions:read` | `teams:*`<br>`teams:id:*` | Read members and External Group Synchronization setup for teams. |
|
||||
| `teams.permissions:write` | `teams:*`<br>`teams:id:*` | Add, remove and update members and manage External Group Synchronization setup for teams. |
|
||||
| `dashboards:read` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Read one or more dashboards. |
|
||||
| `dashboards:create` | `folders:*`<br>`folders:id:*` | Create dashboards in one or more folders. |
|
||||
| `dashboards:write` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Update one or more dashboards. |
|
||||
| `dashboards:edit` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Edit one or more dashboards (only in ui). |
|
||||
| `dashboards:delete` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Delete one or more dashboards. |
|
||||
| `dashboards.permissions:read` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Read permissions for one or more dashboards. |
|
||||
| `dashboards.permissions:write` | `dashboards:*`<br>`dashboards:id:*`<br>`folders:*`<br>`folders:id:*` | Update permissions for one or more dashboards. |
|
||||
| `folders:read` | `folders:*`<br>`folders:id:*` | Read one or more folders. |
|
||||
| `folders:create` | n/a | Create folders. |
|
||||
| `folders:write` | `folders:*`<br>`folders:id:*` | Update one or more folders. |
|
||||
| `folders:delete` | `folders:*`<br>`folders:id:*` | Delete one or more folders. |
|
||||
| `folers.permissions:read` | `folders:*`<br>`folders:id:*` | Read permissions for one or more folders. |
|
||||
| `folders.permissions:write` | `folders:*`<br>`folders:id:*` | Update permissions for one or more folders. |
|
||||
| `annotations.read` | `annotations:*`<br>`annotations:type:*` | Read annotations and annotation tags. |
|
||||
| `annotations.create` | `annotations:*`<br>`annotations:type:*` | Create annotations. |
|
||||
| `annotations.write` | `annotations:*`<br>`annotations:type:*` | Update annotations. |
|
||||
| `annotations.delete` | `annotations:*`<br>`annotations:type:*` | Delete annotations. |
|
||||
| `alert.rules:read` | `folders:*`<br>`folders:id:*` | Read Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
|
||||
| `alert.rules:create` | `folders:*`<br>`folders:id:*` | Create Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
|
||||
| `alert.rules:update` | `folders:*`<br>`folders:id:*` | Update Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
|
||||
| `alert.rules:delete` | `folders:*`<br>`folders:id:*` | Delete Grafana alert rules in a folder. Combine this permission with `folders:read` in a scope that includes the folder and `datasources:query` in the scope of data sources the user can query. |
|
||||
| `alert.rules.external:read` | `datasources:*`<br>`datasources:uid:*` | Read alert rules in data sources that support alerting (Prometheus, Mimir, and Loki) |
|
||||
| `alert.rules.external:write` | `datasources:*`<br>`datasources:uid:*` | Create, update, and delete alert rules in data sources that support alerting (Mimir and Loki). |
|
||||
| `alert.instances:read` | n/a | Read alerts and silences in the current organization. |
|
||||
| `alert.instances:create` | n/a | Create silences in the current organization. |
|
||||
| `alert.instances:update` | n/a | Update and expire silences in the current organization. |
|
||||
| `alert.instances.external:read` | `datasources:*`<br>`datasources:uid:*` | Read alerts and silences in data sources that support alerting. |
|
||||
| `alert.instances.external:write` | `datasources:*`<br>`datasources:uid:*` | Manage alerts and silences in data sources that support alerting. |
|
||||
| `alert.notifications:create` | n/a | Create templates, contact points, notification policies, and mute timings in the current organization. |
|
||||
| `alert.notifications:read` | n/a | Read all templates, contact points, notification policies, and mute timings in the current organization. |
|
||||
| `alert.notifications:update` | n/a | Update templates, contact points, notification policies, and mute timings in the current organization. |
|
||||
| `alert.notifications:delete` | n/a | Delete templates, contact points, notification policies, and mute timings in the current organization. |
|
||||
| `alert.notifications.external:read` | `datasources:*`<br>`datasources:uid:*` | Read templates, contact points, notification policies, and mute timings in data sources that support alerting. |
|
||||
| `alert.notifications.external:write` | `datasources:*`<br>`datasources:uid:*` | Manage templates, contact points, notification policies, and mute timings in data sources that support alerting. |
|
||||
|
||||
## Scope definitions
|
||||
|
||||
|
||||
@@ -231,3 +231,46 @@ By default, the Grafana Server Admin is the only user who can create and manage
|
||||
1. [Create a custom role]({{< ref "#create-your-custom-role" >}}) with `roles.builtin:add` and `roles:write` permissions, then create a built-in role assignment for `Editor` organization role.
|
||||
|
||||
Note that any user with the ability to modify roles can only create, update or delete roles with permissions they themselves have been granted. For example, a user with the `Editor` role would be able to create and manage roles only with the permissions they have, or with a subset of them.
|
||||
|
||||
## Create a custom role to access alerts in a folder
|
||||
|
||||
To see an alert rule in Grafana, the user must have read access to the folder that stores the alert rule, permission to read alerts in the folder, and permission to query all data sources that the rule uses.
|
||||
|
||||
The API command in this example is based on the following:
|
||||
|
||||
- A `Test-Folder` with ID `92`
|
||||
- Two data sources: `DS1` with UID `_oAfGYUnk`, and `DS2` with UID `YYcBGYUnk`
|
||||
- An alert rule that is stored in `Test-Folder` and queries the two data sources.
|
||||
The following request creates a custom role that includes permissions to access the alert rule:
|
||||
|
||||
```
|
||||
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
|
||||
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"version": 1,
|
||||
"name": "custom:alerts.reader.in.folder.123",
|
||||
"displayName": "Read-only access to alerts in folder Test-Folder",
|
||||
"description": "Let user query DS1 and DS2, and read alerts in folder Test-Folders",
|
||||
"group":"Custom",
|
||||
"global": true,
|
||||
"permissions": [
|
||||
{
|
||||
"action": "folders:read",
|
||||
"scope": "folders:id:92"
|
||||
},
|
||||
{
|
||||
"action": "alert.rules:read",
|
||||
"scope": "folders:id:92"
|
||||
},
|
||||
{
|
||||
"action": "datasources:query",
|
||||
"scope": "datasources:uid:_oAfGYUnk"
|
||||
},
|
||||
{
|
||||
"action": "datasources:query",
|
||||
"scope": "datasources:uid:YYcBGYUnk"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ Your Grafana license includes a maximum number of _Viewer_ and _Editor/Admin_ ac
|
||||
|
||||
- An _active user_ is a user who has signed in to Grafana within the last 30 days. This is a rolling window that is updated daily.
|
||||
- When you reach the number of maximum active viewers or editor/admins, only currently active users can sign in; new users and non-active users cannot sign in when you reach the limit.
|
||||
- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/ddmins, new editors and admins can still sign in.
|
||||
- Grafana applies sign-in restrictions separately for viewers and editor/admins. If your Grafana license reaches its limit of active viewers but not its limit of active editor/admins, new editors and admins can still sign in.
|
||||
- The number of dashboards that a user can view or edit, and the number of organizations that they can access does not affect the active user count. A user with editor permissions for many dashboards across many different organizations counts as one editor.
|
||||
- A license limit banner appears to administrators when Grafana reaches its active user limit; editors and viewers do not see the banner.
|
||||
To change user roles to make better use of your licenses, refer to [Optimize your tiered license](#optimize-your-tiered-license).
|
||||
@@ -123,7 +123,7 @@ After you apply the token, Grafana Enterprise resets your license and updates th
|
||||
|
||||
> If you are running Grafana Enterprise 8.2 or earlier, the license grants you the total number of licensed users _for each user type_.
|
||||
|
||||
For example, if your current license includes 60 viewers and 40 dditor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel.
|
||||
For example, if your current license includes 60 viewers and 40 editor/admins, the new license includes 100 viewers and 100 editor/admins. Grafana Enterprise 8.3 removes the distinction between viewers and editor/admins as shown on the **Utilization** panel.
|
||||
|
||||
Before you upgrade to Grafana 8.3, ensure that the total number of active users in Grafana does not exceed the number of users in your combined license. If it does, then new users cannot sign in to Grafana 8.3 until the active user count returns below the licensed limit.
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ The table below describes all SAML configuration options. Continue reading below
|
||||
| ---------------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
||||
| `enabled` | No | Whether SAML authentication is allowed | `false` |
|
||||
| `single_logout` | No | Whether SAML Single Logout enabled | `false` |
|
||||
| `allow_sign_up` | No | Whether to allow new Grafana user creation through SAML login. If set to `false`, then only existing Grafana users can log in with SAML. | `true` |
|
||||
| `allow_idp_initiated` | No | Whether SAML IdP-initiated login is allowed | `false` |
|
||||
| `certificate` or `certificate_path` | Yes | Base64-encoded string or Path for the SP X.509 certificate | |
|
||||
| `private_key` or `private_key_path` | Yes | Base64-encoded string or Path for the SP private key | |
|
||||
@@ -142,6 +143,10 @@ For Grafana to map the user information, it looks at the individual attributes w
|
||||
|
||||
Grafana provides configuration options that let you modify which keys to look at for these values. The data we need to create the user in Grafana is Name, Login handle, and email.
|
||||
|
||||
### Allow new user signups
|
||||
|
||||
By default, new Grafana users using SAML authentication will have an account created for them automatically. To decouple authentication and account creation and ensure only users with existing accounts can log in with SAML, set the `allow_sign_up` option to false.
|
||||
|
||||
### Configure team sync
|
||||
|
||||
> Team sync support for SAML only available in Grafana v7.0+
|
||||
|
||||
@@ -55,16 +55,16 @@ In split view, timepickers for both panels can be linked (if you change one, the
|
||||
|
||||
To close the newly created query, click on the Close Split button.
|
||||
|
||||
## Navigate between Explore and a dashboard
|
||||
|
||||
To help accelerate workflows that involve regularly switching from Explore to a dashboard and vice-versa, Grafana provides you with the ability to return to the origin dashboard after navigating to Explore from the panel's dropdown.
|
||||
|
||||
After you've navigated to Explore, you should notice a "Back" button in the Explore toolbar. Simply click it to return to the origin dashboard. To bring changes you make in Explore back to the dashboard, click the arrow next to the button to reveal a "Return to panel with changes" menu item.
|
||||
|
||||
{{< figure src="/static/img/docs/explore/explore_return_dropdown-7-4.png" class="docs-image--no-shadow" max-width= "400px" caption="Screenshot of the expanded explore return dropdown" >}}
|
||||
|
||||
## Share shortened link
|
||||
|
||||
> **Note:** Available in Grafana 7.3 and later versions.
|
||||
|
||||
The Share shortened link capability allows you to create smaller and simpler URLs of the format /goto/:uid instead of using longer URLs with query parameters. To create a shortened link to the executed query, click the **Share** option in the Explore toolbar. A shortened link that is never used will automatically get deleted after seven (7) days.
|
||||
|
||||
## Available feature toggles
|
||||
|
||||
### explore2Dashboard
|
||||
|
||||
> **Note:** Available in Grafana 8.5.0 and later versions.
|
||||
|
||||
Enabled by default, allows users to create panels in dashboards from within Explore.
|
||||
|
||||
@@ -97,6 +97,17 @@ You can change the order of received logs from the default descending order (new
|
||||
|
||||
Each log row has an extendable area with its labels and detected fields, for more robust interaction. For all labels we have added the ability to filter for (positive filter) and filter out (negative filter) selected labels. Each field or label also has a stats icon to display ad-hoc statistics in relation to all displayed logs.
|
||||
|
||||
### Escaping newlines
|
||||
|
||||
Explore automatically detects some incorrectly escaped sequences in log lines, such as newlines (`\n`, `\r`) or tabs (`\t`). When it detects such sequences, Explore provides an "Escape newlines" option.
|
||||
|
||||
To automatically fix incorrectly escaped sequences that Explore has detected:
|
||||
|
||||
1. Click "Escape newlines" to replace the sequences.
|
||||
2. Manually review the replacements to confirm their correctness.
|
||||
|
||||
Explore replaces these sequences. When it does so, the option will change from "Escape newlines" to "Remove escaping". Evaluate the changes as the parsing may not be accurate based on the input received. You can revert the replacements by clicking "Remove escaping".
|
||||
|
||||
#### Derived fields links
|
||||
|
||||
By using Derived fields, you can turn any part of a log message into an internal or external link. The created link is visible as a button next to the Detected field in the Log details view.
|
||||
|
||||
@@ -20,9 +20,9 @@ This is the API documentation for the new Grafana Annotations feature released i
|
||||
|
||||
See note in the [introduction]({{< ref "#annotations-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| ---------------- | -------------- |
|
||||
| annotations:read | annotations:\* |
|
||||
| Action | Scope |
|
||||
| ---------------- | ----------------------- |
|
||||
| annotations:read | annotations:type:<type> |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
|
||||
338
docs/sources/http_api/serviceaccount.md
Normal file
338
docs/sources/http_api/serviceaccount.md
Normal file
@@ -0,0 +1,338 @@
|
||||
+++
|
||||
title = "Service account HTTP API "
|
||||
description = "Grafana service account HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "serviceaccount"]
|
||||
aliases = ["/docs/grafana/latest/http_api/serviceaccount/"]
|
||||
+++
|
||||
|
||||
# Service account API
|
||||
|
||||
> If you are running Grafana Enterprise and have [Fine-grained access control]({{< relref "../enterprise/access-control/_index.md" >}}) enabled, for some endpoints you would need to have relevant permissions.
|
||||
> Refer to specific resources to understand what permissions are required.
|
||||
|
||||
## Search service accounts with Paging
|
||||
|
||||
`GET /api/serviceaccounts/search?perpage=10&page=1&query=myserviceaccount`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#user-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| -------------------- | ------------------------- |
|
||||
| serviceaccounts:read | global:serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/serviceaccounts/search?perpage=10&page=1&query=mygraf HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the user list E.g. if `totalCount` is equal to 100 users and the `perpage` parameter is set to 10 then there are 10 pages of users. The `query` parameter is optional and it will return results where the query value is contained in one of the `name`. Query values with spaces need to be URL encoded e.g. `query=Jane%20Doe`.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
{
|
||||
```
|
||||
|
||||
## Create service account
|
||||
|
||||
`POST /api/serviceaccounts`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| --------------------- | ------------------ |
|
||||
| serviceaccounts:write | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/serviceaccounts HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Get single serviceaccount by Id
|
||||
|
||||
`GET /api/serviceaccounts/:id`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| -------------------- | ------------------ |
|
||||
| serviceaccounts:read | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/serviceaccounts/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Update service account
|
||||
|
||||
`PATCH /api/serviceaccounts/:id`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| --------------------- | ------------------ |
|
||||
| serviceaccounts:write | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
PUT /api/serviceaccounts/2 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service account tokens
|
||||
|
||||
## Get service account tokens
|
||||
|
||||
`GET /api/serviceaccounts/:id/tokens`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| -------------------- | ------------------ |
|
||||
| serviceaccounts:read | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
GET /api/serviceaccounts/2/tokens HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Create service account tokens
|
||||
|
||||
`POST /api/serviceaccounts/:id/tokens`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| --------------------- | ------------------ |
|
||||
| serviceaccounts:write | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/serviceaccounts/2/tokens HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
## Delete service account tokens
|
||||
|
||||
`DELETE /api/serviceaccounts/:id/tokens/:tokenId`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| --------------------- | ------------------ |
|
||||
| serviceaccounts:write | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/serviceaccounts/2/tokens/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
```http
|
||||
GET /api/serviceaccounts/2/tokens HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "grafana",
|
||||
"role": "Viewer",
|
||||
"created": "2022-03-23T10:31:02Z",
|
||||
"expiration": null,
|
||||
"secondsUntilExpiration": 0,
|
||||
"hasExpired": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Create service account tokens
|
||||
|
||||
`POST /api/serviceaccounts/:id/tokens`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| --------------------- | ------------------ |
|
||||
| serviceaccounts:write | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
POST /api/serviceaccounts/2/tokens HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
|
||||
{
|
||||
"name": "grafana",
|
||||
"role": "Viewer"
|
||||
}
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": 7,
|
||||
"name": "grafana",
|
||||
"key": "eyJrIjoiVjFxTHZ6dGdPSjg5Um92MjN1RlhjMkNqYkZUbm9jYkwiLCJuIjoiZ3JhZmFuYSIsImlkIjoxfQ=="
|
||||
}
|
||||
```
|
||||
|
||||
## Delete service account tokens
|
||||
|
||||
`DELETE /api/serviceaccounts/:id/tokens/:tokenId`
|
||||
|
||||
#### Required permissions
|
||||
|
||||
See note in the [introduction]({{< ref "#serviceaccount-api" >}}) for an explanation.
|
||||
|
||||
| Action | Scope |
|
||||
| --------------------- | ------------------ |
|
||||
| serviceaccounts:write | serviceaccounts:\* |
|
||||
|
||||
**Example Request**:
|
||||
|
||||
```http
|
||||
DELETE /api/serviceaccounts/2/tokens/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Basic YWRtaW46YWRtaW4=
|
||||
```
|
||||
|
||||
Requires basic authentication and that the authenticated user is a Grafana Admin.
|
||||
|
||||
**Example Response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "API key deleted"
|
||||
}
|
||||
```
|
||||
@@ -16,6 +16,8 @@ You can install and run Grafana using the official Docker images. Our docker ima
|
||||
|
||||
Each edition is available in two variants: Alpine and Ubuntu. See below.
|
||||
|
||||
For documentation regarding the configuration of a docker image, refer to [configure a Grafana Docker image](https://grafana.com/docs/grafana/latest/administration/configure-docker/).
|
||||
|
||||
This topic also contains important information about [migrating from earlier Docker image versions](#migrate-from-previous-docker-containers-versions).
|
||||
|
||||
> **Note:** You can use [Grafana Cloud](https://grafana.com/products/cloud/features/#cloud-logs) to avoid the overhead of installing, maintaining, and scaling your observability stack. The free forever plan includes Grafana, 10K Prometheus series, 50 GB logs, and more.[Create a free account to get started](https://grafana.com/auth/sign-up/create-user?pg=docs-grafana-install&plcmt=in-text).
|
||||
|
||||
@@ -8,6 +8,9 @@ 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.5.0-beta1]({{< relref "release-notes-8-5-0-beta1" >}})
|
||||
- [Release notes for 8.4.7]({{< relref "release-notes-8-4-7" >}})
|
||||
- [Release notes for 8.4.6]({{< relref "release-notes-8-4-6" >}})
|
||||
- [Release notes for 8.4.5]({{< relref "release-notes-8-4-5" >}})
|
||||
- [Release notes for 8.4.4]({{< relref "release-notes-8-4-4" >}})
|
||||
- [Release notes for 8.4.3]({{< relref "release-notes-8-4-3" >}})
|
||||
|
||||
10
docs/sources/release-notes/release-notes-8-4-6.md
Normal file
10
docs/sources/release-notes/release-notes-8-4-6.md
Normal file
@@ -0,0 +1,10 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.4.6"
|
||||
hide_menu = true
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.4.6
|
||||
|
||||
- **Security:** Fixes CVE-2022-24812. For more information, see our [blog](https://grafana.com/blog/2022/04/12/grafana-enterprise-8.4.6-released-with-high-severity-security-fix/)
|
||||
19
docs/sources/release-notes/release-notes-8-4-7.md
Normal file
19
docs/sources/release-notes/release-notes-8-4-7.md
Normal file
@@ -0,0 +1,19 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.4.7"
|
||||
hide_menu = true
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.4.7
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- **CloudWatch:** Added missing MemoryDB Namespace metrics. [#47290](https://github.com/grafana/grafana/pull/47290), [@james-deee](https://github.com/james-deee)
|
||||
- **Histogram Panel:** Take decimal into consideration. [#47330](https://github.com/grafana/grafana/pull/47330), [@mdvictor](https://github.com/mdvictor)
|
||||
- **TimeSeries:** Sort tooltip values based on raw values. [#46738](https://github.com/grafana/grafana/pull/46738), [@dprokop](https://github.com/dprokop)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **API:** Include userId, orgId, uname in request logging middleware. [#47183](https://github.com/grafana/grafana/pull/47183), [@marefr](https://github.com/marefr)
|
||||
- **Elasticsearch:** Respect maxConcurrentShardRequests datasource setting. [#47120](https://github.com/grafana/grafana/pull/47120), [@alexandrst88](https://github.com/alexandrst88)
|
||||
30
docs/sources/release-notes/release-notes-8-5-0-beta1.md
Normal file
30
docs/sources/release-notes/release-notes-8-5-0-beta1.md
Normal file
@@ -0,0 +1,30 @@
|
||||
+++
|
||||
title = "Release notes for Grafana 8.5.0-beta1"
|
||||
hide_menu = true
|
||||
+++
|
||||
|
||||
<!-- Auto generated by update changelog github action -->
|
||||
|
||||
# Release notes for Grafana 8.5.0-beta1
|
||||
|
||||
### Features and enhancements
|
||||
|
||||
- Add config option to enable/disable reporting. (Enterprise)
|
||||
- **Alerting:** Accurately set value for prom-compatible APIs. [#47216](https://github.com/grafana/grafana/pull/47216), [@gotjosh](https://github.com/gotjosh)
|
||||
- **Alerting:** Provisioning API - Notification Policies. [#46755](https://github.com/grafana/grafana/pull/46755), [@alexweav](https://github.com/alexweav)
|
||||
- **Analytics:** Enable grafana and plugin update checks to be operated independently. [#46352](https://github.com/grafana/grafana/pull/46352), [@wbrowne](https://github.com/wbrowne)
|
||||
- **Azure Monitor:** Add support for multiple template variables in resource picker. [#46215](https://github.com/grafana/grafana/pull/46215), [@sarahzinger](https://github.com/sarahzinger)
|
||||
- **Caching:** Add separate TTL for resources cache. (Enterprise)
|
||||
- **Caching:** add support for TLS configuration for Redis Cluster. (Enterprise)
|
||||
- **NewsPanel:** Remove Use Proxy option and update documentation with recommendations. [#47189](https://github.com/grafana/grafana/pull/47189), [@joshhunt](https://github.com/joshhunt)
|
||||
- **OAuth:** Sync GitHub OAuth user name to Grafana if it's set. [#45438](https://github.com/grafana/grafana/pull/45438), [@pallxk](https://github.com/pallxk)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- **Plugins:** Fix Default Nav URL for dashboard includes. [#47143](https://github.com/grafana/grafana/pull/47143), [@wbrowne](https://github.com/wbrowne)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
When user is using Github OAuth, GitHub login is showed as both Grafana login and name. Now the GitHub name is showed as Grafana name, and GitHub login is showed as Grafana Login. Issue [#45438](https://github.com/grafana/grafana/issues/45438)
|
||||
|
||||
The meaning of the default data source has now changed from being a persisted property in a panel. Before when you selected the default data source for a panel and later changed the default data source to another data source it would change all panels who were configured to use the default data source. From now on the default data source is just the default for new panels and changing the default will not impact any currently saved dashboards. Issue [#45132](https://github.com/grafana/grafana/issues/45132)
|
||||
@@ -11,6 +11,7 @@ as info on deprecations, breaking changes and plugin development read the [relea
|
||||
|
||||
## Grafana 8
|
||||
|
||||
- [What's new in 8.5]({{< relref "whats-new-in-v8-5" >}})
|
||||
- [What's new in 8.4]({{< relref "whats-new-in-v8-4" >}})
|
||||
- [What's new in 8.3]({{< relref "whats-new-in-v8-3" >}})
|
||||
- [What's new in 8.2]({{< relref "whats-new-in-v8-2" >}})
|
||||
|
||||
126
docs/sources/whatsnew/whats-new-in-v8-5.md
Normal file
126
docs/sources/whatsnew/whats-new-in-v8-5.md
Normal file
@@ -0,0 +1,126 @@
|
||||
+++
|
||||
title = "What's new in Grafana v8.5"
|
||||
description = "Feature and improvement highlights for Grafana v8.5"
|
||||
keywords = ["grafana", "new", "documentation", "8.5", "release notes"]
|
||||
weight = -33
|
||||
aliases = ["/docs/grafana/latest/guides/whats-new-in-v8-5/"]
|
||||
[_build]
|
||||
list = false
|
||||
+++
|
||||
|
||||
# What’s new in Grafana v8.5
|
||||
|
||||
We’re excited to announce Grafana v8.5, with a variety of improvements that focus on Grafana’s usability, performance, and security.
|
||||
|
||||
We’ve summarized what’s new in the release here, but you might also be interested in the announcement blog post as well. If you’d like all the details you can check out the complete [changelog](https://github.com/grafana/grafana/blob/main/CHANGELOG.md).
|
||||
|
||||
# OSS
|
||||
|
||||
## Alerting - group names for Grafana-managed alert rules
|
||||
|
||||
It’s been tricky to work with more than a small number of Grafana-managed alert rules without groups in namespaces. They’ve also been inconsistent with the [Prometheus Alert Generator Compliance Specification](https://github.com/prometheus/compliance/blob/main/alert_generator/specification.md), which made working with Grafana-managed and Prometheus-managed alerts a confusing experience. With this release, you can still see flattened Grafana-managed alerts in the “List” tab, or use the new “Grouped” tab to work with groups:
|
||||
|
||||
Choose useful group names, and move alert rules between groups.
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/unified/rule-grouping-8-5.png" max-width="38000px" caption="Rule group" >}}
|
||||
|
||||
Rules in a group are evaluated together, so you can also set the interval for the entire group.
|
||||
|
||||
{{< figure src="/static/img/docs/alerting/unified/rule-grouping-details-8-5.png" max-width="38000px" caption="Rule group details" >}}
|
||||
|
||||
## Analytics
|
||||
|
||||
You can now enable Grafana version update checking and Grafana plugins version update checking separately using [a new configuration option to separately enable plugin version update checks](https://grafana.com/docs/grafana/latest/administration/configuration/#check_for_plugin_updates). The [prior version update checking configuration](https://grafana.com/docs/grafana/latest/administration/configuration/#check_for_updates) now only controls Grafana version update checks.
|
||||
|
||||
When enabled, a check runs every 10 minutes. It will notify, via the UI, when a new version is available. The checks will not prompt any auto-updates of the Grafana software, nor will it send any sensitive information.
|
||||
|
||||
Plugin update available:
|
||||
|
||||
{{< figure src="/static/img/docs/analytics/github-plugin-update-available-8-5.png" max-width="1200px" caption="Plugin-available" >}}
|
||||
|
||||
Grafana update available:
|
||||
|
||||
{{< figure src="/static/img/docs/analytics/github-plugin-version-8-5.png" max-width="1200px" caption="Grafana-update-available" >}}
|
||||
|
||||
## Dashboard Panels
|
||||
|
||||
In addition to RSS feeds, the News panel now supports Atom feeds, allowing you to display a wider range of data and information in Grafana.
|
||||
|
||||
### Scrolling in the Bar gauge panel
|
||||
|
||||
The Bar gauge panel now supports scrolling to support displaying large datasets while maintaining the readability of labels. You can set a min width or height for the bars (depending on the chart’s orientation), allowing the content to overflow in the container and become scrollable.
|
||||
|
||||
{{< figure src="/static/img/docs/bar-gauge-panel/vertical-view-8-5.png" max-width="1200px" caption="Vertical view" >}}
|
||||
|
||||
{{< figure src="/static/img/docs/bar-gauge-panel/horizontal-view-8-5.png" max-width="1200px" caption="Horizontal view" >}}
|
||||
|
||||
## Transformations
|
||||
|
||||
### Template variable substitution
|
||||
|
||||
We’ve added support to substitute template variables to transformations. This allows dashboards to be more interactive with transformations when a user inputs calculations and $**interval and $**interval_ms.
|
||||
|
||||
### Grouping to matrix
|
||||
|
||||
A new transformation is available that helps you structure data in a matrix format, using the Grafana table plugin.
|
||||
|
||||
## Expanding the navigation bar (Beta)
|
||||
|
||||
Available by switching on the ‘newNavigation’ feature toggle.
|
||||
You can expand the navigation bar for a better overview of Grafana’s features and installed integrations.
|
||||
This feature is currently in a beta version and we would appreciate your feedback. Sign up for a call with the Grafana team - it only takes 30 minutes, and you'll receive a $40 USD gift card as a token of appreciation for your time.
|
||||
US, UK, Canada, Australia, France, Germany, or South Africa: sign up here
|
||||
Everywhere else in the world: sign up here
|
||||
|
||||
{{< figure src="/static/img/docs/navigation/new-navigation-8-5.png" max-width="1200px" caption="New nav panel" >}}
|
||||
|
||||
## Notifications list for error alerts (Beta)
|
||||
|
||||
Available by switching on the ‘persistNotifications’ feature toggle.
|
||||
In order to support debugging issues in Grafana, error alerts that appear when viewing a dashboard now include a trace ID, and these alerts can be accessed under Profile / Notifications.
|
||||
|
||||
{{< figure src="/static/img/docs/navigation/nav-profile-notification-8-5.png" max-width="1200px" caption="Alert error list" >}}
|
||||
|
||||
## Service accounts (beta)
|
||||
|
||||
Service accounts are a major evolution for machine access within Grafana. You can create multiple API tokens per service account with independent expiration dates, and temporarily disable a service account without deleting it. These benefits make Service Accounts a more flexible way for Terraform and other apps to authenticate with Grafana. Service accounts also work with [fine-grained access control](https://grafana.com/docs/grafana/latest/enterprise/access-control/) in [Grafana Enterprise](https://grafana.com/docs/grafana/latest/enterprise/): you can improve security by granting service accounts specific roles to limit the functions they can perform. Service accounts are available in beta; you can try them out by enabling the `service-accounts` [feature toggle](https://grafana.com/docs/grafana/latest/administration/service-accounts/enable-service-accounts) or, if you use Grafana Cloud, [reaching out to our support team](https://grafana.com/orgs/raintank/tickets#) for early access. Learn more about Service Accounts in our [docs](https://grafana.com/docs/grafana/latest/administration/service-accounts).
|
||||
|
||||
{{< figure src="/static/img/docs/service-accounts/configure-8-5.png" max-width="1200px" caption="Configure service accounts" >}}
|
||||
|
||||
## Observability
|
||||
|
||||
### Trace to Logs for Splunk
|
||||
|
||||
With Trace to Logs, you can view relevant logs for a trace or span with one click. You can now link to Splunk logs from your tracing datasource. In your tracing datasource, configure Trace to Logs by selecting the Splunk datasource and relevant query options like tags to include in the query.
|
||||
|
||||
## Experimental Explore to Dashboard workflow
|
||||
|
||||
Allows users to create panels directly from within explore.
|
||||
|
||||
All queries in Explore get copied over to the new panel, the panel type is automatically selected based on queries’ response
|
||||
With multiple queries, it will view the response from the first, non hidden query to determine the visualization type
|
||||
|
||||
This feature is behind the `explore2Dashboard` feature toggle and is enabled by default.
|
||||
|
||||
# Grafana Enterprise
|
||||
|
||||
## Security
|
||||
|
||||
### Fine-Grained Access Control comes to Alerting (beta)
|
||||
|
||||
Check the Grafana Enterprise / Security section below for more details, including how to enable this beta feature; we’ve implemented
|
||||
[fine-grained access control](https://grafana.com/docs/grafana/latest/enterprise/access-control/) for alerting rules, notification policies, and contact points in [Grafana Enterprise](https://grafana.com/docs/grafana/latest/enterprise/). You can turn on fine-grained access control using the `accesscontrol` [feature toggle](https://grafana.com/docs/grafana/latest/enterprise/access-control/#enable-fine-grained-access-control), or by [reaching out to our support team](https://grafana.com/orgs/raintank/tickets#) for early access if you use Grafana Cloud. For more information on fine-grained access control, visit our [docs](https://grafana.com/docs/grafana/latest/enterprise/access-control/).
|
||||
|
||||
### Control access to dashboards, folders, and annotations (beta)
|
||||
|
||||
You can now use fine-grained access control to manage which specific users, teams, and roles can create, read, update, or delete dashboards, folders, or annotations. These are the latest services to incorporate fine-grained access control, which helps you dial in the specific access your users should have in Grafana. Fine-grained access control is currently in beta, but general availability is just around the corner, planned for our 9.0 release. You can turn on fine-grained access control using the `accesscontrol` [feature toggle](https://grafana.com/docs/grafana/latest/enterprise/access-control/#enable-fine-grained-access-control), or by [reaching out to our support team](https://grafana.com/orgs/raintank/tickets#) for early access if you use Grafana Cloud. For more information on fine-grained access control, visit our [docs](https://grafana.com/docs/grafana/latest/enterprise/access-control/).
|
||||
|
||||
### Configure Azure Key Vault using Managed Identities
|
||||
|
||||
You can already keep secrets in Grafana’s database (like data source credentials) safer by retrieving your database encryption key from a Key Management Service, like AWS KMS or Azure Key Vault. In Grafana v8.5, you can use an Azure Managed Identity to integrate with Azure Key Vault. This simplifies the Key Vault integration and keeps it consistent with Grafana data sources, like Azure Data Explorer.
|
||||
|
||||
## Configure reports more easily
|
||||
|
||||
Reports are a great way to share Grafana dashboards by email with users who don’t regularly sign in to Grafana. In 8.5, we’ve revamped the Report authoring UI to make it quicker and easier for you to create reports. View report details at a glance in list view, consider one configuration step at a time, and save reports for later. Also, Grafana will now emit a log every time a report is sent, so you can confirm its status or learn about send errors. Learn more about Reporting in our [docs](https://grafana.com/docs/grafana/latest/enterprise/reporting/).
|
||||
|
||||
{{< figure src="/static/img/docs/enterprise/report-new-report-8-5.png" max-width="1200px" caption="New report" >}}
|
||||
@@ -34,7 +34,7 @@ e2e.scenario({
|
||||
expect(links).to.have.length.greaterThan(13);
|
||||
|
||||
for (let index = 0; index < links.length; index++) {
|
||||
expect(Cypress.$(links[index]).attr('href')).contains(`var-custom=${encodeURI(variableValue)}`);
|
||||
expect(Cypress.$(links[index]).attr('href')).contains(`var-custom=${variableValue}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -44,14 +44,6 @@ e2e.scenario({
|
||||
// verify all links, should have All value
|
||||
verifyLinks('All');
|
||||
|
||||
// Data links should percent encode var values
|
||||
e2e()
|
||||
.get('[aria-label="Data link"]')
|
||||
.should('exist')
|
||||
.and((link) => {
|
||||
expect(link.attr('href')).contains(encodeURI('test%25value'));
|
||||
});
|
||||
|
||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('be.visible').click();
|
||||
|
||||
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('p2').should('be.visible').click();
|
||||
|
||||
8
go.mod
8
go.mod
@@ -53,7 +53,7 @@ require (
|
||||
github.com/grafana/cuetsy v0.0.0-20211119211437-8c25464cc9bf
|
||||
github.com/grafana/grafana-aws-sdk v0.10.1
|
||||
github.com/grafana/grafana-azure-sdk-go v1.1.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.129.0
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.131.0
|
||||
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/hashicorp/go-hclog v0.16.1
|
||||
@@ -108,8 +108,8 @@ require (
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.5
|
||||
gonum.org/v1/gonum v0.9.3
|
||||
golang.org/x/tools v0.1.9
|
||||
gonum.org/v1/gonum v0.11.0
|
||||
google.golang.org/api v0.60.0
|
||||
google.golang.org/grpc v1.42.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
@@ -272,7 +272,7 @@ require (
|
||||
github.com/elazarl/goproxy v0.0.0-20220115173737-adb46da277ac // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
|
||||
github.com/getkin/kin-openapi v0.91.0 // indirect
|
||||
github.com/getkin/kin-openapi v0.94.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
|
||||
30
go.sum
30
go.sum
@@ -76,6 +76,7 @@ cuelang.org/go v0.4.0/go.mod h1:tz/edkPi+T37AZcb5GlPY+WJkL6KiDlDVupKwL3vvjs=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8=
|
||||
git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.0.0/go.mod h1:SY08giD/XbhTz07tJdpw1SoxQXHPN30+DI3Z04SYqyg=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.2.1/go.mod h1:O6X1iYHP7s2x7NjUKsXVhkwWrQhxrd+d8/3rRadj4CI=
|
||||
@@ -286,7 +287,10 @@ github.com/aerospike/aerospike-client-go v1.27.0/go.mod h1:zj8LBEnWBDOVEIJt8LvaR
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
|
||||
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
@@ -440,6 +444,7 @@ github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+Wji
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/bonitoo-io/go-sql-bigquery v0.3.4-1.4.0/go.mod h1:J4Y6YJm0qTWB9aFziB7cPeSyc6dOZFyJdteSeybVpXQ=
|
||||
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
@@ -865,6 +870,8 @@ github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW
|
||||
github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4=
|
||||
github.com/getkin/kin-openapi v0.91.0 h1:mOSAljTAQONM0YVtI3+LvIQaa0zPwa3SH6UuiyEnbYQ=
|
||||
github.com/getkin/kin-openapi v0.91.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=
|
||||
github.com/getkin/kin-openapi v0.94.0 h1:bAxg2vxgnHHHoeefVdmGbR+oxtJlcv5HsJJa3qmAHuo=
|
||||
github.com/getkin/kin-openapi v0.94.0/go.mod h1:LWZfzOd7PRy8GJ1dJ6mCU6tNdSfOwRac1BUPam4aw6Q=
|
||||
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=
|
||||
@@ -892,6 +899,7 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm
|
||||
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
||||
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
||||
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||
github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||
github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -905,6 +913,7 @@ github.com/go-kit/kit v0.11.0/go.mod h1:73/6Ixaufkvb5Osvkls8C79vuQ49Ba1rUEUYNSf+
|
||||
github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
@@ -1044,6 +1053,8 @@ github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9G
|
||||
github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0=
|
||||
github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts=
|
||||
github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0=
|
||||
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
@@ -1359,6 +1370,8 @@ github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOax
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.125.0/go.mod h1:9YiJ5GUxIsIEUC0qR9+BJVP5M7mCSP6uc6Ne62YKkgc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.129.0 h1:apmA8x59QvW5Wov+FhAfM0IiNGjMi8V2Ou7xyMk1leE=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.129.0/go.mod h1:4edtosZepfQF9jkQwRywJsNSJzXTHmzbmcVcAl8MEQc=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.131.0 h1:8M+Qfch4WNi3PPpRhWtmcLFTCq8zlIjnxrc8iRigAY0=
|
||||
github.com/grafana/grafana-plugin-sdk-go v0.131.0/go.mod h1:jmrxelOJKrIK0yrsIzcotS8pbqPZozbmJgGy7k3hK1k=
|
||||
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa h1:+pXjAxavVR2FKKNsuuCXGCWEj8XGc1Af6SPiyBpzU2A=
|
||||
github.com/grafana/loki v1.6.2-0.20211015002020-7832783b1caa/go.mod h1:0O8o/juxNSKN/e+DzWDTRkl7Zm8CkZcz0NDqEdojlrk=
|
||||
github.com/grafana/saml v0.0.0-20211007135653-aed1b2edd86b h1:YiSGp34F4V0G08HHx1cJBf2GVgwYAkXQjzuVs1t8jYk=
|
||||
@@ -2068,6 +2081,7 @@ github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG
|
||||
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
|
||||
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
@@ -2264,6 +2278,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
@@ -2546,6 +2561,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/gopher-lua v0.0.0-20180630135845-46796da1b0b4/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU=
|
||||
github.com/yuin/gopher-lua v0.0.0-20200816102855-ee81675732da/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||
@@ -2754,6 +2770,10 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o
|
||||
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -2782,6 +2802,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -3206,8 +3227,9 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8=
|
||||
golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -3220,13 +3242,14 @@ gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJ
|
||||
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
|
||||
gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/gonum v0.9.3 h1:DnoIG+QAMaF5NvxnGe/oKsgKcAc6PcUyl8q0VetfQ8s=
|
||||
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
|
||||
gonum.org/v1/gonum v0.11.0 h1:f1IJhK4Km5tBJmaiJXtk/PkL4cdVX6J+tGiM187uT5E=
|
||||
gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA=
|
||||
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
|
||||
gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo=
|
||||
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.3.2/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
@@ -3520,6 +3543,7 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
honnef.co/go/tools v0.2.0/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
inet.af/netaddr v0.0.0-20210707202901-70468d781e6c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "8.4.5",
|
||||
"testing": "8.4.5"
|
||||
"stable": "8.4.6",
|
||||
"testing": "8.5.0-beta1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"packages": ["packages/*"],
|
||||
"version": "8.5.0-pre"
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "8.5.0"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"license": "AGPL-3.0-only",
|
||||
"private": true,
|
||||
"name": "grafana",
|
||||
"version": "8.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"repository": "github:grafana/grafana",
|
||||
"scripts": {
|
||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||
@@ -142,7 +142,7 @@
|
||||
"@types/papaparse": "5.3.2",
|
||||
"@types/pluralize": "^0.0.29",
|
||||
"@types/prismjs": "1.26.0",
|
||||
"@types/rc-time-picker": "^3",
|
||||
"@types/rc-time-picker": "3.4.1",
|
||||
"@types/react": "17.0.42",
|
||||
"@types/react-beautiful-dnd": "13.1.2",
|
||||
"@types/react-dom": "17.0.14",
|
||||
@@ -257,7 +257,7 @@
|
||||
"@grafana/slate-react": "0.22.10-grafana",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"@jaegertracing/jaeger-ui-components": "workspace:*",
|
||||
"@kusto/monaco-kusto": "4.1.6",
|
||||
"@kusto/monaco-kusto": "5.1.1",
|
||||
"@lezer/common": "0.15.12",
|
||||
"@lezer/lr": "0.15.8",
|
||||
"@lingui/core": "3.13.2",
|
||||
@@ -325,7 +325,7 @@
|
||||
"lru-cache": "7.7.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"minisearch": "5.0.0-beta1",
|
||||
"moment": "2.29.1",
|
||||
"moment": "2.29.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"monaco-editor": "^0.31.1",
|
||||
"monaco-promql": "^1.7.2",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/data",
|
||||
"version": "8.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"description": "Grafana Data Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
@@ -22,14 +22,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@grafana/schema": "8.5.0-pre",
|
||||
"@grafana/schema": "8.5.0",
|
||||
"@types/d3-interpolate": "^1.4.0",
|
||||
"d3-interpolate": "1.4.0",
|
||||
"date-fns": "2.28.0",
|
||||
"eventemitter3": "4.0.7",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "4.0.12",
|
||||
"moment": "2.29.1",
|
||||
"moment": "2.29.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"ol": "6.14.1",
|
||||
"papaparse": "5.3.2",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
ApplyFieldOverrideOptions,
|
||||
DataFrame,
|
||||
DataLink,
|
||||
DisplayProcessor,
|
||||
DisplayValue,
|
||||
DynamicConfigValue,
|
||||
@@ -352,7 +353,7 @@ export const getLinksSupplier =
|
||||
const timeRangeUrl = locationUtil.getTimeRangeUrlParams();
|
||||
const { timeField } = getTimeField(frame);
|
||||
|
||||
return field.config.links.map((link) => {
|
||||
return field.config.links.map((link: DataLink) => {
|
||||
const variablesQuery = locationUtil.getVariablesUrlParams();
|
||||
let dataFrameVars = {};
|
||||
let valueVars = {};
|
||||
@@ -438,7 +439,7 @@ export const getLinksSupplier =
|
||||
}
|
||||
|
||||
let href = locationUtil.assureBaseUrl(link.url.replace(/\n/g, ''));
|
||||
href = replaceVariables(href, variables, encodeURIComponent);
|
||||
href = replaceVariables(href, variables);
|
||||
href = locationUtil.processUrl(href);
|
||||
|
||||
const info: LinkModel<Field> = {
|
||||
|
||||
@@ -188,7 +188,7 @@ describe('align frames', () => {
|
||||
],
|
||||
});
|
||||
|
||||
const out = outerJoinDataFrames({ frames: [series1], enforceSort: true, keepOriginIndices: true })!;
|
||||
const out = outerJoinDataFrames({ frames: [series1], keepOriginIndices: true })!;
|
||||
expect(
|
||||
out.fields.map((f) => ({
|
||||
name: f.name,
|
||||
|
||||
@@ -48,13 +48,6 @@ export interface JoinOptions {
|
||||
*/
|
||||
keep?: FieldMatcher;
|
||||
|
||||
/**
|
||||
* When the result is a single frame, this will to a quick check to see if the values are sorted,
|
||||
* and sort if necessary. If the first/last values are in order the whole vector is assumed to be
|
||||
* sorted
|
||||
*/
|
||||
enforceSort?: boolean;
|
||||
|
||||
/**
|
||||
* @internal -- used when we need to keep a reference to the original frame/field index
|
||||
*/
|
||||
@@ -65,6 +58,21 @@ function getJoinMatcher(options: JoinOptions): FieldMatcher {
|
||||
return options.joinBy ?? pickBestJoinField(options.frames);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function maybeSortFrame(frame: DataFrame, fieldIdx: number) {
|
||||
if (fieldIdx >= 0) {
|
||||
let sortByField = frame.fields[fieldIdx];
|
||||
|
||||
if (sortByField.type !== FieldType.string && !isLikelyAscendingVector(sortByField.values)) {
|
||||
frame = sortDataFrame(frame, fieldIdx);
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will return a single frame joined by the first matching field. When a join field is not specified,
|
||||
* the default will use the first time field
|
||||
@@ -109,12 +117,8 @@ export function outerJoinDataFrames(options: JoinOptions): DataFrame | undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (options.enforceSort) {
|
||||
if (joinIndex >= 0) {
|
||||
if (!isLikelyAscendingVector(frameCopy.fields[joinIndex].values)) {
|
||||
frameCopy = sortDataFrame(frameCopy, joinIndex);
|
||||
}
|
||||
}
|
||||
if (joinIndex >= 0) {
|
||||
frameCopy = maybeSortFrame(frameCopy, joinIndex);
|
||||
}
|
||||
|
||||
if (options.keep) {
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface LicenseInfo {
|
||||
stateInfo: string;
|
||||
edition: GrafanaEdition;
|
||||
enabledFeatures: { [key: string]: boolean };
|
||||
trialExpiry?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,4 +181,5 @@ export interface GrafanaConfig {
|
||||
geomapDisableCustomBaseLayer?: boolean;
|
||||
unifiedAlertingEnabled: boolean;
|
||||
angularSupportEnabled: boolean;
|
||||
feedbackLinksEnabled: boolean;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface FeatureToggles {
|
||||
trimDefaults?: boolean;
|
||||
envelopeEncryption?: boolean;
|
||||
httpclientprovider_azure_auth?: boolean;
|
||||
['service-accounts']?: boolean;
|
||||
serviceAccounts?: boolean;
|
||||
database_metrics?: boolean;
|
||||
dashboardPreviews?: boolean;
|
||||
dashboardPreviewsScheduler?: boolean;
|
||||
@@ -52,4 +52,6 @@ export interface FeatureToggles {
|
||||
alertProvisioning?: boolean;
|
||||
storageLocalUpload?: boolean;
|
||||
azureMonitorResourcePickerForMetrics?: boolean;
|
||||
explore2Dashboard?: boolean;
|
||||
persistNotifications?: boolean;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { eventFactory } from '../events/eventFactory';
|
||||
import { BusEventBase, BusEventWithPayload } from '../events/types';
|
||||
import { DataHoverPayload } from '../events';
|
||||
|
||||
export type AlertPayload = [string, string?];
|
||||
export type AlertErrorPayload = [string, (string | Error)?];
|
||||
export type AlertPayload = [string, string?, string?];
|
||||
export type AlertErrorPayload = [string, (string | Error)?, string?];
|
||||
|
||||
export const AppEvents = {
|
||||
alertSuccess: eventFactory<AlertPayload>('alert-success'),
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface NavModelItem extends NavLinkDTO {
|
||||
highlightId?: string;
|
||||
tabSuffix?: ComponentType<{ className?: string }>;
|
||||
hideFromNavbar?: boolean;
|
||||
showIconInNavbar?: boolean;
|
||||
}
|
||||
|
||||
export enum NavSection {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { locationUtil } from './location';
|
||||
describe('locationUtil', () => {
|
||||
const { location } = window;
|
||||
|
||||
beforeAll(() => {
|
||||
beforeEach(() => {
|
||||
// @ts-ignore
|
||||
delete window.location;
|
||||
|
||||
@@ -21,63 +21,97 @@ describe('locationUtil', () => {
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
afterEach(() => {
|
||||
window.location = location;
|
||||
});
|
||||
|
||||
describe('strip base when appSubUrl configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: { appSubUrl: '/subUrl' } as any,
|
||||
getVariablesUrlParams: (() => {}) as any,
|
||||
getTimeRangeForUrl: (() => {}) as any,
|
||||
describe('stripBaseFromUrl', () => {
|
||||
describe('when appSubUrl configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: { appSubUrl: '/subUrl' } as any,
|
||||
getVariablesUrlParams: (() => {}) as any,
|
||||
getTimeRangeForUrl: (() => {}) as any,
|
||||
});
|
||||
});
|
||||
});
|
||||
test('relative url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
|
||||
});
|
||||
test('relative url with multiple subUrl in path', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
|
||||
});
|
||||
test('relative url with subdirectory subUrl', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
|
||||
});
|
||||
test('absolute url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
|
||||
});
|
||||
test('absolute url with multiple subUrl in path', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl(
|
||||
'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/'
|
||||
);
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
|
||||
});
|
||||
test('absolute url with subdirectory subUrl', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/');
|
||||
expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('strip base when appSubUrl not configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: {} as any,
|
||||
getVariablesUrlParams: (() => {}) as any,
|
||||
getTimeRangeForUrl: (() => {}) as any,
|
||||
test('relative url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
|
||||
});
|
||||
test('relative url with multiple subUrl in path', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/thisShouldRemain/subUrl/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
|
||||
});
|
||||
test('relative url with subdirectory subUrl', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/thisShouldRemain/subUrl/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
|
||||
});
|
||||
test('absolute url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/thisShouldRemain/');
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/');
|
||||
});
|
||||
test('absolute url with multiple subUrl in path', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl(
|
||||
'http://www.domain.com:9877/subUrl/thisShouldRemain/subUrl/'
|
||||
);
|
||||
expect(urlWithoutMaster).toBe('/thisShouldRemain/subUrl/');
|
||||
});
|
||||
test('absolute url with subdirectory subUrl', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/thisShouldRemain/subUrl/');
|
||||
expect(urlWithoutMaster).toBe('http://www.domain.com:9877/thisShouldRemain/subUrl/');
|
||||
});
|
||||
});
|
||||
|
||||
test('relative url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
|
||||
describe('when appSubUrl not configured', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
config: {} as any,
|
||||
getVariablesUrlParams: (() => {}) as any,
|
||||
getTimeRangeForUrl: (() => {}) as any,
|
||||
});
|
||||
});
|
||||
|
||||
test('relative url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
|
||||
});
|
||||
|
||||
test('absolute url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
|
||||
});
|
||||
});
|
||||
|
||||
test('absolute url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
|
||||
describe('when origin does not have a port in it', () => {
|
||||
beforeEach(() => {
|
||||
window.location = {
|
||||
...location,
|
||||
hash: '#hash',
|
||||
host: 'www.domain.com',
|
||||
hostname: 'www.domain.com',
|
||||
href: 'http://www.domain.com/path/b?search=a&b=c&d#hash',
|
||||
origin: 'http://www.domain.com',
|
||||
pathname: '/path/b',
|
||||
port: '',
|
||||
protocol: 'http:',
|
||||
search: '?search=a&b=c&d',
|
||||
};
|
||||
});
|
||||
|
||||
test('relative url', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('/subUrl/grafana/');
|
||||
});
|
||||
|
||||
test('URL with same host, different port', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.domain.com:9877/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('http://www.domain.com:9877/subUrl/grafana/');
|
||||
});
|
||||
|
||||
test('URL of a completely different origin', () => {
|
||||
const urlWithoutMaster = locationUtil.stripBaseFromUrl('http://www.another-domain.com/subUrl/grafana/');
|
||||
expect(urlWithoutMaster).toBe('http://www.another-domain.com/subUrl/grafana/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,22 +7,43 @@ let grafanaConfig: GrafanaConfig = { appSubUrl: '' } as any;
|
||||
let getTimeRangeUrlParams: () => RawTimeRange;
|
||||
let getVariablesUrlParams: (scopedVars?: ScopedVars) => UrlQueryMap;
|
||||
|
||||
const maybeParseUrl = (input: string): URL | undefined => {
|
||||
try {
|
||||
return new URL(input);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
* @internal
|
||||
*/
|
||||
const stripBaseFromUrl = (url: string): string => {
|
||||
const stripBaseFromUrl = (urlOrPath: string): string => {
|
||||
// Will only return a URL object if the input is actually a valid URL
|
||||
const parsedUrl = maybeParseUrl(urlOrPath);
|
||||
if (parsedUrl) {
|
||||
// If the input is a URL, and for a different origin that we're on, just bail
|
||||
// and return it. There's no need to strip anything from it
|
||||
if (parsedUrl.origin !== window.location.origin) {
|
||||
return urlOrPath;
|
||||
}
|
||||
}
|
||||
|
||||
const appSubUrl = grafanaConfig.appSubUrl ?? '';
|
||||
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
||||
const isAbsoluteUrl = url.startsWith('http');
|
||||
const isAbsoluteUrl = urlOrPath.startsWith('http');
|
||||
|
||||
let segmentToStrip = appSubUrl;
|
||||
|
||||
if (!url.startsWith('/') || isAbsoluteUrl) {
|
||||
if (!urlOrPath.startsWith('/') || isAbsoluteUrl) {
|
||||
segmentToStrip = `${window.location.origin}${appSubUrl}`;
|
||||
}
|
||||
|
||||
return url.length > 0 && url.indexOf(segmentToStrip) === 0 ? url.slice(segmentToStrip.length - stripExtraChars) : url;
|
||||
return urlOrPath.length > 0 && urlOrPath.indexOf(segmentToStrip) === 0
|
||||
? urlOrPath.slice(segmentToStrip.length - stripExtraChars)
|
||||
: urlOrPath;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/e2e-selectors",
|
||||
"version": "8.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"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.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"description": "Grafana End-to-End Test Library",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -48,7 +48,7 @@
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@cypress/webpack-preprocessor": "5.11.1",
|
||||
"@grafana/e2e-selectors": "8.5.0-pre",
|
||||
"@grafana/e2e-selectors": "8.5.0",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@mochajs/json-file-reporter": "^1.2.0",
|
||||
"babel-loader": "8.2.4",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/runtime",
|
||||
"version": "8.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"description": "Grafana Runtime Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -22,9 +22,9 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grafana/data": "8.5.0-pre",
|
||||
"@grafana/e2e-selectors": "8.5.0-pre",
|
||||
"@grafana/ui": "8.5.0-pre",
|
||||
"@grafana/data": "8.5.0",
|
||||
"@grafana/e2e-selectors": "8.5.0",
|
||||
"@grafana/ui": "8.5.0",
|
||||
"@sentry/browser": "6.19.1",
|
||||
"history": "4.10.1",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -36,6 +36,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
externalUserMngLinkName = '';
|
||||
externalUserMngInfo = '';
|
||||
allowOrgCreate = false;
|
||||
feedbackLinksEnabled = true;
|
||||
disableLoginForm = false;
|
||||
defaultDatasource = ''; // UID
|
||||
alertingEnabled = false;
|
||||
@@ -119,7 +120,6 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
this.theme2 = createTheme({ colors: { mode } });
|
||||
this.theme = this.theme2.v1;
|
||||
this.bootData = options.bootData;
|
||||
this.buildInfo = options.buildInfo;
|
||||
|
||||
const defaults = {
|
||||
datasources: {},
|
||||
@@ -131,7 +131,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
appUrl: '',
|
||||
appSubUrl: '',
|
||||
buildInfo: {
|
||||
version: 'v1.0',
|
||||
version: '1.0',
|
||||
commit: '1',
|
||||
env: 'production',
|
||||
},
|
||||
@@ -142,6 +142,8 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
||||
|
||||
merge(this, defaults, options);
|
||||
|
||||
this.buildInfo = options.buildInfo || defaults.buildInfo;
|
||||
|
||||
if (this.dateFormats) {
|
||||
systemDateFormats.update(this.dateFormats);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/schema",
|
||||
"version": "8.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"description": "Grafana Schema Library",
|
||||
"keywords": [
|
||||
"typescript"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"author": "Grafana Labs",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@grafana/toolkit",
|
||||
"version": "8.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"description": "Grafana Toolkit",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -28,10 +28,10 @@
|
||||
"dependencies": {
|
||||
"@babel/core": "7.13.14",
|
||||
"@babel/preset-env": "7.13.12",
|
||||
"@grafana/data": "8.5.0-pre",
|
||||
"@grafana/data": "8.5.0",
|
||||
"@grafana/eslint-config": "2.5.2",
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
"@grafana/ui": "8.5.0-pre",
|
||||
"@grafana/ui": "8.5.0",
|
||||
"@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.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"description": "Grafana Components Library",
|
||||
"keywords": [
|
||||
"grafana",
|
||||
@@ -33,9 +33,9 @@
|
||||
"@emotion/css": "11.7.1",
|
||||
"@emotion/react": "11.8.2",
|
||||
"@grafana/aws-sdk": "0.0.35",
|
||||
"@grafana/data": "8.5.0-pre",
|
||||
"@grafana/e2e-selectors": "8.5.0-pre",
|
||||
"@grafana/schema": "8.5.0-pre",
|
||||
"@grafana/data": "8.5.0",
|
||||
"@grafana/e2e-selectors": "8.5.0",
|
||||
"@grafana/schema": "8.5.0",
|
||||
"@grafana/slate-react": "0.22.10-grafana",
|
||||
"@monaco-editor/react": "4.3.1",
|
||||
"@popperjs/core": "2.11.4",
|
||||
@@ -58,7 +58,7 @@
|
||||
"jquery": "3.6.0",
|
||||
"lodash": "4.17.21",
|
||||
"memoize-one": "6.0.0",
|
||||
"moment": "2.29.1",
|
||||
"moment": "2.29.2",
|
||||
"monaco-editor": "^0.31.1",
|
||||
"ol": "6.14.1",
|
||||
"prismjs": "1.27.0",
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
topSpacing?: number;
|
||||
}
|
||||
|
||||
function getIconFromSeverity(severity: AlertVariant): string {
|
||||
export function getIconFromSeverity(severity: AlertVariant): string {
|
||||
switch (severity) {
|
||||
case 'error':
|
||||
case 'warning':
|
||||
@@ -150,7 +150,7 @@ const getStyles = (
|
||||
color: ${theme.colors.text.secondary};
|
||||
padding-top: ${theme.spacing(1)};
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
`,
|
||||
buttonWrapper: css`
|
||||
padding: ${theme.spacing(1)};
|
||||
|
||||
@@ -232,12 +232,17 @@ const Meta = memo(({ children, className, separator = '|' }: ChildProps & { sepa
|
||||
const styles = useStyles2(getMetaStyles);
|
||||
let meta = children;
|
||||
|
||||
const filtered = React.Children.toArray(children).filter(Boolean);
|
||||
if (!filtered.length) {
|
||||
return null;
|
||||
}
|
||||
meta = filtered.map((element, i) => (
|
||||
<div key={`element_${i}`} className={styles.metadataItem}>
|
||||
{element}
|
||||
</div>
|
||||
));
|
||||
// Join meta data elements by separator
|
||||
if (Array.isArray(children) && separator) {
|
||||
const filtered = React.Children.toArray(children).filter(Boolean);
|
||||
if (!filtered.length) {
|
||||
return null;
|
||||
}
|
||||
if (filtered.length > 1 && separator) {
|
||||
meta = filtered.reduce((prev, curr, i) => [
|
||||
prev,
|
||||
<span key={`separator_${i}`} className={styles.separator}>
|
||||
@@ -261,6 +266,9 @@ const getMetaStyles = (theme: GrafanaTheme2) => ({
|
||||
margin: theme.spacing(0.5, 0, 0),
|
||||
lineHeight: theme.typography.bodySmall.lineHeight,
|
||||
overflowWrap: 'anywhere',
|
||||
}),
|
||||
metadataItem: css({
|
||||
// Needed to allow for clickable children in metadata
|
||||
zIndex: 0,
|
||||
}),
|
||||
separator: css({
|
||||
|
||||
@@ -60,7 +60,7 @@ export const CollapsableSection: FC<Props> = ({
|
||||
{loading ? (
|
||||
<Spinner className={styles.spinner} />
|
||||
) : (
|
||||
<Icon name={open ? 'angle-down' : 'angle-right'} className={styles.icon} />
|
||||
<Icon name={open ? 'angle-up' : 'angle-down'} className={styles.icon} />
|
||||
)}
|
||||
</button>
|
||||
<div className={styles.label} id={`collapse-label-${id}`}>
|
||||
|
||||
@@ -82,7 +82,7 @@ function sameProps(prevProps: any, nextProps: any, propsToDiff: Array<string | P
|
||||
*/
|
||||
export interface GraphNGState {
|
||||
alignedFrame: DataFrame;
|
||||
alignedData: AlignedData;
|
||||
alignedData?: AlignedData;
|
||||
config?: UPlotConfigBuilder;
|
||||
}
|
||||
|
||||
@@ -98,7 +98,9 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
constructor(props: GraphNGProps) {
|
||||
super(props);
|
||||
this.state = this.prepState(props);
|
||||
let state = this.prepState(props);
|
||||
state.alignedData = state.config!.prepData!([state.alignedFrame]) as AlignedData;
|
||||
this.state = state;
|
||||
this.plotInstance = React.createRef();
|
||||
}
|
||||
|
||||
@@ -131,7 +133,6 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
state = {
|
||||
alignedFrame,
|
||||
alignedData: config!.prepData!([alignedFrame]) as AlignedData,
|
||||
config,
|
||||
};
|
||||
|
||||
@@ -229,12 +230,13 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
|
||||
if (shouldReconfig) {
|
||||
newState.config = this.props.prepConfig(newState.alignedFrame, this.props.frames, this.getTimeRange);
|
||||
newState.alignedData = newState.config.prepData!([newState.alignedFrame]) as AlignedData;
|
||||
pluginLog('GraphNG', false, 'config recreated', newState.config);
|
||||
}
|
||||
}
|
||||
|
||||
newState && this.setState(newState);
|
||||
newState.alignedData = newState.config!.prepData!([newState.alignedFrame]) as AlignedData;
|
||||
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +257,7 @@ export class GraphNG extends React.Component<GraphNGProps, GraphNGState> {
|
||||
{(vizWidth: number, vizHeight: number) => (
|
||||
<UPlotChart
|
||||
config={config}
|
||||
data={alignedData}
|
||||
data={alignedData!}
|
||||
width={vizWidth}
|
||||
height={vizHeight}
|
||||
timeRange={timeRange}
|
||||
|
||||
@@ -58,22 +58,6 @@ Object {
|
||||
"values": [Function],
|
||||
},
|
||||
],
|
||||
"bands": Array [
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
2,
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
4,
|
||||
3,
|
||||
],
|
||||
},
|
||||
],
|
||||
"cursor": Object {
|
||||
"dataIdx": [Function],
|
||||
"drag": Object {
|
||||
|
||||
@@ -13,7 +13,7 @@ export function MultiSelect<T>(props: MultiSelectCommonProps<T>) {
|
||||
return <SelectBase {...props} isMulti />;
|
||||
}
|
||||
|
||||
interface AsyncSelectProps<T> extends Omit<SelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
|
||||
export interface AsyncSelectProps<T> extends Omit<SelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
|
||||
// AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options
|
||||
value?: SelectableValue<T> | null;
|
||||
invalid?: boolean;
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { Themeable2 } from '../../types';
|
||||
import { preparePlotData } from '../uPlot/utils';
|
||||
import { preparePlotData2, getStackingGroups } from '../uPlot/utils';
|
||||
import { preparePlotFrame } from './utils';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@@ -50,7 +50,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
|
||||
const alignedDataFrame = preparePlotFrame(props.sparkline, props.config);
|
||||
|
||||
this.state = {
|
||||
data: preparePlotData([alignedDataFrame]),
|
||||
data: preparePlotData2(alignedDataFrame, getStackingGroups(alignedDataFrame)),
|
||||
alignedDataFrame,
|
||||
configBuilder: this.prepareConfig(alignedDataFrame),
|
||||
};
|
||||
@@ -64,7 +64,7 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
|
||||
|
||||
return {
|
||||
...state,
|
||||
data: preparePlotData([frame]),
|
||||
data: preparePlotData2(frame, getStackingGroups(frame)),
|
||||
alignedDataFrame: frame,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,9 +304,9 @@ export const Table: FC<Props> = memo((props: Props) => {
|
||||
totalColumnsWidth={totalColumnsWidth}
|
||||
/>
|
||||
)}
|
||||
{paginationEl}
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
{paginationEl}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -184,6 +184,9 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
div:not(:only-child):first-child {
|
||||
flex-grow: 0.6;
|
||||
}
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`,
|
||||
paginationSummary: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
@@ -191,11 +194,16 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
||||
margin-left: auto;
|
||||
`,
|
||||
|
||||
tableContentWrapper: (totalColumnsWidth: number) => css`
|
||||
width: ${totalColumnsWidth ?? '100%'};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
tableContentWrapper: (totalColumnsWidth: number) => {
|
||||
const width = totalColumnsWidth !== undefined ? `${totalColumnsWidth}px` : '100%';
|
||||
|
||||
return css`
|
||||
label: tableContentWrapper;
|
||||
width: ${width};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
},
|
||||
row: css`
|
||||
label: row;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
|
||||
@@ -19,7 +19,7 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
|
||||
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
const { eventBus, sync } = this.context as PanelContext;
|
||||
const { theme, timeZone, legend, renderers, tweakAxis, tweakScale } = this.props;
|
||||
const { theme, timeZone, renderers, tweakAxis, tweakScale } = this.props;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
@@ -29,7 +29,6 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
|
||||
eventBus,
|
||||
sync,
|
||||
allFrames,
|
||||
legend,
|
||||
renderers,
|
||||
tweakScale,
|
||||
tweakAxis,
|
||||
|
||||
@@ -23,10 +23,9 @@ import {
|
||||
VisibilityMode,
|
||||
ScaleDirection,
|
||||
ScaleOrientation,
|
||||
VizLegendOptions,
|
||||
StackingMode,
|
||||
} from '@grafana/schema';
|
||||
import { collectStackingGroups, INTERNAL_NEGATIVE_Y_PREFIX, orderIdsByCalcs, preparePlotData } from '../uPlot/utils';
|
||||
import { getStackingGroups, preparePlotData2 } from '../uPlot/utils';
|
||||
import uPlot from 'uplot';
|
||||
import { buildScaleKey } from '../GraphNG/utils';
|
||||
|
||||
@@ -40,7 +39,6 @@ const defaultConfig: GraphFieldConfig = {
|
||||
|
||||
export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
sync?: () => DashboardCursorSync;
|
||||
legend?: VizLegendOptions;
|
||||
}> = ({
|
||||
frame,
|
||||
theme,
|
||||
@@ -50,13 +48,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
sync,
|
||||
allFrames,
|
||||
renderers,
|
||||
legend,
|
||||
tweakScale = (opts) => opts,
|
||||
tweakAxis = (opts) => opts,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder(timeZone);
|
||||
|
||||
builder.setPrepData((prepData) => preparePlotData(prepData, undefined, legend));
|
||||
builder.setPrepData((frames) => preparePlotData2(frames[0], builder.getStackingGroups()));
|
||||
|
||||
// X is the first field in the aligned frame
|
||||
const xField = frame.fields[0];
|
||||
@@ -122,8 +119,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
let customRenderedFields =
|
||||
renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? [];
|
||||
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
|
||||
let indexByName: Map<string, number> | undefined;
|
||||
|
||||
for (let i = 1; i < frame.fields.length; i++) {
|
||||
@@ -328,20 +323,11 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
});
|
||||
}
|
||||
}
|
||||
collectStackingGroups(field, stackingGroups, seriesIndex);
|
||||
}
|
||||
|
||||
if (stackingGroups.size !== 0) {
|
||||
for (const [group, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
for (let j = seriesIdxs.length - 1; j > 0; j--) {
|
||||
builder.addBand({
|
||||
series: [seriesIdxs[j], seriesIdxs[j - 1]],
|
||||
dir: group.startsWith(INTERNAL_NEGATIVE_Y_PREFIX) ? 1 : -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let stackingGroups = getStackingGroups(frame);
|
||||
|
||||
builder.setStackingGroups(stackingGroups);
|
||||
|
||||
// hook up custom/composite renderers
|
||||
renderers?.forEach((r) => {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GraphFieldConfig, GraphDrawStyle } from '@grafana/schema';
|
||||
import uPlot from 'uplot';
|
||||
import createMockRaf from 'mock-raf';
|
||||
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
|
||||
import { preparePlotData } from './utils';
|
||||
import { preparePlotData2, getStackingGroups } from './utils';
|
||||
import { SeriesProps } from './config/UPlotSeriesBuilder';
|
||||
|
||||
const mockRaf = createMockRaf();
|
||||
@@ -55,7 +55,7 @@ const mockData = () => {
|
||||
|
||||
const config = new UPlotConfigBuilder();
|
||||
config.addSeries({} as SeriesProps);
|
||||
return { data: [data], timeRange, config };
|
||||
return { data: data, timeRange, config };
|
||||
};
|
||||
|
||||
describe('UPlotChart', () => {
|
||||
@@ -75,7 +75,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { unmount } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // mock
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // mock
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@@ -94,7 +94,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // mock
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // mock
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@@ -104,11 +104,11 @@ describe('UPlotChart', () => {
|
||||
|
||||
expect(uPlot).toBeCalledTimes(1);
|
||||
|
||||
data[0].fields[1].values.set(0, 1);
|
||||
data.fields[1].values.set(0, 1);
|
||||
|
||||
rerender(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // changed
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // changed
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@@ -124,7 +124,13 @@ describe('UPlotChart', () => {
|
||||
it('skips uPlot intialization for width and height equal 0', async () => {
|
||||
const { data, timeRange, config } = mockData();
|
||||
const { queryAllByTestId } = render(
|
||||
<UPlotChart data={preparePlotData(data)} config={config} timeRange={timeRange} width={0} height={0} />
|
||||
<UPlotChart
|
||||
data={preparePlotData2(data, getStackingGroups(data))}
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={0}
|
||||
height={0}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(queryAllByTestId('uplot-main-div')).toHaveLength(1);
|
||||
@@ -136,7 +142,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // frame
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@@ -150,7 +156,13 @@ describe('UPlotChart', () => {
|
||||
nextConfig.addSeries({} as SeriesProps);
|
||||
|
||||
rerender(
|
||||
<UPlotChart data={preparePlotData(data)} config={nextConfig} timeRange={timeRange} width={100} height={100} />
|
||||
<UPlotChart
|
||||
data={preparePlotData2(data, getStackingGroups(data))}
|
||||
config={nextConfig}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(destroyMock).toBeCalledTimes(1);
|
||||
@@ -162,7 +174,7 @@ describe('UPlotChart', () => {
|
||||
|
||||
const { rerender } = render(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // frame
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={100}
|
||||
@@ -173,7 +185,7 @@ describe('UPlotChart', () => {
|
||||
// we wait 1 frame for plugins initialisation logic to finish
|
||||
rerender(
|
||||
<UPlotChart
|
||||
data={preparePlotData(data)} // frame
|
||||
data={preparePlotData2(data, getStackingGroups(data))} // frame
|
||||
config={config}
|
||||
timeRange={timeRange}
|
||||
width={200}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
|
||||
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
||||
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
||||
import { AxisPlacement } from '@grafana/schema';
|
||||
import { pluginLog } from '../utils';
|
||||
import { getStackingBands, pluginLog, StackingGroup } from '../utils';
|
||||
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
|
||||
|
||||
const cursorDefaults: Cursor = {
|
||||
@@ -33,12 +33,14 @@ const cursorDefaults: Cursor = {
|
||||
};
|
||||
|
||||
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
|
||||
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
private series: UPlotSeriesBuilder[] = [];
|
||||
private axes: Record<string, UPlotAxisBuilder> = {};
|
||||
private scales: UPlotScaleBuilder[] = [];
|
||||
private bands: Band[] = [];
|
||||
private stackingGroups: StackingGroup[] = [];
|
||||
private cursor: Cursor | undefined;
|
||||
private select: uPlot.Select | undefined;
|
||||
private hasLeftAxis = false;
|
||||
@@ -143,6 +145,14 @@ export class UPlotConfigBuilder {
|
||||
this.bands.push(band);
|
||||
}
|
||||
|
||||
setStackingGroups(groups: StackingGroup[]) {
|
||||
this.stackingGroups = groups;
|
||||
}
|
||||
|
||||
getStackingGroups() {
|
||||
return this.stackingGroups;
|
||||
}
|
||||
|
||||
setTooltipInterpolator(interpolator: PlotTooltipInterpolator) {
|
||||
this.tooltipInterpolator = interpolator;
|
||||
}
|
||||
@@ -151,10 +161,10 @@ export class UPlotConfigBuilder {
|
||||
return this.tooltipInterpolator;
|
||||
}
|
||||
|
||||
setPrepData(prepData: PrepData) {
|
||||
setPrepData(prepData: PreDataStacked) {
|
||||
this.prepData = (frames) => {
|
||||
this.frames = frames;
|
||||
return prepData(frames);
|
||||
return prepData(frames, this.getStackingGroups());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -221,6 +231,14 @@ export class UPlotConfigBuilder {
|
||||
config.tzDate = this.tzDate;
|
||||
config.padding = this.padding;
|
||||
|
||||
if (this.stackingGroups.length) {
|
||||
this.stackingGroups.forEach((group) => {
|
||||
getStackingBands(group).forEach((band) => {
|
||||
this.addBand(band);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (this.bands.length) {
|
||||
config.bands = this.bands;
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ function mapDrawStyleToPathBuilder(
|
||||
lineInterpolation?: LineInterpolation,
|
||||
barAlignment = 0,
|
||||
barWidthFactor = 0.6,
|
||||
barMaxWidth = Infinity
|
||||
barMaxWidth = 200
|
||||
): Series.PathBuilder {
|
||||
const pathBuilders = uPlot.paths;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { orderIdsByCalcs, preparePlotData, timeFormatToTemplate } from './utils';
|
||||
import { getStackingGroups, preparePlotData2, timeFormatToTemplate } from './utils';
|
||||
import { FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { GraphTransform, StackingMode } from '@grafana/schema';
|
||||
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
|
||||
import Units from 'ol/proj/Units';
|
||||
|
||||
describe('timeFormatToTemplate', () => {
|
||||
it.each`
|
||||
@@ -16,7 +17,7 @@ describe('timeFormatToTemplate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('preparePlotData', () => {
|
||||
describe('preparePlotData2', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
@@ -27,7 +28,7 @@ describe('preparePlotData', () => {
|
||||
});
|
||||
|
||||
it('creates array from DataFrame', () => {
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -63,7 +64,7 @@ describe('preparePlotData', () => {
|
||||
{ name: 'c', values: [20, 20, 20], config: { custom: { transform: GraphTransform.NegativeY } } },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -104,7 +105,7 @@ describe('preparePlotData', () => {
|
||||
{ name: 'i', values: [20, undefined, 20, 20], config: { custom: { transform: GraphTransform.NegativeY } } },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -178,7 +179,7 @@ describe('preparePlotData', () => {
|
||||
{ name: 'c', values: [20, 20, 20] },
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -226,7 +227,7 @@ describe('preparePlotData', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -273,7 +274,7 @@ describe('preparePlotData', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -329,7 +330,7 @@ describe('preparePlotData', () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -397,7 +398,7 @@ describe('preparePlotData', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -472,7 +473,7 @@ describe('preparePlotData', () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
expect(preparePlotData2(df, getStackingGroups(df))).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
@@ -507,239 +508,330 @@ describe('preparePlotData', () => {
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
describe('ignores nullish-only stacks', () => {
|
||||
test('single stacking group', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, null, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, null, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, undefined, 20],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
null,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
null,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
null,
|
||||
30,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
test('multiple stacking groups', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, undefined, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, undefined, 10],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, undefined, 20],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
values: [1, 2, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
|
||||
},
|
||||
{
|
||||
name: 'f',
|
||||
values: [1, 2, null],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackB' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df])).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
null,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
null,
|
||||
20,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
null,
|
||||
40,
|
||||
],
|
||||
Array [
|
||||
1,
|
||||
2,
|
||||
null,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
4,
|
||||
null,
|
||||
],
|
||||
Array [
|
||||
3,
|
||||
6,
|
||||
null,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
describe('with legend sorted', () => {
|
||||
it('should affect when single group', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-10, 20, 10],
|
||||
state: { calcs: { max: 20 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [10, 10, 10],
|
||||
state: { calcs: { max: 10 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [20, 20, 20],
|
||||
state: { calcs: { max: 20 } },
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'stackA' } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: false } as any)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
30,
|
||||
20,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
10,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
50,
|
||||
40,
|
||||
],
|
||||
]
|
||||
`);
|
||||
expect(preparePlotData([df], undefined, { sortBy: 'Max', sortDesc: true } as any)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
9997,
|
||||
9998,
|
||||
9999,
|
||||
],
|
||||
Array [
|
||||
-10,
|
||||
20,
|
||||
10,
|
||||
],
|
||||
Array [
|
||||
20,
|
||||
50,
|
||||
40,
|
||||
],
|
||||
Array [
|
||||
10,
|
||||
40,
|
||||
30,
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('orderIdsByCalcs', () => {
|
||||
const ids = [1, 2, 3, 4];
|
||||
const frame = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [9997, 9998, 9999] },
|
||||
{ name: 'a', values: [-10, 20, 10], state: { calcs: { min: -10 } } },
|
||||
{ name: 'b', values: [20, 20, 20], state: { calcs: { min: 20 } } },
|
||||
{ name: 'c', values: [10, 10, 10], state: { calcs: { min: 10 } } },
|
||||
{ name: 'd', values: [30, 30, 30] },
|
||||
],
|
||||
describe('auto stacking groups', () => {
|
||||
test('split on stacking mode', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Percent } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [4, 5, 6],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ legend: undefined },
|
||||
{ legend: { sortBy: 'Min' } },
|
||||
{ legend: { sortDesc: false } },
|
||||
{ legend: {} },
|
||||
{ sortBy: 'Mik', sortDesc: true },
|
||||
])('should return without ordering if legend option is %o', (legend: any) => {
|
||||
const result = orderIdsByCalcs({ ids, frame, legend });
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
test('split pos/neg', () => {
|
||||
// since we expect most series to be Pos, we try to bail early when scanning all values
|
||||
// as soon as we find a value >= 0, it's assumed Pos, else Neg
|
||||
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-1, null, -3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [0, 0, 0],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
1,
|
||||
3,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should order the ids based on the frame stat', () => {
|
||||
const resultDesc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: true } as any });
|
||||
expect(resultDesc).toEqual([4, 2, 3, 1]);
|
||||
const resultAsc = orderIdsByCalcs({ ids, frame, legend: { sortBy: 'Min', sortDesc: false } as any });
|
||||
expect(resultAsc).toEqual([1, 3, 2, 4]);
|
||||
test('split pos/neg with NegY', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [-1, null, -3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal }, transform: GraphTransform.NegativeY } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [0, 0, 0],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
2,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('split on drawStyle, lineInterpolation, barAlignment', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Bars,
|
||||
barAlignment: BarAlignment.After,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Bars,
|
||||
barAlignment: BarAlignment.Before,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.Linear,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
lineInterpolation: LineInterpolation.Smooth,
|
||||
stacking: { mode: StackingMode.Normal },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { drawStyle: GraphDrawStyle.Points, stacking: { mode: StackingMode.Normal } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
4,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
5,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('split on axis & units (scaleKey)', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } }, unit: Units.FEET },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal } }, unit: Units.DEGREES },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
2,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('split on explicit stacking group & mode & pos/neg w/NegY', () => {
|
||||
const df = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'time', type: FieldType.time, values: [0, 1, 2] },
|
||||
{
|
||||
name: 'a',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'A' } } },
|
||||
},
|
||||
{
|
||||
name: 'b',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'A' } } },
|
||||
},
|
||||
{
|
||||
name: 'c',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Percent, group: 'A' } } },
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Normal, group: 'B' } } },
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, 3],
|
||||
config: { custom: { stacking: { mode: StackingMode.Percent, group: 'B' } } },
|
||||
},
|
||||
{
|
||||
name: 'e',
|
||||
values: [1, 2, 3],
|
||||
config: {
|
||||
custom: { stacking: { mode: StackingMode.Percent, group: 'B' }, transform: GraphTransform.NegativeY },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(getStackingGroups(df)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
1,
|
||||
2,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
3,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
4,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": 1,
|
||||
"series": Array [
|
||||
5,
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"dir": -1,
|
||||
"series": Array [
|
||||
6,
|
||||
],
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { DataFrame, ensureTimeField, Field, FieldType } from '@grafana/data';
|
||||
import { GraphFieldConfig, GraphTransform, StackingMode, VizLegendOptions } from '@grafana/schema';
|
||||
import { orderBy } from 'lodash';
|
||||
import { DataFrame, ensureTimeField, FieldType } from '@grafana/data';
|
||||
import { BarAlignment, GraphDrawStyle, GraphTransform, LineInterpolation, StackingMode } from '@grafana/schema';
|
||||
import uPlot, { AlignedData, Options, PaddingSide } from 'uplot';
|
||||
import { attachDebugger } from '../../utils';
|
||||
import { createLogger } from '../../utils/logger';
|
||||
import { buildScaleKey } from '../GraphNG/utils';
|
||||
|
||||
const ALLOWED_FORMAT_STRINGS_REGEX = /\b(YYYY|YY|MMMM|MMM|MM|M|DD|D|WWWW|WWW|HH|H|h|AA|aa|a|mm|m|ss|s|fff)\b/g;
|
||||
export const INTERNAL_NEGATIVE_Y_PREFIX = '__internalNegY';
|
||||
|
||||
export function timeFormatToTemplate(f: string) {
|
||||
return f.replace(ALLOWED_FORMAT_STRINGS_REGEX, (match) => `{${match}}`);
|
||||
@@ -41,115 +40,225 @@ interface StackMeta {
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotData(
|
||||
frames: DataFrame[],
|
||||
onStackMeta?: (meta: StackMeta) => void,
|
||||
legend?: VizLegendOptions
|
||||
): AlignedData {
|
||||
const frame = frames[0];
|
||||
const result: any[] = [];
|
||||
const stackingGroups: Map<string, number[]> = new Map();
|
||||
let seriesIndex = 0;
|
||||
|
||||
for (let i = 0; i < frame.fields.length; i++) {
|
||||
const f = frame.fields[i];
|
||||
|
||||
if (f.type === FieldType.time) {
|
||||
result.push(ensureTimeField(f).values.toArray());
|
||||
seriesIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
collectStackingGroups(f, stackingGroups, seriesIndex);
|
||||
const customConfig: GraphFieldConfig = f.config.custom || {};
|
||||
|
||||
const values = f.values.toArray();
|
||||
|
||||
if (customConfig.transform === GraphTransform.NegativeY) {
|
||||
result.push(values.map((v) => (v == null ? v : v * -1)));
|
||||
} else if (customConfig.transform === GraphTransform.Constant) {
|
||||
result.push(new Array(values.length).fill(values[0]));
|
||||
} else {
|
||||
result.push(values);
|
||||
}
|
||||
seriesIndex++;
|
||||
}
|
||||
|
||||
// Stacking
|
||||
if (stackingGroups.size !== 0) {
|
||||
const byPct = frame.fields[1].config.custom?.stacking?.mode === StackingMode.Percent;
|
||||
const dataLength = result[0].length;
|
||||
const alignedTotals = Array(stackingGroups.size);
|
||||
alignedTotals[0] = null;
|
||||
|
||||
// array or stacking groups
|
||||
for (const [_, seriesIds] of stackingGroups.entries()) {
|
||||
const seriesIdxs = orderIdsByCalcs({ ids: seriesIds, legend, frame });
|
||||
const noValueStack = Array(dataLength).fill(true);
|
||||
const groupTotals = byPct ? Array(dataLength).fill(0) : null;
|
||||
|
||||
if (byPct) {
|
||||
for (let j = 0; j < seriesIdxs.length; j++) {
|
||||
const currentlyStacking = result[seriesIdxs[j]];
|
||||
|
||||
for (let k = 0; k < dataLength; k++) {
|
||||
const v = currentlyStacking[k];
|
||||
groupTotals![k] += v == null ? 0 : +v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const acc = Array(dataLength).fill(0);
|
||||
|
||||
for (let j = 0; j < seriesIdxs.length; j++) {
|
||||
let seriesIdx = seriesIdxs[j];
|
||||
|
||||
alignedTotals[seriesIdx] = groupTotals;
|
||||
|
||||
const currentlyStacking = result[seriesIdx];
|
||||
|
||||
for (let k = 0; k < dataLength; k++) {
|
||||
const v = currentlyStacking[k];
|
||||
if (v != null && noValueStack[k]) {
|
||||
noValueStack[k] = false;
|
||||
}
|
||||
acc[k] += v == null ? 0 : v / (byPct ? groupTotals![k] : 1);
|
||||
}
|
||||
|
||||
result[seriesIdx] = acc.slice().map((v, i) => (noValueStack[i] ? null : v));
|
||||
}
|
||||
}
|
||||
|
||||
onStackMeta &&
|
||||
onStackMeta({
|
||||
totals: alignedTotals as AlignedData,
|
||||
});
|
||||
}
|
||||
|
||||
return result as AlignedData;
|
||||
export interface StackingGroup {
|
||||
series: number[];
|
||||
dir: StackDirection;
|
||||
}
|
||||
|
||||
export function collectStackingGroups(f: Field, groups: Map<string, number[]>, seriesIdx: number) {
|
||||
const customConfig = f.config.custom;
|
||||
if (!customConfig) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
customConfig.stacking?.mode !== StackingMode.None &&
|
||||
customConfig.stacking?.group &&
|
||||
!customConfig.hideFrom?.viz
|
||||
) {
|
||||
const group =
|
||||
customConfig.transform === GraphTransform.NegativeY
|
||||
? `${INTERNAL_NEGATIVE_Y_PREFIX}-${customConfig.stacking.group}`
|
||||
: customConfig.stacking.group;
|
||||
/** @internal */
|
||||
const enum StackDirection {
|
||||
Pos = 1,
|
||||
Neg = -1,
|
||||
}
|
||||
|
||||
if (!groups.has(group)) {
|
||||
groups.set(group, [seriesIdx]);
|
||||
} else {
|
||||
groups.set(group, groups.get(group)!.concat(seriesIdx));
|
||||
// generates bands between adjacent group series
|
||||
/** @internal */
|
||||
export function getStackingBands(group: StackingGroup) {
|
||||
let bands: uPlot.Band[] = [];
|
||||
let { series, dir } = group;
|
||||
let lastIdx = series.length - 1;
|
||||
|
||||
let rSeries = series.slice().reverse();
|
||||
|
||||
rSeries.forEach((si, i) => {
|
||||
if (i !== lastIdx) {
|
||||
let nextIdx = rSeries[i + 1];
|
||||
bands.push({
|
||||
series: [si, nextIdx],
|
||||
// fill direction is inverted from stack direction
|
||||
dir: (-1 * dir) as 1 | -1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return bands;
|
||||
}
|
||||
|
||||
// expects an AlignedFrame
|
||||
/** @internal */
|
||||
export function getStackingGroups(frame: DataFrame) {
|
||||
let groups: Map<string, StackingGroup> = new Map();
|
||||
|
||||
frame.fields.forEach(({ config, values }, i) => {
|
||||
// skip x or time field
|
||||
if (i === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { custom } = config;
|
||||
|
||||
if (custom == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: currently all AlignedFrame fields end up in uplot series & data, even custom.hideFrom?.viz
|
||||
// ideally hideFrom.viz fields would be excluded so we can remove this
|
||||
if (custom.hideFrom?.viz) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { stacking } = custom;
|
||||
|
||||
if (stacking == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { mode: stackingMode, group: stackingGroup } = stacking;
|
||||
|
||||
// not stacking
|
||||
if (stackingMode === StackingMode.None) {
|
||||
return;
|
||||
}
|
||||
|
||||
// will this be stacked up or down after any transforms applied
|
||||
let vals = values.toArray();
|
||||
let transform = custom.transform;
|
||||
let stackDir =
|
||||
transform === GraphTransform.Constant
|
||||
? vals[0] > 0
|
||||
? StackDirection.Pos
|
||||
: StackDirection.Neg
|
||||
: transform === GraphTransform.NegativeY
|
||||
? vals.some((v) => v > 0)
|
||||
? StackDirection.Neg
|
||||
: StackDirection.Pos
|
||||
: vals.some((v) => v > 0)
|
||||
? StackDirection.Pos
|
||||
: StackDirection.Neg;
|
||||
|
||||
let drawStyle = custom.drawStyle as GraphDrawStyle;
|
||||
let drawStyle2 =
|
||||
drawStyle === GraphDrawStyle.Bars
|
||||
? (custom.barAlignment as BarAlignment)
|
||||
: drawStyle === GraphDrawStyle.Line
|
||||
? (custom.lineInterpolation as LineInterpolation)
|
||||
: null;
|
||||
|
||||
let stackKey = `${stackDir}|${stackingMode}|${stackingGroup}|${buildScaleKey(config)}|${drawStyle}|${drawStyle2}`;
|
||||
|
||||
let group = groups.get(stackKey);
|
||||
|
||||
if (group == null) {
|
||||
group = {
|
||||
series: [],
|
||||
dir: stackDir,
|
||||
};
|
||||
|
||||
groups.set(stackKey, group);
|
||||
}
|
||||
|
||||
group.series.push(i);
|
||||
});
|
||||
|
||||
return [...groups.values()];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotData2(
|
||||
frame: DataFrame,
|
||||
stackingGroups: StackingGroup[],
|
||||
onStackMeta?: (meta: StackMeta) => void
|
||||
) {
|
||||
let data = Array(frame.fields.length) as AlignedData;
|
||||
|
||||
let dataLen = frame.length;
|
||||
let zeroArr = Array(dataLen).fill(0);
|
||||
let accums = Array.from({ length: stackingGroups.length }, () => zeroArr.slice());
|
||||
|
||||
frame.fields.forEach((field, i) => {
|
||||
let vals = field.values.toArray();
|
||||
|
||||
if (i === 0) {
|
||||
if (field.type === FieldType.time) {
|
||||
data[i] = ensureTimeField(field).values.toArray();
|
||||
} else {
|
||||
data[i] = vals;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let { custom } = field.config;
|
||||
|
||||
if (!custom || custom.hideFrom?.viz) {
|
||||
data[i] = vals;
|
||||
return;
|
||||
}
|
||||
|
||||
// apply transforms
|
||||
if (custom.transform === GraphTransform.Constant) {
|
||||
vals = Array(vals.length).fill(vals[0]);
|
||||
} else {
|
||||
vals = vals.slice();
|
||||
|
||||
if (custom.transform === GraphTransform.NegativeY) {
|
||||
for (let i = 0; i < vals.length; i++) {
|
||||
if (vals[i] != null) {
|
||||
vals[i] *= -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stackingMode = custom.stacking?.mode;
|
||||
|
||||
if (!stackingMode || stackingMode === StackingMode.None) {
|
||||
data[i] = vals;
|
||||
} else {
|
||||
let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1);
|
||||
|
||||
let accum = accums[stackIdx];
|
||||
let stacked = (data[i] = Array(dataLen));
|
||||
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
let v = vals[i];
|
||||
|
||||
if (v != null) {
|
||||
stacked[i] = accum[i] += v;
|
||||
} else {
|
||||
stacked[i] = v; // we may want to coerce to 0 here
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (onStackMeta) {
|
||||
let accumsBySeriesIdx = data.map((vals, i) => {
|
||||
let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1);
|
||||
return stackIdx !== -1 ? accums[stackIdx] : vals;
|
||||
});
|
||||
|
||||
onStackMeta({
|
||||
totals: accumsBySeriesIdx as AlignedData,
|
||||
});
|
||||
}
|
||||
|
||||
// re-compute by percent
|
||||
frame.fields.forEach((field, i) => {
|
||||
if (i === 0 || field.config.custom?.hideFrom?.viz) {
|
||||
return;
|
||||
}
|
||||
|
||||
let stackingMode = field.config.custom?.stacking?.mode;
|
||||
|
||||
if (stackingMode === StackingMode.Percent) {
|
||||
let stackIdx = stackingGroups.findIndex((group) => group.series.indexOf(i) > -1);
|
||||
let accum = accums[stackIdx];
|
||||
let group = stackingGroups[stackIdx];
|
||||
|
||||
let stacked = data[i];
|
||||
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
let v = stacked[i];
|
||||
|
||||
if (v != null) {
|
||||
// v / accum will always be pos, so properly (re)sign by group stacking dir
|
||||
stacked[i] = group.dir * (v / accum[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,23 +324,3 @@ export const pluginLogger = createLogger('uPlot');
|
||||
export const pluginLog = pluginLogger.logger;
|
||||
// pluginLogger.enable();
|
||||
attachDebugger('graphng', undefined, pluginLogger);
|
||||
|
||||
type OrderIdsByCalcsOptions = {
|
||||
legend?: VizLegendOptions;
|
||||
ids: number[];
|
||||
frame: DataFrame;
|
||||
};
|
||||
export function orderIdsByCalcs({ legend, ids, frame }: OrderIdsByCalcsOptions) {
|
||||
if (!legend?.sortBy || legend.sortDesc == null) {
|
||||
return ids;
|
||||
}
|
||||
const orderedIds = orderBy<number>(
|
||||
ids,
|
||||
(id) => {
|
||||
return frame.fields[id].state?.calcs?.[legend.sortBy!.toLowerCase()];
|
||||
},
|
||||
legend.sortDesc ? 'desc' : 'asc'
|
||||
);
|
||||
|
||||
return orderedIds;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,6 @@ const ScaleDistributionEditor: React.FC<FieldOverrideEditorProps<ScaleDistributi
|
||||
<Select
|
||||
menuShouldPortal
|
||||
allowCustomValue={false}
|
||||
autoFocus
|
||||
options={LOG_DISTRIBUTION_OPTIONS}
|
||||
value={value.log || 2}
|
||||
prefix={'base'}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@jaegertracing/jaeger-ui-components",
|
||||
"version": "8.5.0-pre",
|
||||
"version": "8.5.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
@@ -26,8 +26,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "11.7.1",
|
||||
"@grafana/data": "8.5.0-pre",
|
||||
"@grafana/ui": "8.5.0-pre",
|
||||
"@grafana/data": "8.5.0",
|
||||
"@grafana/ui": "8.5.0",
|
||||
"chance": "^1.0.10",
|
||||
"classnames": "^2.2.5",
|
||||
"combokeys": "^3.0.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"lodash": "4.17.21",
|
||||
"lru-memoize": "^1.1.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"moment": "2.29.1",
|
||||
"moment": "2.29.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"prop-types": "15.8.1",
|
||||
"react": "17.0.2",
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
@@ -149,6 +150,23 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
Grants: []string{string(models.ROLE_VIEWER)},
|
||||
}
|
||||
|
||||
apikeyReaderRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 1,
|
||||
Name: "fixed:apikeys:reader",
|
||||
DisplayName: "APIKeys reader",
|
||||
Description: "Gives access to read api keys.",
|
||||
Group: "API Keys",
|
||||
Permissions: []ac.Permission{
|
||||
{
|
||||
Action: ac.ActionAPIKeyRead,
|
||||
Scope: ac.ScopeAPIKeysAll,
|
||||
},
|
||||
},
|
||||
},
|
||||
Grants: []string{string(models.ROLE_ADMIN)},
|
||||
}
|
||||
|
||||
apikeyWriterRole := ac.RoleRegistration{
|
||||
Role: ac.RoleDTO{
|
||||
Version: 1,
|
||||
@@ -156,19 +174,15 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
DisplayName: "APIKeys writer",
|
||||
Description: "Gives access to add and delete api keys.",
|
||||
Group: "API Keys",
|
||||
Permissions: []ac.Permission{
|
||||
Permissions: ac.ConcatPermissions(apikeyReaderRole.Role.Permissions, []ac.Permission{
|
||||
{
|
||||
Action: ac.ActionAPIKeyCreate,
|
||||
},
|
||||
{
|
||||
Action: ac.ActionAPIKeyRead,
|
||||
Scope: ac.ScopeAPIKeysAll,
|
||||
},
|
||||
{
|
||||
Action: ac.ActionAPIKeyDelete,
|
||||
Scope: ac.ScopeAPIKeysAll,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
Grants: []string{string(models.ROLE_ADMIN)},
|
||||
}
|
||||
@@ -410,7 +424,7 @@ func (hs *HTTPServer) declareFixedRoles() error {
|
||||
orgMaintainerRole, teamsCreatorRole, teamsWriterRole, datasourcesExplorerRole,
|
||||
annotationsReaderRole, dashboardAnnotationsWriterRole, annotationsWriterRole,
|
||||
dashboardsCreatorRole, dashboardsReaderRole, dashboardsWriterRole,
|
||||
foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyWriterRole,
|
||||
foldersCreatorRole, foldersReaderRole, foldersWriterRole, apikeyReaderRole, apikeyWriterRole,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -462,6 +476,12 @@ var teamsEditAccessEvaluator = ac.EvalAll(
|
||||
),
|
||||
)
|
||||
|
||||
// apiKeyAccessEvaluator is used to protect the "Configuration > API keys" page access
|
||||
var apiKeyAccessEvaluator = ac.EvalPermission(ac.ActionAPIKeyRead)
|
||||
|
||||
// serviceAccountAccessEvaluator is used to protect the "Configuration > Service accounts" page access
|
||||
var serviceAccountAccessEvaluator = ac.EvalPermission(serviceaccounts.ActionRead)
|
||||
|
||||
// Metadata helpers
|
||||
// getAccessControlMetadata returns the accesscontrol metadata associated with a given resource
|
||||
func (hs *HTTPServer) getAccessControlMetadata(c *models.ReqContext,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/auth"
|
||||
@@ -49,11 +47,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
|
||||
mock := mockstore.NewSQLStoreMock()
|
||||
adminLogoutUserScenario(t, "Should not be allowed when calling POST on",
|
||||
"/api/admin/users/1/logout", "/api/admin/users/:id/logout", func(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.GetUserByIdQuery) error {
|
||||
cmd.Result = &models.User{Id: testUserID}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, 400, sc.resp.Code)
|
||||
}, mock)
|
||||
@@ -182,8 +175,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
|
||||
@@ -202,8 +193,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, 200, sc.resp.Code)
|
||||
|
||||
@@ -222,8 +211,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
adminCreateUserScenario(t, "Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, 400, sc.resp.Code)
|
||||
|
||||
@@ -241,8 +228,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
adminCreateUserScenario(t, "Should return an error", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
|
||||
bus.ClearBusHandlers()
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
assert.Equal(t, 412, sc.resp.Code)
|
||||
|
||||
@@ -256,8 +241,6 @@ func TestAdminAPIEndpoint(t *testing.T) {
|
||||
func putAdminScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
|
||||
cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc, sqlStore sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
SQLStore: sqlStore,
|
||||
@@ -284,10 +267,7 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string
|
||||
|
||||
func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
AuthTokenService: auth.NewFakeUserAuthTokenService(),
|
||||
SQLStore: sqlStore,
|
||||
}
|
||||
@@ -312,12 +292,9 @@ func adminLogoutUserScenario(t *testing.T, desc string, url string, routePattern
|
||||
|
||||
func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, routePattern string, cmd models.RevokeAuthTokenCmd, fn scenarioFunc, sqlStore sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
AuthTokenService: fakeAuthTokenService,
|
||||
SQLStore: sqlStore,
|
||||
}
|
||||
@@ -343,12 +320,9 @@ func adminRevokeUserAuthTokenScenario(t *testing.T, desc string, url string, rou
|
||||
|
||||
func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, routePattern string, fn scenarioFunc, sqlStore sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
AuthTokenService: fakeAuthTokenService,
|
||||
SQLStore: sqlStore,
|
||||
}
|
||||
@@ -372,14 +346,11 @@ func adminGetUserAuthTokensScenario(t *testing.T, desc string, url string, route
|
||||
|
||||
func adminDisableUserScenario(t *testing.T, desc string, action string, url string, routePattern string, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
|
||||
|
||||
authInfoService := &logintest.AuthInfoServiceFake{}
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
SQLStore: mockstore.NewSQLStoreMock(),
|
||||
AuthTokenService: fakeAuthTokenService,
|
||||
authInfoService: authInfoService,
|
||||
@@ -410,8 +381,6 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern
|
||||
SQLStore: mockstore.NewSQLStoreMock(),
|
||||
}
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
sc.sqlStore = hs.SQLStore
|
||||
sc.authInfoService = &logintest.AuthInfoServiceFake{}
|
||||
@@ -430,10 +399,7 @@ func adminDeleteUserScenario(t *testing.T, desc string, url string, routePattern
|
||||
|
||||
func adminCreateUserScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
Login: loginservice.LoginServiceMock{
|
||||
ExpectedUserForm: cmd,
|
||||
NoExistingOrgId: nonExistingOrgID,
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
@@ -491,7 +490,7 @@ func (hs *HTTPServer) NotificationTest(c *models.ReqContext) response.Response {
|
||||
SecureSettings: dto.SecureSettings,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(c.Req.Context(), cmd); err != nil {
|
||||
if err := hs.AlertNotificationService.HandleNotificationTestCommand(c.Req.Context(), cmd); err != nil {
|
||||
if errors.Is(err, models.ErrSmtpNotEnabled) {
|
||||
return response.Error(412, err.Error(), err)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/search"
|
||||
@@ -124,18 +123,12 @@ func TestAlertingAPIEndpoint(t *testing.T) {
|
||||
}
|
||||
|
||||
func callPauseAlert(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.PauseAlertCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func postAlertScenario(t *testing.T, hs *HTTPServer, desc string, url string, routePattern string, role models.RoleType,
|
||||
cmd dtos.PauseAlertCommand, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||
c.Req.Body = mockRequestBody(cmd)
|
||||
|
||||
@@ -20,17 +20,18 @@ import (
|
||||
|
||||
func (hs *HTTPServer) GetAnnotations(c *models.ReqContext) response.Response {
|
||||
query := &annotations.ItemQuery{
|
||||
From: c.QueryInt64("from"),
|
||||
To: c.QueryInt64("to"),
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.QueryInt64("userId"),
|
||||
AlertId: c.QueryInt64("alertId"),
|
||||
DashboardId: c.QueryInt64("dashboardId"),
|
||||
PanelId: c.QueryInt64("panelId"),
|
||||
Limit: c.QueryInt64("limit"),
|
||||
Tags: c.QueryStrings("tags"),
|
||||
Type: c.Query("type"),
|
||||
MatchAny: c.QueryBool("matchAny"),
|
||||
From: c.QueryInt64("from"),
|
||||
To: c.QueryInt64("to"),
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.QueryInt64("userId"),
|
||||
AlertId: c.QueryInt64("alertId"),
|
||||
DashboardId: c.QueryInt64("dashboardId"),
|
||||
PanelId: c.QueryInt64("panelId"),
|
||||
Limit: c.QueryInt64("limit"),
|
||||
Tags: c.QueryStrings("tags"),
|
||||
Type: c.Query("type"),
|
||||
MatchAny: c.QueryBool("matchAny"),
|
||||
SignedInUser: c.SignedInUser,
|
||||
}
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
@@ -63,22 +64,7 @@ func (hs *HTTPServer) PostAnnotation(c *models.ReqContext) response.Response {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
|
||||
var canSave bool
|
||||
var err error
|
||||
if cmd.DashboardId != 0 {
|
||||
canSave, err = canSaveDashboardAnnotation(c, cmd.DashboardId)
|
||||
} else { // organization annotations
|
||||
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
canSave = canSaveOrganizationAnnotation(c)
|
||||
} else {
|
||||
// This is an additional validation needed only for FGAC Organization Annotations.
|
||||
// It is not possible to do it in the middleware because we need to look
|
||||
// into the request to determine if this is a Organization annotation or not
|
||||
canSave, err = hs.canCreateOrganizationAnnotation(c)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || !canSave {
|
||||
if canSave, err := hs.canCreateAnnotation(c, cmd.DashboardId); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
@@ -192,21 +178,12 @@ func (hs *HTTPServer) UpdateAnnotation(c *models.ReqContext) response.Response {
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.OrgId)
|
||||
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.SignedInUser)
|
||||
if resp != nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
canSave := true
|
||||
if annotation.GetType() == annotations.Dashboard {
|
||||
canSave, err = canSaveDashboardAnnotation(c, annotation.DashboardId)
|
||||
} else {
|
||||
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
canSave = canSaveOrganizationAnnotation(c)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || !canSave {
|
||||
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
@@ -239,20 +216,12 @@ func (hs *HTTPServer) PatchAnnotation(c *models.ReqContext) response.Response {
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.OrgId)
|
||||
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.SignedInUser)
|
||||
if resp != nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
canSave := true
|
||||
if annotation.GetType() == annotations.Dashboard {
|
||||
canSave, err = canSaveDashboardAnnotation(c, annotation.DashboardId)
|
||||
} else {
|
||||
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
canSave = canSaveOrganizationAnnotation(c)
|
||||
}
|
||||
}
|
||||
if err != nil || !canSave {
|
||||
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
@@ -310,7 +279,7 @@ func (hs *HTTPServer) MassDeleteAnnotations(c *models.ReqContext) response.Respo
|
||||
var dashboardId int64
|
||||
|
||||
if cmd.AnnotationId != 0 {
|
||||
annotation, respErr := findAnnotationByID(c.Req.Context(), repo, cmd.AnnotationId, c.OrgId)
|
||||
annotation, respErr := findAnnotationByID(c.Req.Context(), repo, cmd.AnnotationId, c.SignedInUser)
|
||||
if respErr != nil {
|
||||
return respErr
|
||||
}
|
||||
@@ -358,21 +327,12 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Respon
|
||||
|
||||
repo := annotations.GetRepository()
|
||||
|
||||
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.OrgId)
|
||||
annotation, resp := findAnnotationByID(c.Req.Context(), repo, annotationID, c.SignedInUser)
|
||||
if resp != nil {
|
||||
return resp
|
||||
}
|
||||
|
||||
canSave := true
|
||||
if annotation.GetType() == annotations.Dashboard {
|
||||
canSave, err = canSaveDashboardAnnotation(c, annotation.DashboardId)
|
||||
} else {
|
||||
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
canSave = canSaveOrganizationAnnotation(c)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil || !canSave {
|
||||
if canSave, err := hs.canSaveAnnotation(c, annotation); err != nil || !canSave {
|
||||
return dashboardGuardianResponse(err)
|
||||
}
|
||||
|
||||
@@ -387,7 +347,18 @@ func (hs *HTTPServer) DeleteAnnotationByID(c *models.ReqContext) response.Respon
|
||||
return response.Success("Annotation deleted")
|
||||
}
|
||||
|
||||
func canSaveDashboardAnnotation(c *models.ReqContext, dashboardID int64) (bool, error) {
|
||||
func (hs *HTTPServer) canSaveAnnotation(c *models.ReqContext, annotation *annotations.ItemDTO) (bool, error) {
|
||||
if annotation.GetType() == annotations.Dashboard {
|
||||
return canEditDashboard(c, annotation.DashboardId)
|
||||
} else {
|
||||
if !hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
return c.SignedInUser.HasRole(models.ROLE_EDITOR), nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func canEditDashboard(c *models.ReqContext, dashboardID int64) (bool, error) {
|
||||
guard := guardian.New(c.Req.Context(), dashboardID, c.OrgId, c.SignedInUser)
|
||||
if canEdit, err := guard.CanEdit(); err != nil || !canEdit {
|
||||
return false, err
|
||||
@@ -396,12 +367,13 @@ func canSaveDashboardAnnotation(c *models.ReqContext, dashboardID int64) (bool,
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func canSaveOrganizationAnnotation(c *models.ReqContext) bool {
|
||||
return c.SignedInUser.HasRole(models.ROLE_EDITOR)
|
||||
}
|
||||
|
||||
func findAnnotationByID(ctx context.Context, repo annotations.Repository, annotationID int64, orgID int64) (*annotations.ItemDTO, response.Response) {
|
||||
items, err := repo.Find(ctx, &annotations.ItemQuery{AnnotationId: annotationID, OrgId: orgID})
|
||||
func findAnnotationByID(ctx context.Context, repo annotations.Repository, annotationID int64, user *models.SignedInUser) (*annotations.ItemDTO, response.Response) {
|
||||
query := &annotations.ItemQuery{
|
||||
AnnotationId: annotationID,
|
||||
OrgId: user.OrgId,
|
||||
SignedInUser: user,
|
||||
}
|
||||
items, err := repo.Find(ctx, query)
|
||||
|
||||
if err != nil {
|
||||
return nil, response.Error(500, "Failed to find annotation", err)
|
||||
@@ -446,9 +418,21 @@ func AnnotationTypeScopeResolver() (string, accesscontrol.AttributeScopeResolveF
|
||||
return "", accesscontrol.ErrInvalidScope
|
||||
}
|
||||
|
||||
annotation, resp := findAnnotationByID(ctx, annotations.GetRepository(), int64(annotationId), orgID)
|
||||
// tempUser is used to resolve annotation type.
|
||||
// The annotation doesn't get returned to the real user, so real user's permissions don't matter here.
|
||||
tempUser := &models.SignedInUser{
|
||||
OrgId: orgID,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgID: {
|
||||
accesscontrol.ActionDashboardsRead: {accesscontrol.ScopeDashboardsAll},
|
||||
accesscontrol.ActionAnnotationsRead: {accesscontrol.ScopeAnnotationsAll},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
annotation, resp := findAnnotationByID(ctx, annotations.GetRepository(), int64(annotationId), tempUser)
|
||||
if resp != nil {
|
||||
return "", err
|
||||
return "", errors.New("could not resolve annotation type")
|
||||
}
|
||||
|
||||
if annotation.GetType() == annotations.Organization {
|
||||
@@ -460,9 +444,23 @@ func AnnotationTypeScopeResolver() (string, accesscontrol.AttributeScopeResolveF
|
||||
return accesscontrol.ScopeAnnotationsProvider.GetResourceScope(""), annotationTypeResolver
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) canCreateOrganizationAnnotation(c *models.ReqContext) (bool, error) {
|
||||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
|
||||
func (hs *HTTPServer) canCreateAnnotation(c *models.ReqContext, dashboardId int64) (bool, error) {
|
||||
if dashboardId != 0 {
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeDashboard)
|
||||
if canSave, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canSave {
|
||||
return canSave, err
|
||||
}
|
||||
}
|
||||
return canEditDashboard(c, dashboardId)
|
||||
} else { // organization annotations
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
evaluator := accesscontrol.EvalPermission(accesscontrol.ActionAnnotationsCreate, accesscontrol.ScopeAnnotationsTypeOrganization)
|
||||
return hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator)
|
||||
} else {
|
||||
return c.SignedInUser.HasRole(models.ROLE_EDITOR), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) canMassDeleteAnnotations(c *models.ReqContext, dashboardID int64) (bool, error) {
|
||||
@@ -476,7 +474,7 @@ func (hs *HTTPServer) canMassDeleteAnnotations(c *models.ReqContext, dashboardID
|
||||
return false, err
|
||||
}
|
||||
|
||||
canSave, err = canSaveDashboardAnnotation(c, dashboardID)
|
||||
canSave, err = canEditDashboard(c, dashboardID)
|
||||
if err != nil || !canSave {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
@@ -293,8 +292,6 @@ var fakeAnnoRepo *fakeAnnotationsRepo
|
||||
func postAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
|
||||
cmd dtos.PostAnnotationsCmd, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
hs := setupSimpleHTTPServer(nil)
|
||||
store := sqlstore.InitTestDB(t)
|
||||
store.Cfg = hs.Cfg
|
||||
@@ -324,8 +321,6 @@ func postAnnotationScenario(t *testing.T, desc string, url string, routePattern
|
||||
func putAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
|
||||
cmd dtos.UpdateAnnotationsCmd, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
hs := setupSimpleHTTPServer(nil)
|
||||
store := sqlstore.InitTestDB(t)
|
||||
store.Cfg = hs.Cfg
|
||||
@@ -354,8 +349,6 @@ func putAnnotationScenario(t *testing.T, desc string, url string, routePattern s
|
||||
|
||||
func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
hs := setupSimpleHTTPServer(nil)
|
||||
store := sqlstore.InitTestDB(t)
|
||||
store.Cfg = hs.Cfg
|
||||
@@ -385,8 +378,6 @@ func patchAnnotationScenario(t *testing.T, desc string, url string, routePattern
|
||||
func deleteAnnotationsScenario(t *testing.T, desc string, url string, routePattern string, role models.RoleType,
|
||||
cmd dtos.MassDeleteAnnotationsCmd, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
hs := setupSimpleHTTPServer(nil)
|
||||
store := sqlstore.InitTestDB(t)
|
||||
store.Cfg = hs.Cfg
|
||||
@@ -624,6 +615,18 @@ func TestAPI_Annotations_AccessControl(t *testing.T) {
|
||||
},
|
||||
want: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "AccessControl create dashboard annotation with incorrect permissions is forbidden",
|
||||
args: args{
|
||||
permissions: []*accesscontrol.Permission{{
|
||||
Action: accesscontrol.ActionAnnotationsCreate, Scope: accesscontrol.ScopeAnnotationsTypeOrganization,
|
||||
}},
|
||||
url: "/api/annotations",
|
||||
method: http.MethodPost,
|
||||
body: mockRequestBody(postDashboardCmd),
|
||||
},
|
||||
want: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "AccessControl create organization annotation with permissions is allowed",
|
||||
args: args{
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
var plog = log.New("api")
|
||||
@@ -61,9 +63,9 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/org/teams", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsRead)), hs.Index)
|
||||
r.Get("/org/teams/edit/*", authorize(reqCanAccessTeams, teamsEditAccessEvaluator), hs.Index)
|
||||
r.Get("/org/teams/new", authorize(reqCanAccessTeams, ac.EvalPermission(ac.ActionTeamsCreate)), hs.Index)
|
||||
r.Get("/org/serviceaccounts", middleware.ReqOrgAdmin, hs.Index)
|
||||
r.Get("/org/serviceaccounts/:serviceAccountId", middleware.ReqOrgAdmin, hs.Index)
|
||||
r.Get("/org/apikeys/", reqOrgAdmin, hs.Index)
|
||||
r.Get("/org/serviceaccounts", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index)
|
||||
r.Get("/org/serviceaccounts/:serviceAccountId", authorize(reqOrgAdmin, ac.EvalPermission(serviceaccounts.ActionRead)), hs.Index)
|
||||
r.Get("/org/apikeys/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead)), hs.Index)
|
||||
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
|
||||
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin", reqGrafanaAdmin, hs.Index)
|
||||
@@ -278,7 +280,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
keysRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead, ac.ScopeAPIKeysAll)), routing.Wrap(hs.GetAPIKeys))
|
||||
keysRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyCreate)), quota("api_key"), routing.Wrap(hs.AddAPIKey))
|
||||
keysRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyDelete, apikeyIDScope)), routing.Wrap(hs.DeleteAPIKey))
|
||||
}, reqOrgAdmin)
|
||||
})
|
||||
|
||||
// Preferences
|
||||
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
|
||||
@@ -417,7 +419,14 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard))
|
||||
})
|
||||
|
||||
apiRoute.Get("/alert-notifiers", reqEditorRole, routing.Wrap(
|
||||
var notifiersAuthHandler web.Handler
|
||||
if hs.Cfg.UnifiedAlerting.IsEnabled() {
|
||||
notifiersAuthHandler = reqSignedIn
|
||||
} else {
|
||||
notifiersAuthHandler = reqEditorRole
|
||||
}
|
||||
|
||||
apiRoute.Get("/alert-notifiers", notifiersAuthHandler, routing.Wrap(
|
||||
hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())),
|
||||
)
|
||||
|
||||
@@ -438,7 +447,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
orgRoute.Get("/lookup", routing.Wrap(hs.GetAlertNotificationLookup))
|
||||
})
|
||||
|
||||
apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead, ac.ScopeAnnotationsAll)), routing.Wrap(hs.GetAnnotations))
|
||||
apiRoute.Get("/annotations", authorize(reqSignedIn, ac.EvalPermission(ac.ActionAnnotationsRead)), routing.Wrap(hs.GetAnnotations))
|
||||
apiRoute.Post("/annotations/mass-delete", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAnnotationsDelete)), routing.Wrap(hs.MassDeleteAnnotations))
|
||||
|
||||
apiRoute.Group("/annotations", func(annotationsRoute routing.RouteRegister) {
|
||||
|
||||
@@ -70,6 +70,9 @@ func (hs *HTTPServer) AddAPIKey(c *models.ReqContext) response.Response {
|
||||
if !cmd.Role.IsValid() {
|
||||
return response.Error(400, "Invalid role specified", nil)
|
||||
}
|
||||
if !c.OrgRole.Includes(cmd.Role) {
|
||||
return response.Error(http.StatusForbidden, "Cannot assign a role higher than user's role", nil)
|
||||
}
|
||||
|
||||
if hs.Cfg.ApiKeyMaxSecondsToLive != -1 {
|
||||
if cmd.SecondsToLive == 0 {
|
||||
|
||||
@@ -27,10 +27,10 @@ func (hs *HTTPServer) initAppPluginRoutes(r *web.Mux) {
|
||||
Renegotiation: tls.RenegotiateFreelyAsClient,
|
||||
},
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
}).DialContext,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ func AppPluginRoute(route *plugins.Route, appID string, hs *HTTPServer) web.Hand
|
||||
|
||||
proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID, hs.Cfg, hs.PluginSettings, hs.SecretsService)
|
||||
proxy.Transport = pluginProxyTransport
|
||||
|
||||
proxy.ServeHTTP(c.Resp, c.Req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||
@@ -53,8 +52,6 @@ func loggedInUserScenario(t *testing.T, desc string, url string, routePattern st
|
||||
|
||||
func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url string, routePattern string, role models.RoleType, fn scenarioFunc, sqlStore sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
sc.sqlStore = sqlStore
|
||||
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||
@@ -82,8 +79,6 @@ func loggedInUserScenarioWithRole(t *testing.T, desc string, method string, url
|
||||
|
||||
func anonymousUserScenario(t *testing.T, desc string, method string, url string, routePattern string, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := setupScenarioContext(t, url)
|
||||
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||
sc.context = c
|
||||
@@ -197,7 +192,9 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa
|
||||
tracer, err := tracing.InitializeTracerForTest()
|
||||
require.NoError(t, err)
|
||||
authProxy := authproxy.ProvideAuthProxy(cfg, remoteCacheSvc, loginservice.LoginServiceMock{}, sqlStore)
|
||||
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy)
|
||||
loginService := &logintest.LoginServiceFake{}
|
||||
authenticator := &logintest.AuthenticatorFake{}
|
||||
ctxHdlr := contexthandler.ProvideService(cfg, userAuthTokenSvc, authJWTSvc, remoteCacheSvc, renderSvc, sqlStore, tracer, authProxy, loginService, authenticator)
|
||||
|
||||
return ctxHdlr
|
||||
}
|
||||
@@ -238,7 +235,6 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
|
||||
store := sqlstore.InitTestDB(t)
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Bus: bus.GetBus(),
|
||||
Live: newTestLive(t, store),
|
||||
Features: features,
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
@@ -329,7 +325,6 @@ func setupSimpleHTTPServer(features *featuremgmt.FeatureManager) *HTTPServer {
|
||||
return &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus.GetBus(),
|
||||
AccessControl: accesscontrolmock.New().WithDisabled(),
|
||||
}
|
||||
}
|
||||
@@ -380,7 +375,6 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus.GetBus(),
|
||||
Live: newTestLive(t, db),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
RouteRegister: routeRegister,
|
||||
|
||||
@@ -453,9 +453,10 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) response.Response {
|
||||
dash.Meta.IsHome = true
|
||||
dash.Meta.CanEdit = c.SignedInUser.HasRole(models.ROLE_EDITOR)
|
||||
dash.Meta.FolderTitle = "General"
|
||||
dash.Dashboard = simplejson.New()
|
||||
|
||||
jsonParser := json.NewDecoder(file)
|
||||
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
|
||||
if err := jsonParser.Decode(dash.Dashboard); err != nil {
|
||||
return response.Error(500, "Failed to load home dashboard", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -9,7 +8,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboardsnapshots"
|
||||
@@ -52,12 +50,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) {
|
||||
sqlmock.ExpectedDashboardAclInfoList = aclMockResp
|
||||
sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{}
|
||||
|
||||
// we need it here for now for the guadian service to work
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error {
|
||||
query.Result = aclMockResp
|
||||
return nil
|
||||
})
|
||||
|
||||
return mockSnapshotResult
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/usagestats"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
@@ -46,7 +45,7 @@ func TestGetHomeDashboard(t *testing.T) {
|
||||
cfg.StaticRootPath = "../../public/"
|
||||
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg, Bus: bus.New(),
|
||||
Cfg: cfg,
|
||||
pluginStore: &fakePluginStore{},
|
||||
SQLStore: mockstore.NewSQLStoreMock(),
|
||||
}
|
||||
@@ -96,7 +95,7 @@ func newTestLive(t *testing.T, store *sqlstore.SQLStore) *live.GrafanaLive {
|
||||
nil,
|
||||
&usagestats.UsageStatsMock{T: t},
|
||||
nil,
|
||||
features, nil)
|
||||
features, accesscontrolmock.New())
|
||||
require.NoError(t, err)
|
||||
return gLive
|
||||
}
|
||||
@@ -957,31 +956,17 @@ func (hs *HTTPServer) callGetDashboard(sc *scenarioContext) {
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) callGetDashboardVersion(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionQuery) error {
|
||||
query.Result = &models.DashboardVersion{}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = hs.GetDashboardVersion
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) callGetDashboardVersions(sc *scenarioContext) {
|
||||
bus.AddHandler("test", func(ctx context.Context, query *models.GetDashboardVersionsQuery) error {
|
||||
query.Result = []*models.DashboardVersionDTO{}
|
||||
return nil
|
||||
})
|
||||
|
||||
sc.handlerFunc = hs.GetDashboardVersions
|
||||
sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec()
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) callDeleteDashboardByUID(t *testing.T,
|
||||
sc *scenarioContext, mockDashboard *dashboards.FakeDashboardService) {
|
||||
bus.AddHandler("test", func(ctx context.Context, cmd *models.DeleteDashboardCommand) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
hs.dashboardService = mockDashboard
|
||||
sc.handlerFunc = hs.DeleteDashboardByUID
|
||||
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
|
||||
@@ -1003,11 +988,8 @@ func callPostDashboardShouldReturnSuccess(sc *scenarioContext) {
|
||||
|
||||
func postDashboardScenario(t *testing.T, desc string, url string, routePattern string, cmd models.SaveDashboardCommand, dashboardService dashboards.DashboardService, folderService dashboards.FolderService, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: cfg,
|
||||
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||
Live: newTestLive(t, sqlstore.InitTestDB(t)),
|
||||
@@ -1040,12 +1022,9 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
|
||||
func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, role models.RoleType, fn scenarioFunc, sqlmock sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
hs := HTTPServer{
|
||||
Cfg: cfg,
|
||||
Bus: bus.GetBus(),
|
||||
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||
Live: newTestLive(t, sqlstore.InitTestDB(t)),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
@@ -1076,13 +1055,10 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
|
||||
|
||||
func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore sqlstore.Store) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
mockSQLStore := mockstore.NewSQLStoreMock()
|
||||
hs := HTTPServer{
|
||||
Cfg: cfg,
|
||||
Bus: bus.GetBus(),
|
||||
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||
Live: newTestLive(t, sqlstore.InitTestDB(t)),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
@@ -1115,8 +1091,8 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
|
||||
}
|
||||
|
||||
func (sc *scenarioContext) ToJSON() *simplejson.Json {
|
||||
var result *simplejson.Json
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(&result)
|
||||
result := simplejson.New()
|
||||
err := json.NewDecoder(sc.resp.Body).Decode(result)
|
||||
require.NoError(sc.t, err)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -56,22 +56,23 @@ const (
|
||||
)
|
||||
|
||||
type NavLink struct {
|
||||
Id string `json:"id,omitempty"`
|
||||
Text string `json:"text"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Section string `json:"section,omitempty"`
|
||||
SubTitle string `json:"subTitle,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Img string `json:"img,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
SortWeight int64 `json:"sortWeight,omitempty"`
|
||||
Divider bool `json:"divider,omitempty"`
|
||||
HideFromMenu bool `json:"hideFromMenu,omitempty"`
|
||||
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||
Children []*NavLink `json:"children,omitempty"`
|
||||
HighlightText string `json:"highlightText,omitempty"`
|
||||
HighlightID string `json:"highlightId,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Text string `json:"text"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Section string `json:"section,omitempty"`
|
||||
SubTitle string `json:"subTitle,omitempty"`
|
||||
Icon string `json:"icon,omitempty"`
|
||||
Img string `json:"img,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
SortWeight int64 `json:"sortWeight,omitempty"`
|
||||
Divider bool `json:"divider,omitempty"`
|
||||
HideFromMenu bool `json:"hideFromMenu,omitempty"`
|
||||
HideFromTabs bool `json:"hideFromTabs,omitempty"`
|
||||
ShowIconInNavbar bool `json:"showIconInNavbar,omitempty"`
|
||||
Children []*NavLink `json:"children,omitempty"`
|
||||
HighlightText string `json:"highlightText,omitempty"`
|
||||
HighlightID string `json:"highlightId,omitempty"`
|
||||
}
|
||||
|
||||
// NavIDCfg is the id for org configuration navigation node
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
service "github.com/grafana/grafana/pkg/services/dashboards/manager"
|
||||
@@ -355,8 +354,6 @@ func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) {
|
||||
|
||||
func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) {
|
||||
t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
sc := setupScenarioContext(t, ctx.url)
|
||||
|
||||
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -138,10 +137,7 @@ func callCreateFolder(sc *scenarioContext) {
|
||||
func createFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
|
||||
cmd models.CreateFolderCommand, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
t.Cleanup(bus.ClearBusHandlers)
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
Cfg: setting.NewCfg(),
|
||||
folderService: folderService,
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
@@ -170,8 +166,6 @@ func callUpdateFolder(sc *scenarioContext) {
|
||||
func updateFolderScenario(t *testing.T, desc string, url string, routePattern string, folderService dashboards.FolderService,
|
||||
cmd models.UpdateFolderCommand, fn scenarioFunc) {
|
||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
hs := HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
folderService: folderService,
|
||||
|
||||
@@ -113,6 +113,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *models.ReqContext) (map[string]i
|
||||
"rudderstackDataPlaneUrl": setting.RudderstackDataPlaneUrl,
|
||||
"rudderstackSdkUrl": setting.RudderstackSdkUrl,
|
||||
"rudderstackConfigUrl": setting.RudderstackConfigUrl,
|
||||
"feedbackLinksEnabled": hs.Cfg.FeedbackLinksEnabled,
|
||||
"applicationInsightsConnectionString": hs.Cfg.ApplicationInsightsConnectionString,
|
||||
"applicationInsightsEndpointUrl": hs.Cfg.ApplicationInsightsEndpointUrl,
|
||||
"disableLoginForm": setting.DisableLoginForm,
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
@@ -45,7 +44,6 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
|
||||
hs := &HTTPServer{
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Bus: bus.GetBus(),
|
||||
License: &licensing.OSSLicensingService{Cfg: cfg},
|
||||
RenderService: &rendering.RenderingService{
|
||||
Cfg: cfg,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user